편집 요약 없음 태그: 수동 되돌리기 |
편집 요약 없음 |
||
| 1번째 줄: | 1번째 줄: | ||
/** | /** | ||
* MediaWiki 커스텀 펼치기/접기 애니메이션 | * MediaWiki 커스텀 펼치기/접기 슬라이드 애니메이션 | ||
* | * | ||
* 지원 | * 같은 mw-customcollapsible-XXX ID가 2개 존재하는 틀 지원 | ||
* | * | ||
* | * 예: | ||
* | * | ||
* | * [첫 번째] | ||
* <div id="mw-customcollapsible-XXX"> | |||
* <div class="mw-customtoggle-XXX">[ 버튼 ]</div> | |||
* </div> | |||
* | |||
* [두 번째] | |||
* <div id="mw-customcollapsible-XXX" class="mw-collapsed"> | |||
* <div class="mw-customtoggle-XXX">[ 버튼 ]</div> | |||
* <table>내용</table> | |||
* </div> | |||
* | |||
* 틀 원본 수정 필요 없음. | |||
*/ | */ | ||
| 14번째 줄: | 23번째 줄: | ||
'use strict'; | 'use strict'; | ||
var DURATION = | var DURATION = 350; | ||
var ANIMATING_CLASS = 'mw- | |||
var INITIALIZED = 'data-slide-collapse-ready'; | |||
var CONTENT_CLASS = 'mw-slide-collapse-content'; | |||
var ANIMATING_CLASS = 'mw-slide-collapse-animating'; | |||
/* | /* -------------------------------- | ||
* mw-customtoggle-XXX 에서 XXX 추출 | |||
* -------------------------------- */ | |||
* mw-customtoggle- | function getName( toggle ) { | ||
* | |||
function | |||
var prefix = 'mw-customtoggle-'; | var prefix = 'mw-customtoggle-'; | ||
var classes = toggle.classList; | var classes = toggle.classList; | ||
| 31번째 줄: | 41번째 줄: | ||
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 | |||
); | |||
} | } | ||
} | } | ||
| 38번째 줄: | 50번째 줄: | ||
} | } | ||
/** | |||
* | /* -------------------------------- | ||
*/ | * 같은 ID를 가진 collapsible 전부 찾기 | ||
function | * | ||
if ( | * getElementById() 사용하지 않음! | ||
return | * -------------------------------- */ | ||
function getTargets( name ) { | |||
if ( !name ) { | |||
return []; | |||
} | } | ||
return | var id = | ||
'mw-customcollapsible-' + name; | |||
return Array.prototype.slice.call( | |||
document.querySelectorAll( | |||
'[id="' + | |||
CSS.escape( id ) + | |||
'"]' | |||
) | |||
); | ); | ||
} | } | ||
/* -------------------------------- | |||
* 실제 내용이 들어있는 collapsible 찾기 | |||
* | |||
* table 등이 들어있는 두 번째 div를 선택 | |||
* -------------------------------- */ | |||
function getContentTarget( name ) { | |||
var targets = getTargets( name ); | |||
var i; | |||
target = | /* | ||
* 버튼 외에 다른 요소가 들어있는 target을 찾는다. | |||
*/ | |||
for ( i = 0; i < targets.length; i++ ) { | |||
var target = targets[ i ]; | |||
var toggle = target.querySelector( | |||
'[class*="mw-customtoggle-"]' | |||
); | |||
if ( !toggle ) { | |||
continue; | |||
} | |||
var node = toggle.nextElementSibling; | |||
if ( node ) { | |||
return target; | |||
} | |||
} | } | ||
return | return null; | ||
} | } | ||
/* | |||
* | /* -------------------------------- | ||
* 실제 내용 wrapper 생성 | |||
* | * | ||
* table | * table 자체의 높이를 변경하지 않고 | ||
* | * 바깥 DIV 높이를 애니메이션 | ||
*/ | * -------------------------------- */ | ||
function prepareContent( target ) { | |||
var existing = | |||
target.querySelector( | |||
':scope > .' + CONTENT_CLASS | |||
); | |||
if ( existing ) { | |||
return existing; | |||
} | |||
var toggle = | |||
target.querySelector( | |||
':scope > [class*="mw-customtoggle-"]' | |||
); | |||
if ( !toggle ) { | |||
return null; | |||
} | |||
var firstContent = | |||
toggle.nextSibling; | |||
if ( !firstContent ) { | |||
return null; | |||
} | |||
var wrapper = | |||
document.createElement( 'div' ); | |||
wrapper.className = | |||
CONTENT_CLASS; | |||
/* | |||
* toggle 뒤의 모든 노드를 wrapper로 이동 | |||
*/ | |||
var node = firstContent; | |||
var nodes = []; | |||
while ( node ) { | |||
nodes.push( node ); | |||
node = node.nextSibling; | |||
} | } | ||
nodes.forEach( function ( child ) { | |||
wrapper.appendChild( child ); | |||
} ); | |||
target.appendChild( wrapper ); | |||
return wrapper; | |||
} | } | ||
/* -------------------------------- | |||
* 같은 이름의 토글들 aria 상태 변경 | |||
* -------------------------------- */ | |||
function setToggleState( name, expanded ) { | |||
var selector = | |||
'.mw-customtoggle-' + | |||
CSS.escape( name ); | |||
var toggles = | |||
document.querySelectorAll( selector ); | |||
toggles.forEach( function ( toggle ) { | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
expanded ? 'true' : 'false' | |||
); | |||
} ); | |||
} | } | ||
/* | |||
/* -------------------------------- | |||
* 펼치기 | * 펼치기 | ||
*/ | * -------------------------------- */ | ||
function expand( target, content | |||
if ( content.classList.contains( ANIMATING_CLASS ) ) { | function expand( name, target, content ) { | ||
if ( | |||
content.classList.contains( | |||
ANIMATING_CLASS | |||
) | |||
) { | |||
return; | return; | ||
} | } | ||
content.classList.add( ANIMATING_CLASS ); | content.classList.add( | ||
ANIMATING_CLASS | |||
); | |||
target.classList.remove( 'mw-collapsed' ); | /* | ||
* MediaWiki 접힌 상태 제거 | |||
*/ | |||
target.classList.remove( | |||
'mw-collapsed' | |||
); | |||
/* | /* | ||
* display:none | * 부모 자체가 display:none 된 경우 복구 | ||
* | */ | ||
target.style.display = | |||
target.dataset.originalDisplay || | |||
'inline-block'; | |||
/* | |||
* 내용 표시 | |||
*/ | |||
content.style.display = 'block'; | |||
content.style.overflow = 'hidden'; | |||
/* | |||
* 자연 높이를 먼저 계산 | |||
*/ | */ | ||
content.style.height = 'auto'; | content.style.height = 'auto'; | ||
var | var endHeight = | ||
content.scrollHeight; | |||
/* | /* | ||
* | * 시작 높이 | ||
*/ | */ | ||
content.style.height = '0px'; | content.style.height = '0px'; | ||
/* | /* | ||
* | * 0px 상태를 브라우저에 확정 | ||
*/ | */ | ||
content.offsetHeight; | content.offsetHeight; | ||
content.style.transition = | |||
'height ' + | |||
DURATION + | |||
'ms cubic-bezier(0.4, 0, 0.2, 1)'; | |||
requestAnimationFrame( | |||
function () { | |||
content.style.height = | |||
endHeight + 'px'; | |||
{ | |||
} | } | ||
); | ); | ||
setToggleState( | |||
name, | |||
true | |||
); | |||
window.setTimeout( | |||
function () { | |||
content.style.transition = ''; | |||
content.style.height = ''; | |||
content.style.overflow = ''; | |||
content.style.display = ''; | |||
content.classList.remove( | |||
ANIMATING_CLASS | |||
); | |||
}, | |||
DURATION + 30 | |||
); | |||
} | |||
/* -------------------------------- | |||
* 접기 | |||
* -------------------------------- */ | |||
function collapse( name, target, content ) { | |||
if ( | |||
content.classList.contains( | |||
function collapse( target, content | ANIMATING_CLASS | ||
if ( content.classList.contains( ANIMATING_CLASS ) ) { | ) | ||
) { | |||
return; | return; | ||
} | } | ||
content.classList.add( ANIMATING_CLASS ); | content.classList.add( | ||
ANIMATING_CLASS | |||
); | |||
var startHeight = | var startHeight = | ||
content.getBoundingClientRect().height; | content.getBoundingClientRect() | ||
.height; | |||
content.style.display = 'block'; | |||
content.style.display = | |||
content.style.overflow = 'hidden'; | content.style.overflow = 'hidden'; | ||
content.style.height = | |||
startHeight + 'px'; | |||
/* | /* | ||
* | * 현재 높이 확정 | ||
*/ | */ | ||
content.offsetHeight; | content.offsetHeight; | ||
content.style.transition = | |||
'height ' + | |||
DURATION + | |||
'ms cubic-bezier(0.4, 0, 0.2, 1)'; | |||
requestAnimationFrame( | |||
function () { | |||
content.style.height = | |||
'0px'; | |||
{ | |||
} | } | ||
); | ); | ||
setToggleState( | |||
name, | |||
false | |||
); | |||
content. | window.setTimeout( | ||
function () { | |||
content.style.transition = ''; | |||
content.style.height = ''; | |||
content.style.overflow = ''; | |||
content.style.display = 'none'; | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
content.classList.remove( | |||
ANIMATING_CLASS | |||
); | |||
}, | |||
DURATION + 30 | |||
); | |||
} | |||
/* -------------------------------- | |||
}; | * 현재 열린 상태인지 검사 | ||
* -------------------------------- */ | |||
function isOpen( target, content ) { | |||
if ( | |||
target.classList.contains( | |||
'mw-collapsed' | |||
) | |||
) { | |||
return false; | |||
} | |||
if ( | |||
window.getComputedStyle( | |||
content | |||
).display === 'none' | |||
) { | |||
return false; | |||
} | |||
return true; | |||
} | } | ||
/* | |||
* | /* -------------------------------- | ||
*/ | * 버튼 클릭 | ||
function | * -------------------------------- */ | ||
var name = | |||
function handleToggle( event ) { | |||
var toggle = | |||
event.target.closest( | |||
'[class*="mw-customtoggle-"]' | |||
); | |||
if ( !toggle ) { | |||
return; | |||
} | |||
var name = | |||
getName( toggle ); | |||
if ( !name ) { | if ( !name ) { | ||
return | return; | ||
} | } | ||
var target = | var target = | ||
getContentTarget( name ); | |||
/* | |||
* 우리가 처리할 수 없는 구조면 | |||
* MediaWiki 기본 기능을 그대로 사용 | |||
*/ | |||
if ( !target ) { | if ( !target ) { | ||
return | return; | ||
} | } | ||
var content = | var content = | ||
prepareContent( target ); | |||
if ( !content ) { | if ( !content ) { | ||
return | return; | ||
} | } | ||
if ( | /* | ||
* 여기까지 왔을 때만 | |||
* MediaWiki 기본 즉시 토글 차단 | |||
*/ | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
event.stopImmediatePropagation(); | |||
if ( | |||
isOpen( | |||
target, | |||
content | |||
) | |||
) { | |||
collapse( | |||
name, | |||
target, | target, | ||
content | content | ||
); | ); | ||
} else { | } else { | ||
expand( | |||
name, | |||
target, | target, | ||
content | content | ||
); | ); | ||
} | } | ||
} | |||
/* | /* -------------------------------- | ||
* | * 초기 설정 | ||
* | * -------------------------------- */ | ||
function initialize() { | |||
var toggles = | |||
document.querySelectorAll( | |||
'[class*="mw-customtoggle-"]' | |||
' | |||
); | ); | ||
if ( ! | toggles.forEach( | ||
return; | function ( toggle ) { | ||
if ( | |||
toggle.hasAttribute( | |||
INITIALIZED | |||
) | |||
) { | |||
return; | |||
} | |||
var name = | |||
getName( toggle ); | |||
if ( !name ) { | |||
return; | |||
} | |||
var target = | |||
getContentTarget( | |||
name | |||
); | |||
if ( !target ) { | |||
return; | |||
} | |||
var content = | |||
prepareContent( | |||
target | |||
); | |||
if ( !content ) { | |||
return; | |||
} | |||
/* | |||
* 원래 display 기억 | |||
*/ | |||
if ( | |||
!target.dataset.originalDisplay | |||
) { | |||
target.dataset.originalDisplay = | |||
target.style.display || | |||
'inline-block'; | |||
} | |||
/* | |||
* 원래 mw-collapsed인 경우 | |||
* 내용만 숨긴다. | |||
* | |||
* 버튼 부모 전체를 숨기지 않음. | |||
*/ | |||
if ( | |||
target.classList.contains( | |||
'mw-collapsed' | |||
) | |||
) { | |||
content.style.display = | |||
'none'; | |||
setToggleState( | |||
name, | |||
false | |||
); | |||
} | |||
toggle.setAttribute( | |||
INITIALIZED, | |||
'1' | |||
); | |||
} | } | ||
); | |||
} | |||
/* -------------------------------- | |||
* 클릭 이벤트 | |||
* | |||
* capture에서 처리하되, | |||
* 실제 target을 찾은 경우에만 | |||
* MediaWiki 기본 이벤트 차단 | |||
* -------------------------------- */ | |||
document.addEventListener( | |||
'click', | |||
handleToggle, | |||
true | true | ||
); | ); | ||
/* | |||
* | /* -------------------------------- | ||
*/ | * 키보드 | ||
* -------------------------------- */ | |||
document.addEventListener( | document.addEventListener( | ||
'keydown', | 'keydown', | ||
| 373번째 줄: | 554번째 줄: | ||
} | } | ||
handleToggle( event ); | |||
}, | |||
true | |||
); | |||
/* -------------------------------- | |||
* 실행 | |||
* -------------------------------- */ | |||
if ( | |||
document.readyState === 'loading' | |||
) { | |||
document.addEventListener( | |||
'DOMContentLoaded', | |||
initialize | |||
); | |||
} else { | |||
initialize(); | |||
} | |||
/* | |||
* MediaWiki 동적 로딩 대응 | |||
*/ | |||
if ( | |||
window.mw && | |||
mw.hook | |||
) { | |||
mw.hook( | |||
'wikipage.content' | |||
).add( | |||
function () { | |||
initialize(); | |||
} | |||
); | |||
} | |||
}() ); | }() ); | ||
2026년 8월 20일 (목) 13:30 판
/**
* MediaWiki 커스텀 펼치기/접기 슬라이드 애니메이션
*
* 같은 mw-customcollapsible-XXX ID가 2개 존재하는 틀 지원
*
* 예:
*
* [첫 번째]
* <div id="mw-customcollapsible-XXX">
* <div class="mw-customtoggle-XXX">[ 버튼 ]</div>
* </div>
*
* [두 번째]
* <div id="mw-customcollapsible-XXX" class="mw-collapsed">
* <div class="mw-customtoggle-XXX">[ 버튼 ]</div>
* <table>내용</table>
* </div>
*
* 틀 원본 수정 필요 없음.
*/
( function () {
'use strict';
var DURATION = 350;
var INITIALIZED = 'data-slide-collapse-ready';
var CONTENT_CLASS = 'mw-slide-collapse-content';
var ANIMATING_CLASS = 'mw-slide-collapse-animating';
/* --------------------------------
* mw-customtoggle-XXX 에서 XXX 추출
* -------------------------------- */
function getName( 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;
}
/* --------------------------------
* 같은 ID를 가진 collapsible 전부 찾기
*
* getElementById() 사용하지 않음!
* -------------------------------- */
function getTargets( name ) {
if ( !name ) {
return [];
}
var id =
'mw-customcollapsible-' + name;
return Array.prototype.slice.call(
document.querySelectorAll(
'[id="' +
CSS.escape( id ) +
'"]'
)
);
}
/* --------------------------------
* 실제 내용이 들어있는 collapsible 찾기
*
* table 등이 들어있는 두 번째 div를 선택
* -------------------------------- */
function getContentTarget( name ) {
var targets = getTargets( name );
var i;
/*
* 버튼 외에 다른 요소가 들어있는 target을 찾는다.
*/
for ( i = 0; i < targets.length; i++ ) {
var target = targets[ i ];
var toggle = target.querySelector(
'[class*="mw-customtoggle-"]'
);
if ( !toggle ) {
continue;
}
var node = toggle.nextElementSibling;
if ( node ) {
return target;
}
}
return null;
}
/* --------------------------------
* 실제 내용 wrapper 생성
*
* table 자체의 높이를 변경하지 않고
* 바깥 DIV 높이를 애니메이션
* -------------------------------- */
function prepareContent( target ) {
var existing =
target.querySelector(
':scope > .' + CONTENT_CLASS
);
if ( existing ) {
return existing;
}
var toggle =
target.querySelector(
':scope > [class*="mw-customtoggle-"]'
);
if ( !toggle ) {
return null;
}
var firstContent =
toggle.nextSibling;
if ( !firstContent ) {
return null;
}
var wrapper =
document.createElement( 'div' );
wrapper.className =
CONTENT_CLASS;
/*
* toggle 뒤의 모든 노드를 wrapper로 이동
*/
var node = firstContent;
var nodes = [];
while ( node ) {
nodes.push( node );
node = node.nextSibling;
}
nodes.forEach( function ( child ) {
wrapper.appendChild( child );
} );
target.appendChild( wrapper );
return wrapper;
}
/* --------------------------------
* 같은 이름의 토글들 aria 상태 변경
* -------------------------------- */
function setToggleState( name, expanded ) {
var selector =
'.mw-customtoggle-' +
CSS.escape( name );
var toggles =
document.querySelectorAll( selector );
toggles.forEach( function ( toggle ) {
toggle.setAttribute(
'aria-expanded',
expanded ? 'true' : 'false'
);
} );
}
/* --------------------------------
* 펼치기
* -------------------------------- */
function expand( name, target, content ) {
if (
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
content.classList.add(
ANIMATING_CLASS
);
/*
* MediaWiki 접힌 상태 제거
*/
target.classList.remove(
'mw-collapsed'
);
/*
* 부모 자체가 display:none 된 경우 복구
*/
target.style.display =
target.dataset.originalDisplay ||
'inline-block';
/*
* 내용 표시
*/
content.style.display = 'block';
content.style.overflow = 'hidden';
/*
* 자연 높이를 먼저 계산
*/
content.style.height = 'auto';
var endHeight =
content.scrollHeight;
/*
* 시작 높이
*/
content.style.height = '0px';
/*
* 0px 상태를 브라우저에 확정
*/
content.offsetHeight;
content.style.transition =
'height ' +
DURATION +
'ms cubic-bezier(0.4, 0, 0.2, 1)';
requestAnimationFrame(
function () {
content.style.height =
endHeight + 'px';
}
);
setToggleState(
name,
true
);
window.setTimeout(
function () {
content.style.transition = '';
content.style.height = '';
content.style.overflow = '';
content.style.display = '';
content.classList.remove(
ANIMATING_CLASS
);
},
DURATION + 30
);
}
/* --------------------------------
* 접기
* -------------------------------- */
function collapse( name, target, content ) {
if (
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
content.classList.add(
ANIMATING_CLASS
);
var startHeight =
content.getBoundingClientRect()
.height;
content.style.display = 'block';
content.style.overflow = 'hidden';
content.style.height =
startHeight + 'px';
/*
* 현재 높이 확정
*/
content.offsetHeight;
content.style.transition =
'height ' +
DURATION +
'ms cubic-bezier(0.4, 0, 0.2, 1)';
requestAnimationFrame(
function () {
content.style.height =
'0px';
}
);
setToggleState(
name,
false
);
window.setTimeout(
function () {
content.style.transition = '';
content.style.height = '';
content.style.overflow = '';
content.style.display = 'none';
target.classList.add(
'mw-collapsed'
);
content.classList.remove(
ANIMATING_CLASS
);
},
DURATION + 30
);
}
/* --------------------------------
* 현재 열린 상태인지 검사
* -------------------------------- */
function isOpen( target, content ) {
if (
target.classList.contains(
'mw-collapsed'
)
) {
return false;
}
if (
window.getComputedStyle(
content
).display === 'none'
) {
return false;
}
return true;
}
/* --------------------------------
* 버튼 클릭
* -------------------------------- */
function handleToggle( event ) {
var toggle =
event.target.closest(
'[class*="mw-customtoggle-"]'
);
if ( !toggle ) {
return;
}
var name =
getName( toggle );
if ( !name ) {
return;
}
var target =
getContentTarget( name );
/*
* 우리가 처리할 수 없는 구조면
* MediaWiki 기본 기능을 그대로 사용
*/
if ( !target ) {
return;
}
var content =
prepareContent( target );
if ( !content ) {
return;
}
/*
* 여기까지 왔을 때만
* MediaWiki 기본 즉시 토글 차단
*/
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (
isOpen(
target,
content
)
) {
collapse(
name,
target,
content
);
} else {
expand(
name,
target,
content
);
}
}
/* --------------------------------
* 초기 설정
* -------------------------------- */
function initialize() {
var toggles =
document.querySelectorAll(
'[class*="mw-customtoggle-"]'
);
toggles.forEach(
function ( toggle ) {
if (
toggle.hasAttribute(
INITIALIZED
)
) {
return;
}
var name =
getName( toggle );
if ( !name ) {
return;
}
var target =
getContentTarget(
name
);
if ( !target ) {
return;
}
var content =
prepareContent(
target
);
if ( !content ) {
return;
}
/*
* 원래 display 기억
*/
if (
!target.dataset.originalDisplay
) {
target.dataset.originalDisplay =
target.style.display ||
'inline-block';
}
/*
* 원래 mw-collapsed인 경우
* 내용만 숨긴다.
*
* 버튼 부모 전체를 숨기지 않음.
*/
if (
target.classList.contains(
'mw-collapsed'
)
) {
content.style.display =
'none';
setToggleState(
name,
false
);
}
toggle.setAttribute(
INITIALIZED,
'1'
);
}
);
}
/* --------------------------------
* 클릭 이벤트
*
* capture에서 처리하되,
* 실제 target을 찾은 경우에만
* MediaWiki 기본 이벤트 차단
* -------------------------------- */
document.addEventListener(
'click',
handleToggle,
true
);
/* --------------------------------
* 키보드
* -------------------------------- */
document.addEventListener(
'keydown',
function ( event ) {
if (
event.key !== 'Enter' &&
event.key !== ' '
) {
return;
}
handleToggle( event );
},
true
);
/* --------------------------------
* 실행
* -------------------------------- */
if (
document.readyState === 'loading'
) {
document.addEventListener(
'DOMContentLoaded',
initialize
);
} else {
initialize();
}
/*
* MediaWiki 동적 로딩 대응
*/
if (
window.mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function () {
initialize();
}
);
}
}() );