편집 요약 없음 |
편집 요약 없음 |
||
| 2번째 줄: | 2번째 줄: | ||
* MediaWiki 커스텀 펼치기 / 접기 애니메이션 | * MediaWiki 커스텀 펼치기 / 접기 애니메이션 | ||
* | * | ||
* 지원 | * 지원 구조 | ||
* | * | ||
* 1. 일반 MediaWiki 구조 | * 1. 일반 MediaWiki 구조 | ||
* | * | ||
* | * <div class="mw-customtoggle-XXX mw-customtoggle"> | ||
* | * [ 펼치기 · 접기 ] | ||
* | * </div> | ||
* | * | ||
* <div | |||
* id="mw-customcollapsible-XXX" | |||
* class="mw-collapsible mw-collapsed" | |||
* > | |||
* <div class="mw-collapsible-content"> | |||
* 내용 | |||
* </div> | |||
* </div> | |||
* | * | ||
* | * | ||
* | * 2. 동일 ID를 2개 사용하는 전환형 구조 | ||
* | * | ||
* | * <div | ||
* | * id="mw-customcollapsible-XXX" | ||
* | * class="mw-collapsible" | ||
* > | |||
* <div class="mw-customtoggle-XXX mw-customtoggle"> | |||
* [ 버튼 ] | |||
* </div> | |||
* </div> | |||
* | * | ||
* <div | |||
* id="mw-customcollapsible-XXX" | |||
* class="mw-collapsible mw-collapsed" | |||
* style="display:none;" | |||
* > | |||
* <div class="mw-customtoggle-XXX mw-customtoggle"> | |||
* [ 버튼 ] | |||
* </div> | |||
* | * | ||
* | * 실제 내용 | ||
* | * </div> | ||
* | |||
* | |||
* 3. flex 안에 여러 독립 토글 | |||
* | |||
* mw-customtoggle-TWICE_KOR | |||
* mw-customtoggle-TWICE_OVS | |||
* mw-customtoggle-TWICE_ETC | |||
* mw-customtoggle-TWICE_REL | |||
* | |||
* | |||
* 애니메이션: | |||
* 높이만 변경 | |||
* opacity 사용 안 함 | |||
*/ | */ | ||
| 28번째 줄: | 60번째 줄: | ||
'use strict'; | 'use strict'; | ||
/** | |||
* 애니메이션 시간 | |||
*/ | |||
var DURATION = 300; | var DURATION = 300; | ||
/** | |||
* jQuery easing | |||
*/ | |||
var EASING = 'swing'; | var EASING = 'swing'; | ||
/** | /** | ||
* 토글 | * jQuery data key | ||
*/ | |||
var DATA_DISPLAY = | |||
'mwCustomCollapseDisplay'; | |||
/** | |||
* ----------------------------------------------------- | |||
* 토글 클래스에서 식별자 추출 | |||
* | * | ||
* mw-customtoggle-Flora_LACTEA_01 | * mw-customtoggle-Flora_LACTEA_01 | ||
* → | * → | ||
* Flora_LACTEA_01 | * Flora_LACTEA_01 | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
function getToggleName( toggle ) { | function getToggleName( toggle ) { | ||
var prefix = 'mw-customtoggle-'; | var prefix = | ||
var classes = toggle.classList; | 'mw-customtoggle-'; | ||
var classes = | |||
toggle.classList; | |||
var i; | var i; | ||
for ( i = 0; i < classes.length; i++ ) { | |||
for ( | |||
i = 0; | |||
i < classes.length; | |||
i++ | |||
) { | |||
if ( | if ( | ||
classes[ i ].indexOf( prefix ) === 0 | classes[ i ].indexOf( | ||
prefix | |||
) === 0 | |||
) { | ) { | ||
return classes[ i ].substring( | return classes[ i ].substring( | ||
prefix.length | prefix.length | ||
| 52번째 줄: | 113번째 줄: | ||
} | } | ||
} | } | ||
return null; | return null; | ||
| 58번째 줄: | 120번째 줄: | ||
/** | /** | ||
* attribute selector escape | * ----------------------------------------------------- | ||
* HTML attribute selector escape | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function escapeAttribute( value ) { | function escapeAttribute( value ) { | ||
return String( value ) | return String( value ) | ||
.replace( /\\/g, '\\\\' ) | .replace( | ||
.replace( /"/g, '\\"' ); | /\\/g, | ||
'\\\\' | |||
) | |||
.replace( | |||
/"/g, | |||
'\\"' | |||
); | |||
} | } | ||
/** | /** | ||
* | * ----------------------------------------------------- | ||
* 모든 collapsible | * 같은 name에 대응하는 모든 collapsible 가져오기 | ||
* | |||
* 동일 ID가 여러 개 있어도 모두 반환 | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function getTargets( name ) { | function getTargets( name ) { | ||
if ( !name ) { | |||
return []; | |||
} | |||
var id = | var id = | ||
'mw-customcollapsible-' + name; | 'mw-customcollapsible-' + | ||
name; | |||
return document.querySelectorAll( | return document.querySelectorAll( | ||
| 84번째 줄: | 166번째 줄: | ||
/** | /** | ||
* 직계 .mw-collapsible-content 찾기 | * ----------------------------------------------------- | ||
* 해당 target의 직계 | |||
* .mw-collapsible-content 찾기 | |||
* | * | ||
* | * querySelector를 바로 사용하지 않는 이유: | ||
* | * 안쪽에 중첩된 collapsible이 있을 경우 | ||
* 내부 콘텐츠를 잘못 잡을 수 있음. | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function getDirectContent( target ) { | function getDirectContent( target ) { | ||
var children = target.children; | |||
var children = | |||
target.children; | |||
var i; | var i; | ||
for ( i = 0; i < children.length; i++ ) { | |||
for ( | |||
i = 0; | |||
i < children.length; | |||
i++ | |||
) { | |||
if ( | if ( | ||
children[ i ].classList.contains( | children[ i ] | ||
.classList | |||
.contains( | |||
'mw-collapsible-content' | |||
) | |||
) { | ) { | ||
return children[ i ]; | return children[ i ]; | ||
} | } | ||
} | } | ||
return null; | return null; | ||
| 108번째 줄: | 207번째 줄: | ||
/** | /** | ||
* 실제 | * ----------------------------------------------------- | ||
* 실제 애니메이션할 요소 결정 | |||
* | * | ||
* MediaWiki | * 일반 MediaWiki: | ||
* .mw-collapsible-content | * .mw-collapsible-content | ||
* | * | ||
* | * LACTEA 등의 구조: | ||
* target 자체 | * target 자체 | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
function getAnimatedElement( target ) { | function getAnimatedElement( target ) { | ||
var content = | var content = | ||
getDirectContent( target ); | getDirectContent( | ||
target | |||
); | |||
if ( content ) { | if ( content ) { | ||
return content; | return content; | ||
} | } | ||
return target; | return target; | ||
| 129번째 줄: | 235번째 줄: | ||
/** | /** | ||
* 현재 | * ----------------------------------------------------- | ||
* 현재 접힌 상태인지 확인 | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function isCollapsed( target, element ) { | function isCollapsed( | ||
target, | |||
element | |||
) { | |||
/* | |||
* MediaWiki 상태 클래스 우선 | |||
*/ | |||
if ( | if ( | ||
target.classList.contains( | target.classList.contains( | ||
| 138번째 줄: | 252번째 줄: | ||
) | ) | ||
) { | ) { | ||
return true; | return true; | ||
} | } | ||
/* | |||
* 실제 display:none 여부 확인 | |||
*/ | |||
return ( | return ( | ||
window.getComputedStyle( | window | ||
.getComputedStyle( | |||
element | |||
) | |||
.display === 'none' | |||
); | ); | ||
} | } | ||
| 150번째 줄: | 271번째 줄: | ||
/** | /** | ||
* 펼칠 때 사용할 display | * ----------------------------------------------------- | ||
* 현재 display 값 저장 | |||
* | |||
* inline-block 같은 display를 접었다 다시 펼칠 때 | |||
* 원래 형태를 복구하기 위해 사용 | |||
* ----------------------------------------------------- | |||
*/ | |||
function rememberDisplay( element ) { | |||
var display = | |||
window | |||
.getComputedStyle( | |||
element | |||
) | |||
.display; | |||
if ( | |||
display && | |||
display !== 'none' | |||
) { | |||
$( element ).data( | |||
DATA_DISPLAY, | |||
display | |||
); | |||
} | |||
} | |||
/** | |||
* ----------------------------------------------------- | |||
* 펼칠 때 사용할 display 결정 | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function getOpenDisplay( element ) { | function getOpenDisplay( element ) { | ||
/* | /* | ||
* 이전에 저장된 display가 | * 이전에 저장된 display가 있다면 사용 | ||
*/ | */ | ||
var stored = | var stored = | ||
$( element ).data( | $( element ).data( | ||
DATA_DISPLAY | |||
); | ); | ||
if ( stored ) { | if ( stored ) { | ||
return stored; | return stored; | ||
} | } | ||
/* | /* | ||
* | * 인라인 스타일에 유효한 display가 남아 있는 경우 | ||
*/ | */ | ||
if ( | if ( | ||
display && | element.style.display && | ||
display !== 'none' | element.style.display !== 'none' | ||
) { | ) { | ||
return element.style.display; | |||
} | } | ||
/* | /* | ||
* | * 현재 computed display | ||
*/ | */ | ||
var computed = | |||
window | |||
.getComputedStyle( | |||
element | |||
) | |||
.display; | |||
if ( | if ( | ||
computed && | |||
computed !== 'none' | |||
) { | ) { | ||
return computed; | |||
} | } | ||
/* | |||
* 요소별 자연스러운 display | |||
*/ | |||
switch ( element.tagName ) { | switch ( element.tagName ) { | ||
case 'TABLE': | case 'TABLE': | ||
return 'table'; | return 'table'; | ||
case 'TBODY': | |||
return 'table-row-group'; | |||
case 'THEAD': | |||
return 'table-header-group'; | |||
case 'TFOOT': | |||
return 'table-footer-group'; | |||
case 'TR': | case 'TR': | ||
| 224번째 줄: | 377번째 줄: | ||
case 'TH': | case 'TH': | ||
return 'table-cell'; | return 'table-cell'; | ||
case 'COLGROUP': | |||
return 'table-column-group'; | |||
case 'COL': | |||
return 'table-column'; | |||
case 'CAPTION': | |||
return 'table-caption'; | |||
case 'LI': | case 'LI': | ||
return 'list-item'; | return 'list-item'; | ||
} | |||
/* | |||
* mw-collapsible 자체가 대상인 경우 | |||
* | |||
* 폭이 100%면 block, | |||
* 그렇지 않으면 inline-block을 우선 사용. | |||
* | |||
* LACTEA 상단 버튼 영역 같은 | |||
* display:inline-block 구조 대응. | |||
*/ | |||
if ( | |||
element.classList.contains( | |||
'mw-collapsible' | |||
) | |||
) { | |||
if ( | |||
element.style.width === '100%' || | |||
element.style.width === '100.0%' | |||
) { | |||
return 'block'; | return 'block'; | ||
} | |||
return 'inline-block'; | |||
} | |||
return 'block'; | |||
} | } | ||
/** | /** | ||
* 같은 name의 모든 | * ----------------------------------------------------- | ||
* | * 같은 name의 모든 토글 ARIA 상태 변경 | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
function | function updateAria( | ||
name, | name, | ||
expanded | expanded | ||
| 268번째 줄: | 435번째 줄: | ||
var className = | var className = | ||
'mw-customtoggle-' + name; | 'mw-customtoggle-' + | ||
name; | |||
var toggles = | var toggles = | ||
| 277번째 줄: | 445번째 줄: | ||
var i; | var i; | ||
for ( i = 0; i < toggles.length; i++ ) { | |||
for ( | |||
i = 0; | |||
i < toggles.length; | |||
i++ | |||
) { | |||
toggles[ i ].setAttribute( | toggles[ i ].setAttribute( | ||
'aria-expanded', | 'aria-expanded', | ||
| 289번째 줄: | 463번째 줄: | ||
/** | /** | ||
* ----------------------------------------------------- | |||
* 펼치기 | * 펼치기 | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
function expand( | function expand( | ||
| 300번째 줄: | 476번째 줄: | ||
/* | |||
* 현재 진행 중인 jQuery animation 중지 | |||
* | |||
* true = queue 비움 | |||
* false = 현재 애니메이션을 끝까지 강제 적용하지 않음 | |||
*/ | |||
$element.stop( | $element.stop( | ||
true, | true, | ||
| 306번째 줄: | 488번째 줄: | ||
/* | |||
* MediaWiki 접힘 상태 해제 | |||
*/ | |||
target.classList.remove( | target.classList.remove( | ||
'mw-collapsed' | 'mw-collapsed' | ||
| 311번째 줄: | 496번째 줄: | ||
/* | /** | ||
* | * ------------------------------------------------- | ||
* 일반 MediaWiki 구조 | |||
* | * | ||
* target | * target | ||
* .mw-collapsible- | * └─ .mw-collapsible-content | ||
* ------------------------------------------------- | |||
*/ | */ | ||
if ( element !== target ) { | if ( | ||
element !== target | |||
) { | |||
/* | |||
* slideDown이 display:none에서 | |||
* 자연스럽게 높이를 계산 | |||
*/ | |||
$element | $element | ||
.stop( true, false ) | .stop( | ||
true, | |||
false | |||
) | |||
.slideDown( | .slideDown( | ||
DURATION | { | ||
duration: | |||
DURATION, | |||
easing: | |||
EASING, | |||
complete: | |||
function () { | |||
/* | |||
* jQuery가 생성한 | |||
* 불필요한 height/overflow 제거 | |||
*/ | |||
$element.css( | |||
{ | |||
height: | |||
'', | |||
overflow: | |||
'' | |||
} | |||
); | |||
} | |||
} | |||
); | ); | ||
return; | return; | ||
| 329번째 줄: | 550번째 줄: | ||
/* | /** | ||
* | * ------------------------------------------------- | ||
* target 자체가 display:none 되는 구조 | |||
* | * | ||
* | * LACTEA 등의 구조 | ||
* | * ------------------------------------------------- | ||
*/ | */ | ||
| 342번째 줄: | 564번째 줄: | ||
/* | |||
* 먼저 보이는 상태로 복구 | |||
*/ | |||
$element.css( | $element.css( | ||
{ | { | ||
display: display, | display: | ||
height: 'auto', | display, | ||
overflow: 'hidden' | |||
height: | |||
'auto', | |||
overflow: | |||
'hidden' | |||
} | } | ||
); | ); | ||
/* | |||
* 실제 최종 높이 측정 | |||
*/ | |||
var targetHeight = | var targetHeight = | ||
element.scrollHeight; | element.scrollHeight; | ||
/* | |||
* 0 높이에서 시작 | |||
*/ | |||
$element.css( | $element.css( | ||
{ | { | ||
height: | height: | ||
'0px' | |||
} | } | ||
); | ); | ||
| 364번째 줄: | 600번째 줄: | ||
/* | /* | ||
* reflow | * 강제 reflow | ||
*/ | */ | ||
void element.offsetHeight; | void element.offsetHeight; | ||
/* | |||
* 높이만 애니메이션 | |||
*/ | |||
$element.animate( | $element.animate( | ||
{ | { | ||
height: | height: | ||
targetHeight | targetHeight | ||
}, | }, | ||
{ | { | ||
| 387번째 줄: | 623번째 줄: | ||
function () { | function () { | ||
/* | |||
* 최종적으로 내용 높이에 | |||
* 자동 대응하도록 height 제거 | |||
*/ | |||
$element.css( | $element.css( | ||
{ | { | ||
height: '', | height: | ||
overflow: | '', | ||
overflow: | |||
'' | |||
} | } | ||
); | ); | ||
| 401번째 줄: | 643번째 줄: | ||
/** | /** | ||
* ----------------------------------------------------- | |||
* 접기 | * 접기 | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
function collapse( | function collapse( | ||
| 419번째 줄: | 663번째 줄: | ||
/* | /* | ||
* 현재 | * display:none으로 만들기 전에 | ||
* 현재 display 기억 | |||
*/ | */ | ||
rememberDisplay( | rememberDisplay( | ||
| 426번째 줄: | 671번째 줄: | ||
/* | /** | ||
* | * ------------------------------------------------- | ||
* 일반 MediaWiki 구조 | |||
* ------------------------------------------------- | |||
*/ | */ | ||
if ( element !== target ) { | if ( | ||
element !== target | |||
) { | |||
$element.slideUp( | $element.slideUp( | ||
DURATION, | { | ||
duration: | |||
DURATION, | |||
target.classList.add( | easing: | ||
EASING, | |||
complete: | |||
function () { | |||
target.classList.add( | |||
'mw-collapsed' | |||
); | |||
$element.css( | |||
{ | |||
height: | |||
'', | |||
overflow: | |||
'' | |||
} | |||
); | |||
} | |||
} | } | ||
); | ); | ||
return; | return; | ||
| 445번째 줄: | 714번째 줄: | ||
/* | /** | ||
* | * ------------------------------------------------- | ||
* target 자체가 접히는 구조 | |||
* ------------------------------------------------- | |||
*/ | */ | ||
| 455번째 줄: | 726번째 줄: | ||
/* | |||
* 시작 높이를 고정 | |||
*/ | |||
$element.css( | $element.css( | ||
{ | { | ||
height: | height: | ||
startHeight, | startHeight + 'px', | ||
overflow: | overflow: | ||
'hidden' | 'hidden' | ||
} | } | ||
); | ); | ||
void element.offsetHeight; | void element.offsetHeight; | ||
/* | |||
* 현재 높이 → 0 | |||
*/ | |||
$element.animate( | $element.animate( | ||
{ | { | ||
height: | height: | ||
0 | |||
}, | }, | ||
{ | { | ||
| 490번째 줄: | 761번째 줄: | ||
function () { | function () { | ||
/* | |||
* 완전히 접힌 뒤 상태 반영 | |||
*/ | |||
target.classList.add( | target.classList.add( | ||
'mw-collapsed' | 'mw-collapsed' | ||
| 504번째 줄: | 778번째 줄: | ||
overflow: | overflow: | ||
'' | '' | ||
} | } | ||
| 517번째 줄: | 788번째 줄: | ||
/** | /** | ||
* | * ----------------------------------------------------- | ||
* 대상 하나 토글 | |||
* ----------------------------------------------------- | |||
*/ | |||
function toggleTarget( | |||
target | |||
) { | |||
var element = | |||
getAnimatedElement( | |||
target | |||
); | |||
if ( | |||
isCollapsed( | |||
target, | |||
element | |||
) | |||
) { | |||
expand( | |||
target, | |||
element | |||
); | |||
return true; | |||
} | |||
collapse( | |||
target, | |||
element | |||
); | |||
return false; | |||
} | |||
/** | |||
* ----------------------------------------------------- | |||
* 그룹 토글 | |||
* | |||
* 동일한 XXX를 가진 target이 여러 개면 | |||
* 각각 현재 상태의 반대로 변경. | |||
* | |||
* 예: | |||
* | |||
* LACTEA | |||
* | |||
* 첫 번째: | |||
* 버튼만 표시 | |||
* | |||
* 두 번째: | |||
* 실제 내용, display:none | |||
* | |||
* 클릭하면 | |||
* | |||
* 첫 번째 → 닫힘 | |||
* 두 번째 → 펼침 | |||
* | |||
* 따라서 버튼이 중복 표시되지 않음. | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
function toggleGroup( toggle ) { | function toggleGroup( | ||
toggle | |||
) { | |||
var name = | var name = | ||
| 525번째 줄: | 861번째 줄: | ||
toggle | toggle | ||
); | ); | ||
if ( !name ) { | if ( !name ) { | ||
| 536번째 줄: | 873번째 줄: | ||
); | ); | ||
if ( !targets.length ) { | |||
if ( | |||
!targets || | |||
!targets.length | |||
) { | |||
return false; | return false; | ||
} | } | ||
| 543번째 줄: | 885번째 줄: | ||
var i; | var i; | ||
var openedAny = | |||
false; | |||
for ( | for ( | ||
i = 0; | i = 0; | ||
| 564번째 줄: | 903번째 줄: | ||
); | ); | ||
var collapsed = | |||
isCollapsed( | isCollapsed( | ||
target, | target, | ||
element | element | ||
) | ); | ||
) { | |||
if ( collapsed ) { | |||
expand( | expand( | ||
| 576번째 줄: | 916번째 줄: | ||
element | element | ||
); | ); | ||
openedAny = | |||
true; | |||
} else { | } else { | ||
| 588번째 줄: | 931번째 줄: | ||
/* | /* | ||
* | * 대상이 하나뿐인 일반 구조에서는 | ||
* | * ARIA를 정상적으로 갱신. | ||
* | * | ||
* 동일 ID가 2개인 교대형 구조는 | |||
* 두 요소의 상태가 서로 반대이므로 | |||
* 클릭된 그룹 기준으로 처리. | |||
*/ | */ | ||
if ( | |||
targets.length === 1 | |||
) { | |||
updateAria( | |||
name, | |||
openedAny | |||
); | |||
} | |||
| 599번째 줄: | 954번째 줄: | ||
/** | /** | ||
* 클릭 | * ----------------------------------------------------- | ||
* 클릭 처리 | |||
* | * | ||
* MediaWiki | * capture=true로 | ||
* 먼저 | * MediaWiki jquery.makeCollapsible보다 | ||
* 먼저 이벤트를 처리. | |||
* ----------------------------------------------------- | |||
*/ | */ | ||
document.addEventListener( | document.addEventListener( | ||
| 612번째 줄: | 970번째 줄: | ||
'.mw-customtoggle' | '.mw-customtoggle' | ||
); | ); | ||
if ( !toggle ) { | if ( !toggle ) { | ||
| 618번째 줄: | 977번째 줄: | ||
/* | |||
* 우리가 처리하는 customtoggle인지 확인 | |||
*/ | |||
if ( | if ( | ||
!getToggleName( | !getToggleName( | ||
| 623번째 줄: | 985번째 줄: | ||
) | ) | ||
) { | ) { | ||
return; | return; | ||
} | } | ||
/* | |||
* MediaWiki 기본 토글 동작 중지 | |||
*/ | |||
event.preventDefault(); | event.preventDefault(); | ||
| 643번째 줄: | 1,009번째 줄: | ||
/** | /** | ||
* ----------------------------------------------------- | |||
* 키보드 | |||
* | |||
* Enter / Space | * Enter / Space | ||
* ----------------------------------------------------- | |||
*/ | */ | ||
document.addEventListener( | document.addEventListener( | ||
| 653번째 줄: | 1,023번째 줄: | ||
event.key !== ' ' | event.key !== ' ' | ||
) { | ) { | ||
return; | return; | ||
} | } | ||
| 661번째 줄: | 1,032번째 줄: | ||
'.mw-customtoggle' | '.mw-customtoggle' | ||
); | ); | ||
if ( !toggle ) { | if ( !toggle ) { | ||
| 672번째 줄: | 1,044번째 줄: | ||
) | ) | ||
) { | ) { | ||
return; | return; | ||
} | } | ||
| 689번째 줄: | 1,062번째 줄: | ||
true | true | ||
); | ); | ||
/** | |||
* ----------------------------------------------------- | |||
* 초기 display 상태 기억 | |||
* | |||
* 페이지 처음 로딩됐을 때 현재 보이는 | |||
* mw-collapsible의 display 값을 저장. | |||
* | |||
* 예: | |||
* display:inline-block | |||
* | |||
* 나중에 접었다 펼쳐도 | |||
* inline-block으로 정확히 복원. | |||
* ----------------------------------------------------- | |||
*/ | |||
function initializeDisplays( | |||
root | |||
) { | |||
var collapsibles = | |||
root.querySelectorAll( | |||
'.mw-collapsible' | |||
); | |||
var i; | |||
for ( | |||
i = 0; | |||
i < collapsibles.length; | |||
i++ | |||
) { | |||
var element = | |||
collapsibles[ i ]; | |||
var display = | |||
window | |||
.getComputedStyle( | |||
element | |||
) | |||
.display; | |||
if ( | |||
display && | |||
display !== 'none' | |||
) { | |||
$( element ).data( | |||
DATA_DISPLAY, | |||
display | |||
); | |||
} | |||
} | |||
} | |||
/** | |||
* ----------------------------------------------------- | |||
* 최초 로딩 | |||
* ----------------------------------------------------- | |||
*/ | |||
$( function () { | |||
initializeDisplays( | |||
document | |||
); | |||
} ); | |||
/** | |||
* ----------------------------------------------------- | |||
* MediaWiki 동적 콘텐츠 대응 | |||
* | |||
* Lua / AJAX / 확장기능 등으로 | |||
* 나중에 추가된 콘텐츠의 display도 저장. | |||
* ----------------------------------------------------- | |||
*/ | |||
if ( | |||
mw && | |||
mw.hook | |||
) { | |||
mw.hook( | |||
'wikipage.content' | |||
).add( | |||
function ( $content ) { | |||
if ( | |||
$content && | |||
$content[ 0 ] | |||
) { | |||
initializeDisplays( | |||
$content[ 0 ] | |||
); | |||
} | |||
} | |||
); | |||
} | |||
}( jQuery, mediaWiki ) ); | }( jQuery, mediaWiki ) ); | ||
2026년 8월 20일 (목) 14:11 판
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원 구조
*
* 1. 일반 MediaWiki 구조
*
* <div class="mw-customtoggle-XXX mw-customtoggle">
* [ 펼치기 · 접기 ]
* </div>
*
* <div
* id="mw-customcollapsible-XXX"
* class="mw-collapsible mw-collapsed"
* >
* <div class="mw-collapsible-content">
* 내용
* </div>
* </div>
*
*
* 2. 동일 ID를 2개 사용하는 전환형 구조
*
* <div
* id="mw-customcollapsible-XXX"
* class="mw-collapsible"
* >
* <div class="mw-customtoggle-XXX mw-customtoggle">
* [ 버튼 ]
* </div>
* </div>
*
* <div
* id="mw-customcollapsible-XXX"
* class="mw-collapsible mw-collapsed"
* style="display:none;"
* >
* <div class="mw-customtoggle-XXX mw-customtoggle">
* [ 버튼 ]
* </div>
*
* 실제 내용
* </div>
*
*
* 3. flex 안에 여러 독립 토글
*
* mw-customtoggle-TWICE_KOR
* mw-customtoggle-TWICE_OVS
* mw-customtoggle-TWICE_ETC
* mw-customtoggle-TWICE_REL
*
*
* 애니메이션:
* 높이만 변경
* opacity 사용 안 함
*/
( function ( $, mw ) {
'use strict';
/**
* 애니메이션 시간
*/
var DURATION = 300;
/**
* jQuery easing
*/
var EASING = 'swing';
/**
* jQuery data key
*/
var DATA_DISPLAY =
'mwCustomCollapseDisplay';
/**
* -----------------------------------------------------
* 토글 클래스에서 식별자 추출
*
* 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;
}
/**
* -----------------------------------------------------
* HTML attribute selector escape
* -----------------------------------------------------
*/
function escapeAttribute( value ) {
return String( value )
.replace(
/\\/g,
'\\\\'
)
.replace(
/"/g,
'\\"'
);
}
/**
* -----------------------------------------------------
* 같은 name에 대응하는 모든 collapsible 가져오기
*
* 동일 ID가 여러 개 있어도 모두 반환
* -----------------------------------------------------
*/
function getTargets( name ) {
if ( !name ) {
return [];
}
var id =
'mw-customcollapsible-' +
name;
return document.querySelectorAll(
'[id="' +
escapeAttribute( id ) +
'"]'
);
}
/**
* -----------------------------------------------------
* 해당 target의 직계
* .mw-collapsible-content 찾기
*
* querySelector를 바로 사용하지 않는 이유:
* 안쪽에 중첩된 collapsible이 있을 경우
* 내부 콘텐츠를 잘못 잡을 수 있음.
* -----------------------------------------------------
*/
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
) {
/*
* MediaWiki 상태 클래스 우선
*/
if (
target.classList.contains(
'mw-collapsed'
)
) {
return true;
}
/*
* 실제 display:none 여부 확인
*/
return (
window
.getComputedStyle(
element
)
.display === 'none'
);
}
/**
* -----------------------------------------------------
* 현재 display 값 저장
*
* inline-block 같은 display를 접었다 다시 펼칠 때
* 원래 형태를 복구하기 위해 사용
* -----------------------------------------------------
*/
function rememberDisplay( element ) {
var display =
window
.getComputedStyle(
element
)
.display;
if (
display &&
display !== 'none'
) {
$( element ).data(
DATA_DISPLAY,
display
);
}
}
/**
* -----------------------------------------------------
* 펼칠 때 사용할 display 결정
* -----------------------------------------------------
*/
function getOpenDisplay( element ) {
/*
* 이전에 저장된 display가 있다면 사용
*/
var stored =
$( element ).data(
DATA_DISPLAY
);
if ( stored ) {
return stored;
}
/*
* 인라인 스타일에 유효한 display가 남아 있는 경우
*/
if (
element.style.display &&
element.style.display !== 'none'
) {
return element.style.display;
}
/*
* 현재 computed display
*/
var computed =
window
.getComputedStyle(
element
)
.display;
if (
computed &&
computed !== 'none'
) {
return computed;
}
/*
* 요소별 자연스러운 display
*/
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';
}
/*
* mw-collapsible 자체가 대상인 경우
*
* 폭이 100%면 block,
* 그렇지 않으면 inline-block을 우선 사용.
*
* LACTEA 상단 버튼 영역 같은
* display:inline-block 구조 대응.
*/
if (
element.classList.contains(
'mw-collapsible'
)
) {
if (
element.style.width === '100%' ||
element.style.width === '100.0%'
) {
return 'block';
}
return 'inline-block';
}
return 'block';
}
/**
* -----------------------------------------------------
* 같은 name의 모든 토글 ARIA 상태 변경
* -----------------------------------------------------
*/
function updateAria(
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 );
/*
* 현재 진행 중인 jQuery animation 중지
*
* true = queue 비움
* false = 현재 애니메이션을 끝까지 강제 적용하지 않음
*/
$element.stop(
true,
false
);
/*
* MediaWiki 접힘 상태 해제
*/
target.classList.remove(
'mw-collapsed'
);
/**
* -------------------------------------------------
* 일반 MediaWiki 구조
*
* target
* └─ .mw-collapsible-content
* -------------------------------------------------
*/
if (
element !== target
) {
/*
* slideDown이 display:none에서
* 자연스럽게 높이를 계산
*/
$element
.stop(
true,
false
)
.slideDown(
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
/*
* jQuery가 생성한
* 불필요한 height/overflow 제거
*/
$element.css(
{
height:
'',
overflow:
''
}
);
}
}
);
return;
}
/**
* -------------------------------------------------
* target 자체가 display:none 되는 구조
*
* LACTEA 등의 구조
* -------------------------------------------------
*/
var display =
getOpenDisplay(
element
);
/*
* 먼저 보이는 상태로 복구
*/
$element.css(
{
display:
display,
height:
'auto',
overflow:
'hidden'
}
);
/*
* 실제 최종 높이 측정
*/
var targetHeight =
element.scrollHeight;
/*
* 0 높이에서 시작
*/
$element.css(
{
height:
'0px'
}
);
/*
* 강제 reflow
*/
void element.offsetHeight;
/*
* 높이만 애니메이션
*/
$element.animate(
{
height:
targetHeight
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
/*
* 최종적으로 내용 높이에
* 자동 대응하도록 height 제거
*/
$element.css(
{
height:
'',
overflow:
''
}
);
}
}
);
}
/**
* -----------------------------------------------------
* 접기
* -----------------------------------------------------
*/
function collapse(
target,
element
) {
var $element =
$( element );
$element.stop(
true,
false
);
/*
* display:none으로 만들기 전에
* 현재 display 기억
*/
rememberDisplay(
element
);
/**
* -------------------------------------------------
* 일반 MediaWiki 구조
* -------------------------------------------------
*/
if (
element !== target
) {
$element.slideUp(
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
target.classList.add(
'mw-collapsed'
);
$element.css(
{
height:
'',
overflow:
''
}
);
}
}
);
return;
}
/**
* -------------------------------------------------
* target 자체가 접히는 구조
* -------------------------------------------------
*/
var startHeight =
element
.getBoundingClientRect()
.height;
/*
* 시작 높이를 고정
*/
$element.css(
{
height:
startHeight + 'px',
overflow:
'hidden'
}
);
void element.offsetHeight;
/*
* 현재 높이 → 0
*/
$element.animate(
{
height:
0
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
/*
* 완전히 접힌 뒤 상태 반영
*/
target.classList.add(
'mw-collapsed'
);
$element.css(
{
display:
'none',
height:
'',
overflow:
''
}
);
}
}
);
}
/**
* -----------------------------------------------------
* 대상 하나 토글
* -----------------------------------------------------
*/
function toggleTarget(
target
) {
var element =
getAnimatedElement(
target
);
if (
isCollapsed(
target,
element
)
) {
expand(
target,
element
);
return true;
}
collapse(
target,
element
);
return false;
}
/**
* -----------------------------------------------------
* 그룹 토글
*
* 동일한 XXX를 가진 target이 여러 개면
* 각각 현재 상태의 반대로 변경.
*
* 예:
*
* LACTEA
*
* 첫 번째:
* 버튼만 표시
*
* 두 번째:
* 실제 내용, display:none
*
* 클릭하면
*
* 첫 번째 → 닫힘
* 두 번째 → 펼침
*
* 따라서 버튼이 중복 표시되지 않음.
* -----------------------------------------------------
*/
function toggleGroup(
toggle
) {
var name =
getToggleName(
toggle
);
if ( !name ) {
return false;
}
var targets =
getTargets(
name
);
if (
!targets ||
!targets.length
) {
return false;
}
var i;
var openedAny =
false;
for (
i = 0;
i < targets.length;
i++
) {
var target =
targets[ i ];
var element =
getAnimatedElement(
target
);
var collapsed =
isCollapsed(
target,
element
);
if ( collapsed ) {
expand(
target,
element
);
openedAny =
true;
} else {
collapse(
target,
element
);
}
}
/*
* 대상이 하나뿐인 일반 구조에서는
* ARIA를 정상적으로 갱신.
*
* 동일 ID가 2개인 교대형 구조는
* 두 요소의 상태가 서로 반대이므로
* 클릭된 그룹 기준으로 처리.
*/
if (
targets.length === 1
) {
updateAria(
name,
openedAny
);
}
return true;
}
/**
* -----------------------------------------------------
* 클릭 처리
*
* capture=true로
* MediaWiki jquery.makeCollapsible보다
* 먼저 이벤트를 처리.
* -----------------------------------------------------
*/
document.addEventListener(
'click',
function ( event ) {
var toggle =
event.target.closest(
'.mw-customtoggle'
);
if ( !toggle ) {
return;
}
/*
* 우리가 처리하는 customtoggle인지 확인
*/
if (
!getToggleName(
toggle
)
) {
return;
}
/*
* MediaWiki 기본 토글 동작 중지
*/
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
);
/**
* -----------------------------------------------------
* 초기 display 상태 기억
*
* 페이지 처음 로딩됐을 때 현재 보이는
* mw-collapsible의 display 값을 저장.
*
* 예:
* display:inline-block
*
* 나중에 접었다 펼쳐도
* inline-block으로 정확히 복원.
* -----------------------------------------------------
*/
function initializeDisplays(
root
) {
var collapsibles =
root.querySelectorAll(
'.mw-collapsible'
);
var i;
for (
i = 0;
i < collapsibles.length;
i++
) {
var element =
collapsibles[ i ];
var display =
window
.getComputedStyle(
element
)
.display;
if (
display &&
display !== 'none'
) {
$( element ).data(
DATA_DISPLAY,
display
);
}
}
}
/**
* -----------------------------------------------------
* 최초 로딩
* -----------------------------------------------------
*/
$( function () {
initializeDisplays(
document
);
} );
/**
* -----------------------------------------------------
* MediaWiki 동적 콘텐츠 대응
*
* Lua / AJAX / 확장기능 등으로
* 나중에 추가된 콘텐츠의 display도 저장.
* -----------------------------------------------------
*/
if (
mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
if (
$content &&
$content[ 0 ]
) {
initializeDisplays(
$content[ 0 ]
);
}
}
);
}
}( jQuery, mediaWiki ) );