편집 요약 없음 |
편집 요약 없음 태그: 되돌려진 기여 |
||
| 1번째 줄: | 1번째 줄: | ||
/** | /** | ||
* MediaWiki | * MediaWiki custom toggle 애니메이션 | ||
* | * | ||
* | * 예: | ||
* | * | ||
* | * mw-customtoggle-MALE | ||
* | * ↕ | ||
* | * mw-customcollapsible-MALE | ||
* | * | ||
* | * mw-customtoggle-FEMALE | ||
* ↕ | |||
* mw-customcollapsible-FEMALE | |||
* | |||
* 여러 개를 동시에 사용해도 각각 독립적으로 작동합니다. | |||
*/ | */ | ||
| 18번째 줄: | 22번째 줄: | ||
/** | /** | ||
* mw-customtoggle-XXX 에서 XXX 추출 | |||
* mw-customtoggle- | |||
*/ | */ | ||
function getToggleName( toggle ) { | function getToggleName( toggle ) { | ||
| 39번째 줄: | 39번째 줄: | ||
/** | /** | ||
* CSS | * CSS 선택자 escape | ||
*/ | */ | ||
function escapeSelector( value ) { | function escapeSelector( value ) { | ||
| 53번째 줄: | 53번째 줄: | ||
/** | /** | ||
* | * 해당 이름에 연결된 모든 대상 찾기 | ||
*/ | */ | ||
function | function getTargets( name ) { | ||
if ( !name ) { | if ( !name ) { | ||
return | return []; | ||
} | } | ||
var | var selector = | ||
'.mw-customcollapsible-' + | '.mw-customcollapsible-' + | ||
escapeSelector( name | escapeSelector( name ); | ||
var targets = | |||
Array.prototype.slice.call( | |||
document.querySelectorAll( selector ) | |||
); | |||
/* | |||
* id 형식도 지원 | |||
*/ | |||
var idTarget = document.getElementById( | |||
'mw-customcollapsible-' + name | 'mw-customcollapsible-' + name | ||
); | ); | ||
return | if ( | ||
idTarget && | |||
targets.indexOf( idTarget ) === -1 | |||
) { | |||
targets.push( idTarget ); | |||
} | |||
return targets; | |||
} | } | ||
/** | /** | ||
* 실제 | * 실제 애니메이션 대상 결정 | ||
* | |||
* 중요: | |||
* 토글 버튼 자체가 들어있는 요소 전체를 | |||
* display:none으로 만들지 않도록 한다. | |||
*/ | */ | ||
function getContent( target ) { | function getContent( target, toggle ) { | ||
var content = target.querySelector( | var content = target.querySelector( | ||
'.mw-collapsible-content' | '.mw-collapsible-content' | ||
| 88번째 줄: | 102번째 줄: | ||
} | } | ||
return target; | /* | ||
* target 자체가 toggle을 포함하지 않는다면 | |||
* target 자체를 접어도 안전하다. | |||
*/ | |||
if ( !target.contains( toggle ) ) { | |||
return target; | |||
} | |||
/* | |||
* toggle이 target 내부에 있다면 | |||
* target 전체를 숨기면 버튼까지 사라지므로 | |||
* 애니메이션하지 않는다. | |||
*/ | |||
return null; | |||
} | } | ||
/** | /** | ||
* | * 태그의 자연스러운 display 값 | ||
*/ | */ | ||
function getNaturalDisplay( element ) { | function getNaturalDisplay( element ) { | ||
| 136번째 줄: | 160번째 줄: | ||
/** | /** | ||
* | * 접힌 상태인지 확인 | ||
*/ | */ | ||
function isCollapsed( target, content ) { | function isCollapsed( target, content ) { | ||
| 143번째 줄: | 167번째 줄: | ||
} | } | ||
return window.getComputedStyle( content ).display === 'none'; | return ( | ||
window.getComputedStyle( content ).display === | |||
'none' | |||
); | |||
} | } | ||
| 149번째 줄: | 176번째 줄: | ||
* 펼치기 | * 펼치기 | ||
*/ | */ | ||
function expand( target, content | function expand( target, content ) { | ||
if ( content.classList.contains( ANIMATING_CLASS ) ) { | if ( | ||
content.classList.contains( | |||
ANIMATING_CLASS | |||
) | |||
) { | |||
return; | return; | ||
} | } | ||
content.classList.add( ANIMATING_CLASS ); | content.classList.add( | ||
ANIMATING_CLASS | |||
); | |||
target.classList.remove( | |||
'mw-collapsed' | |||
); | |||
content.style.display = | |||
getNaturalDisplay( content ); | |||
content.style.height = 'auto'; | content.style.height = 'auto'; | ||
content.style.overflow = 'hidden'; | content.style.overflow = 'hidden'; | ||
var targetHeight = content.scrollHeight; | var targetHeight = | ||
content.scrollHeight; | |||
content.style.height = '0px'; | content.style.height = '0px'; | ||
/* | /* | ||
* | * reflow | ||
*/ | */ | ||
content.offsetHeight; | content.offsetHeight; | ||
var animation = content.animate( | var animation = | ||
content.animate( | |||
[ | |||
height: ' | { | ||
height: '0px' | |||
}, | |||
{ | |||
height: | |||
targetHeight + 'px' | |||
} | |||
], | |||
{ | { | ||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | } | ||
); | |||
animation.onfinish = function () { | animation.onfinish = function () { | ||
content.classList.remove( ANIMATING_CLASS ); | content.classList.remove( | ||
ANIMATING_CLASS | |||
); | |||
/* | /* | ||
* 원래 | * 애니메이션 후 원래 CSS 상태로 복구 | ||
*/ | */ | ||
content.style.display = ''; | content.style.display = ''; | ||
content.style.height = ''; | content.style.height = ''; | ||
content.style.overflow = ''; | content.style.overflow = ''; | ||
animation.cancel(); | animation.cancel(); | ||
}; | }; | ||
} | } | ||
| 221번째 줄: | 246번째 줄: | ||
* 접기 | * 접기 | ||
*/ | */ | ||
function collapse( target, content | function collapse( target, content ) { | ||
if ( content.classList.contains( 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 = | |||
getNaturalDisplay( content ); | |||
content.style.height = | |||
startHeight + 'px'; | |||
content.style.overflow = | |||
'hidden'; | |||
content.style. | |||
content.offsetHeight; | content.offsetHeight; | ||
var animation = content.animate( | var animation = | ||
content.animate( | |||
[ | |||
{ | |||
height: | |||
startHeight + 'px' | |||
}, | |||
{ | |||
height: '0px' | |||
} | |||
], | |||
{ | { | ||
duration: DURATION, | |||
easing: 'ease', | |||
fill: 'forwards' | |||
} | } | ||
); | |||
animation.onfinish = function () { | animation.onfinish = function () { | ||
target.classList.add( 'mw-collapsed' ); | target.classList.add( | ||
'mw-collapsed' | |||
); | |||
content.classList.remove( ANIMATING_CLASS ); | content.classList.remove( | ||
ANIMATING_CLASS | |||
); | |||
content.style.display = | |||
'none'; | |||
content.style.display = 'none'; | |||
content.style.height = ''; | content.style.height = ''; | ||
content.style.overflow = ''; | content.style.overflow = ''; | ||
animation.cancel(); | animation.cancel(); | ||
}; | }; | ||
} | } | ||
/** | /** | ||
* 실제 토글 | * 실제 토글 실행 | ||
*/ | */ | ||
function toggleCollapsible( toggle ) { | function toggleCollapsible( toggle ) { | ||
var name = getToggleName( toggle ); | var name = | ||
getToggleName( toggle ); | |||
if ( !name ) { | if ( !name ) { | ||
| 301번째 줄: | 322번째 줄: | ||
} | } | ||
var | var targets = | ||
getTargets( name ); | |||
if ( ! | if ( !targets.length ) { | ||
return false; | return false; | ||
} | } | ||
var content = getContent( target ); | var handled = false; | ||
targets.forEach( | |||
function ( target ) { | |||
var content = | |||
getContent( | |||
target, | |||
toggle | |||
); | |||
/* | |||
* 버튼을 포함하고 있는 요소 자체라면 | |||
* 숨기지 않는다. | |||
*/ | |||
if ( !content ) { | |||
return; | |||
} | |||
if ( | |||
isCollapsed( | |||
target, | |||
content | |||
) | |||
) { | |||
expand( | |||
target, | |||
content | |||
); | |||
} else { | |||
collapse( | |||
target, | |||
content | |||
); | |||
} | |||
handled = true; | |||
} | |||
); | |||
if ( | /* | ||
* 버튼 aria 상태도 변경 | |||
*/ | |||
if ( handled ) { | |||
var expanded = | |||
toggle.getAttribute( | |||
'aria-expanded' | |||
) === 'true'; | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
expanded | |||
? 'false' | |||
: 'true' | |||
); | ); | ||
} | } | ||
return | return handled; | ||
} | } | ||
/** | /** | ||
* | * 클릭 처리 | ||
*/ | */ | ||
document.addEventListener( | document.addEventListener( | ||
'click', | 'click', | ||
function ( event ) { | function ( event ) { | ||
var toggle = event.target.closest( | var toggle = | ||
event.target.closest( | |||
'.mw-customtoggle' | |||
); | |||
if ( !toggle ) { | if ( !toggle ) { | ||
| 347번째 줄: | 403번째 줄: | ||
} | } | ||
if ( !getToggleName( toggle ) ) { | if ( | ||
!getToggleName( toggle ) | |||
) { | |||
return; | return; | ||
} | } | ||
event.preventDefault(); | event.preventDefault(); | ||
event.stopPropagation(); | event.stopPropagation(); | ||
event.stopImmediatePropagation(); | event.stopImmediatePropagation(); | ||
toggleCollapsible( toggle ); | toggleCollapsible( | ||
toggle | |||
); | |||
}, | }, | ||
true | true | ||
| 361번째 줄: | 423번째 줄: | ||
/** | /** | ||
* Enter / Space | * 키보드 Enter / Space | ||
*/ | */ | ||
document.addEventListener( | document.addEventListener( | ||
| 373번째 줄: | 435번째 줄: | ||
} | } | ||
var toggle = event.target.closest( | var toggle = | ||
event.target.closest( | |||
'.mw-customtoggle' | |||
); | |||
if ( !toggle ) { | if ( !toggle ) { | ||
| 381번째 줄: | 444번째 줄: | ||
} | } | ||
if ( !getToggleName( toggle ) ) { | if ( | ||
!getToggleName( toggle ) | |||
) { | |||
return; | return; | ||
} | } | ||
event.preventDefault(); | event.preventDefault(); | ||
event.stopPropagation(); | event.stopPropagation(); | ||
event.stopImmediatePropagation(); | event.stopImmediatePropagation(); | ||
toggleCollapsible( toggle ); | toggleCollapsible( | ||
toggle | |||
); | |||
}, | }, | ||
true | true | ||
2026년 8월 20일 (목) 13:17 판
/**
* MediaWiki custom toggle 애니메이션
*
* 예:
*
* mw-customtoggle-MALE
* ↕
* mw-customcollapsible-MALE
*
* mw-customtoggle-FEMALE
* ↕
* mw-customcollapsible-FEMALE
*
* 여러 개를 동시에 사용해도 각각 독립적으로 작동합니다.
*/
( function () {
'use strict';
var DURATION = 300;
var ANIMATING_CLASS = 'mw-custom-collapse-animating';
/**
* mw-customtoggle-XXX 에서 XXX 추출
*/
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 선택자 escape
*/
function escapeSelector( value ) {
if ( window.CSS && CSS.escape ) {
return CSS.escape( value );
}
return value.replace(
/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g,
'\\\\$1'
);
}
/**
* 해당 이름에 연결된 모든 대상 찾기
*/
function getTargets( name ) {
if ( !name ) {
return [];
}
var selector =
'.mw-customcollapsible-' +
escapeSelector( name );
var targets =
Array.prototype.slice.call(
document.querySelectorAll( selector )
);
/*
* id 형식도 지원
*/
var idTarget = document.getElementById(
'mw-customcollapsible-' + name
);
if (
idTarget &&
targets.indexOf( idTarget ) === -1
) {
targets.push( idTarget );
}
return targets;
}
/**
* 실제 애니메이션 대상 결정
*
* 중요:
* 토글 버튼 자체가 들어있는 요소 전체를
* display:none으로 만들지 않도록 한다.
*/
function getContent( target, toggle ) {
var content = target.querySelector(
'.mw-collapsible-content'
);
if ( content ) {
return content;
}
/*
* target 자체가 toggle을 포함하지 않는다면
* target 자체를 접어도 안전하다.
*/
if ( !target.contains( toggle ) ) {
return target;
}
/*
* toggle이 target 내부에 있다면
* target 전체를 숨기면 버튼까지 사라지므로
* 애니메이션하지 않는다.
*/
return null;
}
/**
* 태그의 자연스러운 display 값
*/
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 ) {
if (
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
content.classList.add(
ANIMATING_CLASS
);
target.classList.remove(
'mw-collapsed'
);
content.style.display =
getNaturalDisplay( content );
content.style.height = 'auto';
content.style.overflow = 'hidden';
var targetHeight =
content.scrollHeight;
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 = '';
animation.cancel();
};
}
/**
* 접기
*/
function collapse( target, content ) {
if (
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
content.classList.add(
ANIMATING_CLASS
);
var startHeight =
content.getBoundingClientRect()
.height;
content.style.display =
getNaturalDisplay( content );
content.style.height =
startHeight + 'px';
content.style.overflow =
'hidden';
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';
content.style.height = '';
content.style.overflow = '';
animation.cancel();
};
}
/**
* 실제 토글 실행
*/
function toggleCollapsible( toggle ) {
var name =
getToggleName( toggle );
if ( !name ) {
return false;
}
var targets =
getTargets( name );
if ( !targets.length ) {
return false;
}
var handled = false;
targets.forEach(
function ( target ) {
var content =
getContent(
target,
toggle
);
/*
* 버튼을 포함하고 있는 요소 자체라면
* 숨기지 않는다.
*/
if ( !content ) {
return;
}
if (
isCollapsed(
target,
content
)
) {
expand(
target,
content
);
} else {
collapse(
target,
content
);
}
handled = true;
}
);
/*
* 버튼 aria 상태도 변경
*/
if ( handled ) {
var expanded =
toggle.getAttribute(
'aria-expanded'
) === 'true';
toggle.setAttribute(
'aria-expanded',
expanded
? 'false'
: 'true'
);
}
return handled;
}
/**
* 클릭 처리
*/
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
);
}() );