편집 요약 없음 |
편집 요약 없음 |
||
| 2번째 줄: | 2번째 줄: | ||
* MediaWiki 커스텀 펼치기 / 접기 애니메이션 | * MediaWiki 커스텀 펼치기 / 접기 애니메이션 | ||
* | * | ||
* 지원 구조 | * 지원: | ||
* | |||
* 1. 일반 MediaWiki 구조 | |||
* | * | ||
* .mw-customtoggle-XXX | * .mw-customtoggle-XXX | ||
* #mw-customcollapsible-XXX | * #mw-customcollapsible-XXX | ||
* | * └─ .mw-collapsible-content | ||
* | |||
* | |||
* 2. 동일 ID를 2개 사용하는 전환형 둘러보기 틀 | |||
* | |||
* #mw-customcollapsible-XXX ← 처음 보이는 버튼 | |||
* └─ .mw-customtoggle-XXX | |||
* | |||
* #mw-customcollapsible-XXX ← 처음 숨겨진 실제 내용 | |||
* ├─ .mw-customtoggle-XXX | |||
* └─ table / .sakura-table-wrapper | |||
* | * | ||
* | * | ||
* | * 클릭 시 같은 XXX를 가진 모든 collapsible을 | ||
* | * 각각 현재 상태의 반대로 전환합니다. | ||
*/ | */ | ||
| 24번째 줄: | 30번째 줄: | ||
var DURATION = 300; | var DURATION = 300; | ||
var EASING = 'swing'; | var EASING = 'swing'; | ||
/** | /** | ||
* 토글 | * 토글 이름 추출 | ||
* | * | ||
* mw-customtoggle-Flora_LACTEA_01 | * mw-customtoggle-Flora_LACTEA_01 | ||
| 59번째 줄: | 58번째 줄: | ||
/** | /** | ||
* | * attribute selector escape | ||
*/ | */ | ||
function escapeAttribute( value ) { | function escapeAttribute( value ) { | ||
| 69번째 줄: | 68번째 줄: | ||
/** | /** | ||
* | * 동일한 이름을 사용하는 | ||
* 모든 collapsible 대상 가져오기 | |||
*/ | |||
function getTargets( name ) { | |||
var id = | |||
'mw-customcollapsible-' + name; | |||
return document.querySelectorAll( | |||
'[id="' + | |||
escapeAttribute( id ) + | |||
'"]' | |||
); | |||
} | |||
/** | |||
* 직계 .mw-collapsible-content 찾기 | |||
* | * | ||
* | * 중첩 collapsible까지 잘못 선택하지 않도록 | ||
* | * children만 검사합니다. | ||
*/ | */ | ||
function | function getDirectContent( target ) { | ||
var children = target.children; | var children = target.children; | ||
var i; | var i; | ||
| 93번째 줄: | 108번째 줄: | ||
/** | /** | ||
* | * 실제 애니메이션 대상 | ||
* | * | ||
* .mw-collapsible- | * MediaWiki 표준: | ||
* .mw-collapsible-content | |||
* | * | ||
* | * 현재 LACTEA 구조: | ||
* target | * target 자체 | ||
*/ | */ | ||
function getAnimatedElement( target ) { | function getAnimatedElement( target ) { | ||
var content = | var content = | ||
getDirectContent( target ); | |||
if ( content ) { | if ( content ) { | ||
| 113번째 줄: | 129번째 줄: | ||
/** | /** | ||
* | * 현재 접혀 있는지 확인 | ||
*/ | */ | ||
function | function isCollapsed( target, element ) { | ||
if ( | if ( | ||
return | target.classList.contains( | ||
'mw-collapsed' | |||
) | |||
) { | |||
return true; | |||
} | } | ||
return | return ( | ||
window.getComputedStyle( | |||
element | |||
).display === 'none' | |||
); | ); | ||
} | } | ||
| 129번째 줄: | 150번째 줄: | ||
/** | /** | ||
* | * 펼칠 때 사용할 display 계산 | ||
*/ | */ | ||
function | function getOpenDisplay( element ) { | ||
/* | /* | ||
* 이전에 | * 이전에 저장된 display가 있으면 사용 | ||
*/ | */ | ||
var | var stored = | ||
$( element ).data( | $( element ).data( | ||
'mwAnimatedDisplay' | |||
); | ); | ||
if ( | if ( stored ) { | ||
return | return stored; | ||
} | } | ||
| 306번째 줄: | 173번째 줄: | ||
/* | |||
* 현재 보이는 상태라면 저장 | |||
*/ | |||
if ( | if ( | ||
display && | display && | ||
display !== 'none' | display !== 'none' | ||
) { | ) { | ||
$( element ).data( | $( element ).data( | ||
'mwAnimatedDisplay', | |||
display | display | ||
); | ); | ||
| 319번째 줄: | 190번째 줄: | ||
/* | |||
* mw-collapsible 자체를 펼치는 구조 | |||
*/ | |||
if ( | |||
element.classList.contains( | |||
'mw-collapsible' | |||
) | |||
) { | |||
/* | |||
* width가 100%인 내용 영역은 block이 | |||
* 가장 자연스럽습니다. | |||
*/ | |||
if ( | |||
element.style.width === '100%' | |||
) { | |||
return 'block'; | |||
} | |||
return 'inline-block'; | |||
} | |||
switch ( element.tagName ) { | |||
case ' | case 'TABLE': | ||
return 'table'; | |||
case 'TR': | case 'TR': | ||
return 'table-row'; | |||
case 'TD': | case 'TD': | ||
case 'TH': | case 'TH': | ||
return 'table-cell'; | |||
case 'LI': | case 'LI': | ||
return 'list-item'; | |||
default: | default: | ||
return 'block'; | |||
} | } | ||
} | } | ||
/** | /** | ||
* 현재 | * 현재 display 값을 기억 | ||
* | |||
* 닫기 전에 호출해야 함 | |||
*/ | */ | ||
function | function rememberDisplay( element ) { | ||
var display = | |||
window.getComputedStyle( | |||
element | |||
).display; | |||
if ( | if ( | ||
display && | |||
display !== 'none' | |||
) { | ) { | ||
$( element ).data( | |||
'mwAnimatedDisplay', | |||
display | |||
); | |||
} | } | ||
} | } | ||
/** | /** | ||
* 같은 | * 같은 name의 모든 토글에 | ||
* aria-expanded 설정 | |||
*/ | */ | ||
function | function updateToggleAria( | ||
name, | name, | ||
expanded | expanded | ||
) { | ) { | ||
var | var className = | ||
'mw-customtoggle-' + name; | 'mw-customtoggle-' + name; | ||
var toggles = | var toggles = | ||
document.getElementsByClassName( | document.getElementsByClassName( | ||
className | |||
); | ); | ||
var i; | var i; | ||
for ( | for ( i = 0; i < toggles.length; i++ ) { | ||
toggles[ i ].setAttribute( | toggles[ i ].setAttribute( | ||
'aria-expanded', | 'aria-expanded', | ||
expanded ? 'true' : 'false' | expanded ? | ||
'true' : | |||
'false' | |||
); | ); | ||
} | } | ||
| 438번째 줄: | 292번째 줄: | ||
*/ | */ | ||
function expand( | function expand( | ||
target, | target, | ||
element | |||
) { | ) { | ||
var $element = | var $element = | ||
$( | $( element ); | ||
$element.stop( | $element.stop( | ||
true, | true, | ||
| 462번째 줄: | 312번째 줄: | ||
/* | /* | ||
* | * 표준 MediaWiki 방식 | ||
* | * | ||
* | * target 안의 | ||
* | * .mw-collapsible-content만 처리 | ||
*/ | */ | ||
if ( | if ( element !== target ) { | ||
$element | |||
.stop( true, false ) | |||
.slideDown( | |||
DURATION | |||
); | ); | ||
/* | return; | ||
} | |||
/* | |||
* LACTEA 같은 구조 | |||
* | |||
* mw-collapsible 자체가 display:none이므로 | |||
* 직접 높이를 애니메이션 | |||
*/ | |||
var display = | |||
getOpenDisplay( | |||
element | |||
); | ); | ||
$element.css( | |||
{ | |||
display: display, | |||
height: 'auto', | |||
overflow: 'hidden' | |||
} | |||
); | |||
var targetHeight = | |||
element.scrollHeight; | |||
$element.css( | |||
{ | |||
height: 0, | |||
opacity: 0 | |||
} | |||
); | |||
/* | |||
* reflow | |||
*/ | |||
void element.offsetHeight; | |||
$element.animate( | |||
{ | |||
height: | |||
targetHeight, | |||
opacity: | |||
1 | |||
}, | |||
{ | |||
duration: | |||
DURATION, | DURATION, | ||
easing: | |||
EASING, | |||
complete: | |||
function () { | function () { | ||
| 548번째 줄: | 390번째 줄: | ||
{ | { | ||
height: '', | height: '', | ||
overflow: '' | overflow: '', | ||
opacity: '' | |||
} | } | ||
); | ); | ||
} | } | ||
} | |||
); | |||
} | } | ||
| 567번째 줄: | 404번째 줄: | ||
*/ | */ | ||
function collapse( | function collapse( | ||
target, | target, | ||
element | |||
) { | ) { | ||
var $element = | var $element = | ||
$( | $( element ); | ||
| 583번째 줄: | 419번째 줄: | ||
/* | /* | ||
* | * 현재 display를 반드시 먼저 기억 | ||
*/ | */ | ||
rememberDisplay( | |||
element | |||
) | ); | ||
/* | |||
* 표준 MediaWiki 구조 | |||
*/ | |||
if ( element !== target ) { | |||
$element.slideUp( | |||
DURATION, | |||
function () { | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
' | |||
} | } | ||
); | ); | ||
return; | |||
} | |||
/* | |||
* mw-collapsible 자체가 접히는 구조 | |||
*/ | |||
var startHeight = | |||
element | |||
.getBoundingClientRect() | |||
.height; | |||
$element.css( | |||
{ | |||
height: | |||
startHeight, | |||
overflow: | |||
'hidden', | |||
opacity: | |||
1 | |||
} | |||
); | |||
} | /* | ||
* reflow | |||
*/ | |||
void element.offsetHeight; | |||
$element.animate( | |||
{ | |||
height: 0, | |||
opacity: 0 | |||
}, | |||
{ | |||
duration: | |||
DURATION, | |||
easing: | |||
EASING, | |||
complete: | |||
function () { | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
$element.css( | |||
{ | |||
display: | |||
'none', | |||
height: | |||
'', | |||
overflow: | |||
'', | |||
opacity: | |||
'' | |||
} | |||
); | |||
} | |||
); | } | ||
); | |||
} | } | ||
/** | /** | ||
* 토글 | * 하나의 name에 연결된 대상 전체 토글 | ||
*/ | */ | ||
function | function toggleGroup( toggle ) { | ||
var name = | var name = | ||
| 690번째 줄: | 531번째 줄: | ||
var | var targets = | ||
getTargets( | |||
name | name | ||
); | ); | ||
if ( ! | if ( !targets.length ) { | ||
return false; | return false; | ||
} | } | ||
var | var i; | ||
getAnimatedElement( | |||
/* | |||
* 핵심: | |||
* | |||
* 하나만 선택하지 않고 | |||
* 같은 이름을 가진 모든 target을 | |||
* 각자 반대 상태로 변경 | |||
*/ | |||
for ( | |||
i = 0; | |||
i < targets.length; | |||
i++ | |||
) { | |||
var target = | |||
targets[ i ]; | |||
var element = | |||
getAnimatedElement( | |||
target | |||
); | |||
if ( | |||
isCollapsed( | |||
target, | |||
element | |||
) | |||
) { | |||
expand( | |||
target, | |||
element | |||
); | |||
} else { | |||
collapse( | |||
target, | |||
element | |||
); | |||
} | |||
} | } | ||
/* | |||
* 이 형태에서는 보이는 토글과 숨겨진 토글이 | |||
* 교대하므로 aria-expanded를 특정 하나에 | |||
* 일괄 적용하는 것은 생략합니다. | |||
*/ | |||
| 737번째 줄: | 602번째 줄: | ||
* | * | ||
* MediaWiki 기본 customtoggle보다 | * MediaWiki 기본 customtoggle보다 | ||
* 먼저 | * 먼저 실행 | ||
*/ | */ | ||
document.addEventListener( | document.addEventListener( | ||
| 769번째 줄: | 634번째 줄: | ||
toggleGroup( | |||
toggle | toggle | ||
); | ); | ||
| 818번째 줄: | 683번째 줄: | ||
toggleGroup( | |||
toggle | toggle | ||
); | ); | ||
| 824번째 줄: | 689번째 줄: | ||
true | true | ||
); | ); | ||
}( jQuery, mediaWiki ) ); | }( jQuery, mediaWiki ) ); | ||
2026년 8월 20일 (목) 14:08 판
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원:
*
* 1. 일반 MediaWiki 구조
*
* .mw-customtoggle-XXX
* #mw-customcollapsible-XXX
* └─ .mw-collapsible-content
*
*
* 2. 동일 ID를 2개 사용하는 전환형 둘러보기 틀
*
* #mw-customcollapsible-XXX ← 처음 보이는 버튼
* └─ .mw-customtoggle-XXX
*
* #mw-customcollapsible-XXX ← 처음 숨겨진 실제 내용
* ├─ .mw-customtoggle-XXX
* └─ table / .sakura-table-wrapper
*
*
* 클릭 시 같은 XXX를 가진 모든 collapsible을
* 각각 현재 상태의 반대로 전환합니다.
*/
( function ( $, mw ) {
'use strict';
var DURATION = 300;
var EASING = 'swing';
/**
* 토글 이름 추출
*
* mw-customtoggle-Flora_LACTEA_01
* →
* Flora_LACTEA_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;
}
/**
* attribute selector escape
*/
function escapeAttribute( value ) {
return String( value )
.replace( /\\/g, '\\\\' )
.replace( /"/g, '\\"' );
}
/**
* 동일한 이름을 사용하는
* 모든 collapsible 대상 가져오기
*/
function getTargets( name ) {
var id =
'mw-customcollapsible-' + name;
return document.querySelectorAll(
'[id="' +
escapeAttribute( id ) +
'"]'
);
}
/**
* 직계 .mw-collapsible-content 찾기
*
* 중첩 collapsible까지 잘못 선택하지 않도록
* children만 검사합니다.
*/
function getDirectContent( target ) {
var children = target.children;
var i;
for ( i = 0; i < children.length; i++ ) {
if (
children[ i ].classList.contains(
'mw-collapsible-content'
)
) {
return children[ i ];
}
}
return null;
}
/**
* 실제 애니메이션 대상
*
* MediaWiki 표준:
* .mw-collapsible-content
*
* 현재 LACTEA 구조:
* target 자체
*/
function getAnimatedElement( target ) {
var content =
getDirectContent( target );
if ( content ) {
return content;
}
return target;
}
/**
* 현재 접혀 있는지 확인
*/
function isCollapsed( target, element ) {
if (
target.classList.contains(
'mw-collapsed'
)
) {
return true;
}
return (
window.getComputedStyle(
element
).display === 'none'
);
}
/**
* 펼칠 때 사용할 display 계산
*/
function getOpenDisplay( element ) {
/*
* 이전에 저장된 display가 있으면 사용
*/
var stored =
$( element ).data(
'mwAnimatedDisplay'
);
if ( stored ) {
return stored;
}
var display =
window.getComputedStyle(
element
).display;
/*
* 현재 보이는 상태라면 저장
*/
if (
display &&
display !== 'none'
) {
$( element ).data(
'mwAnimatedDisplay',
display
);
return display;
}
/*
* mw-collapsible 자체를 펼치는 구조
*/
if (
element.classList.contains(
'mw-collapsible'
)
) {
/*
* width가 100%인 내용 영역은 block이
* 가장 자연스럽습니다.
*/
if (
element.style.width === '100%'
) {
return 'block';
}
return 'inline-block';
}
switch ( element.tagName ) {
case 'TABLE':
return 'table';
case 'TR':
return 'table-row';
case 'TD':
case 'TH':
return 'table-cell';
case 'LI':
return 'list-item';
default:
return 'block';
}
}
/**
* 현재 display 값을 기억
*
* 닫기 전에 호출해야 함
*/
function rememberDisplay( element ) {
var display =
window.getComputedStyle(
element
).display;
if (
display &&
display !== 'none'
) {
$( element ).data(
'mwAnimatedDisplay',
display
);
}
}
/**
* 같은 name의 모든 토글에
* aria-expanded 설정
*/
function updateToggleAria(
name,
expanded
) {
var className =
'mw-customtoggle-' + name;
var toggles =
document.getElementsByClassName(
className
);
var i;
for ( i = 0; i < toggles.length; i++ ) {
toggles[ i ].setAttribute(
'aria-expanded',
expanded ?
'true' :
'false'
);
}
}
/**
* 펼치기
*/
function expand(
target,
element
) {
var $element =
$( element );
$element.stop(
true,
false
);
target.classList.remove(
'mw-collapsed'
);
/*
* 표준 MediaWiki 방식
*
* target 안의
* .mw-collapsible-content만 처리
*/
if ( element !== target ) {
$element
.stop( true, false )
.slideDown(
DURATION
);
return;
}
/*
* LACTEA 같은 구조
*
* mw-collapsible 자체가 display:none이므로
* 직접 높이를 애니메이션
*/
var display =
getOpenDisplay(
element
);
$element.css(
{
display: display,
height: 'auto',
overflow: 'hidden'
}
);
var targetHeight =
element.scrollHeight;
$element.css(
{
height: 0,
opacity: 0
}
);
/*
* reflow
*/
void element.offsetHeight;
$element.animate(
{
height:
targetHeight,
opacity:
1
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
$element.css(
{
height: '',
overflow: '',
opacity: ''
}
);
}
}
);
}
/**
* 접기
*/
function collapse(
target,
element
) {
var $element =
$( element );
$element.stop(
true,
false
);
/*
* 현재 display를 반드시 먼저 기억
*/
rememberDisplay(
element
);
/*
* 표준 MediaWiki 구조
*/
if ( element !== target ) {
$element.slideUp(
DURATION,
function () {
target.classList.add(
'mw-collapsed'
);
}
);
return;
}
/*
* mw-collapsible 자체가 접히는 구조
*/
var startHeight =
element
.getBoundingClientRect()
.height;
$element.css(
{
height:
startHeight,
overflow:
'hidden',
opacity:
1
}
);
/*
* reflow
*/
void element.offsetHeight;
$element.animate(
{
height: 0,
opacity: 0
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
target.classList.add(
'mw-collapsed'
);
$element.css(
{
display:
'none',
height:
'',
overflow:
'',
opacity:
''
}
);
}
}
);
}
/**
* 하나의 name에 연결된 대상 전체 토글
*/
function toggleGroup( toggle ) {
var name =
getToggleName(
toggle
);
if ( !name ) {
return false;
}
var targets =
getTargets(
name
);
if ( !targets.length ) {
return false;
}
var i;
/*
* 핵심:
*
* 하나만 선택하지 않고
* 같은 이름을 가진 모든 target을
* 각자 반대 상태로 변경
*/
for (
i = 0;
i < targets.length;
i++
) {
var target =
targets[ i ];
var element =
getAnimatedElement(
target
);
if (
isCollapsed(
target,
element
)
) {
expand(
target,
element
);
} else {
collapse(
target,
element
);
}
}
/*
* 이 형태에서는 보이는 토글과 숨겨진 토글이
* 교대하므로 aria-expanded를 특정 하나에
* 일괄 적용하는 것은 생략합니다.
*/
return true;
}
/**
* 클릭
*
* MediaWiki 기본 customtoggle보다
* 먼저 실행
*/
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();
toggleGroup(
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();
toggleGroup(
toggle
);
},
true
);
}( jQuery, mediaWiki ) );