참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원:
* - 일반 mw-collapsible-content
* - mw-collapsible 자체가 display:none 되는 구조
* - 동일 ID 2개 교대형 구조
* - flex 형태 복수 토글
* - 중첩 table / rowspan / infobox
*
* 특징:
* - opacity 없음
* - 내부 table 자체의 height는 변경하지 않음
* - wrapper의 height + clip-path만 애니메이션
*/
( function ( $, mw ) {
'use strict';
var DURATION = 180;
var EASING =
'cubic-bezier(0.4, 0, 0.2, 1)';
var DATA_DISPLAY =
'mwCustomCollapseDisplay';
var DATA_ANIMATION =
'mwCustomCollapseAnimation';
/**
* 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;
}
/**
* attribute selector escape
*/
function escapeAttribute( value ) {
return String( value )
.replace(
/\\/g,
'\\\\'
)
.replace(
/"/g,
'\\"'
);
}
/**
* 같은 이름의 모든 target
*/
function getTargets( name ) {
var id =
'mw-customcollapsible-' +
name;
return document.querySelectorAll(
'[id="' +
escapeAttribute( id ) +
'"]'
);
}
/**
* 직계 mw-collapsible-content 검색
*/
function getDirectContent( target ) {
var children =
target.children;
var i;
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
'mw-collapsible-content'
)
) {
return children[ i ];
}
}
return null;
}
/**
* 실제 애니메이션 요소
*
* mw-collapsible-content가 있으면 그것,
* 없으면 target 자체.
*
* 이번 방식에서는 DOM wrapper를 새로 만들지 않음.
*/
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;
}
if (
window.getComputedStyle(
target
).display === 'none'
) {
return true;
}
return (
window.getComputedStyle(
element
).display === 'none'
);
}
/**
* 현재 display 기억
*/
function rememberDisplay( element ) {
var display =
window.getComputedStyle(
element
).display;
if (
display &&
display !== 'none'
) {
$( element ).data(
DATA_DISPLAY,
display
);
}
}
/**
* 펼칠 때 display 결정
*/
function getOpenDisplay( element ) {
var stored =
$( element ).data(
DATA_DISPLAY
);
if ( stored ) {
return stored;
}
if (
element.style.display &&
element.style.display !== 'none'
) {
return element.style.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 'LI':
return 'list-item';
}
/*
* 현재 제공하신 정보상자에서는
* mw-customcollapsible-1이 div이므로 block.
*/
return 'block';
}
/**
* 진행 중인 Web Animation 취소
*/
function cancelAnimation( element ) {
var animation =
$( element ).data(
DATA_ANIMATION
);
if ( animation ) {
try {
animation.cancel();
} catch ( e ) {
// 무시
}
$( element ).removeData(
DATA_ANIMATION
);
}
}
/**
* target 내부의 해당 토글 찾기
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;
var toggles =
target.getElementsByClassName(
className
);
if ( toggles.length ) {
return toggles[ 0 ];
}
return null;
}
/**
* 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,
name,
hideOwnToggle
) {
cancelAnimation(
element
);
var ownToggle =
null;
/*
* 동일 ID 2개 교대형에서
* 두 토글 글자가 겹치는 현상 방지
*/
if ( hideOwnToggle ) {
ownToggle =
getOwnToggle(
target,
name
);
if ( ownToggle ) {
ownToggle.style.visibility =
'hidden';
}
}
target.classList.remove(
'mw-collapsed'
);
/*
* target 자체가 display:none이면
* 먼저 화면에 복구
*/
if (
window.getComputedStyle(
target
).display === 'none'
) {
target.style.display =
getOpenDisplay(
target
);
}
/*
* content 자체가 display:none인 경우
*/
if (
window.getComputedStyle(
element
).display === 'none'
) {
element.style.display =
getOpenDisplay(
element
);
}
/*
* 실제 콘텐츠 레이아웃을 먼저 완성.
*
* 중요:
* 내부 TABLE 높이는 건드리지 않음.
*/
element.style.height =
'auto';
element.style.overflow =
'hidden';
element.style.clipPath =
'inset(0 0 0 0)';
var endHeight =
element.scrollHeight;
/*
* wrapper의 레이아웃 높이만 0으로 설정.
*
* 내부 table은 온전한 크기로 계산된 상태.
*/
element.style.height =
'0px';
element.style.clipPath =
'inset(0 0 100% 0)';
void element.offsetHeight;
var animation =
element.animate(
[
{
height:
'0px',
clipPath:
'inset(0 0 100% 0)'
},
{
height:
endHeight +
'px',
clipPath:
'inset(0 0 0% 0)'
}
],
{
duration:
DURATION,
easing:
EASING,
fill:
'forwards'
}
);
$( element ).data(
DATA_ANIMATION,
animation
);
animation.onfinish =
function () {
$( element ).removeData(
DATA_ANIMATION
);
/*
* 최종 상태는 CSS/HTML에 맡김
*/
element.style.height =
'';
element.style.overflow =
'';
element.style.clipPath =
'';
if ( ownToggle ) {
ownToggle.style.visibility =
'';
}
try {
animation.cancel();
} catch ( e ) {
// 무시
}
};
}
/**
* 접기
*/
function collapse(
target,
element
) {
cancelAnimation(
element
);
rememberDisplay(
target
);
rememberDisplay(
element
);
var startHeight =
element.getBoundingClientRect()
.height;
/*
* 내부 table 자체는 그대로 유지.
*/
element.style.height =
startHeight +
'px';
element.style.overflow =
'hidden';
element.style.clipPath =
'inset(0 0 0% 0)';
void element.offsetHeight;
var animation =
element.animate(
[
{
height:
startHeight +
'px',
clipPath:
'inset(0 0 0% 0)'
},
{
height:
'0px',
clipPath:
'inset(0 0 100% 0)'
}
],
{
duration:
DURATION,
easing:
EASING,
fill:
'forwards'
}
);
$( element ).data(
DATA_ANIMATION,
animation
);
animation.onfinish =
function () {
$( element ).removeData(
DATA_ANIMATION
);
target.classList.add(
'mw-collapsed'
);
/*
* target 자체가 content인 구조
*/
if (
element === target
) {
target.style.display =
'none';
} else {
/*
* 일반
* .mw-collapsible-content
*/
element.style.display =
'none';
}
element.style.height =
'';
element.style.overflow =
'';
element.style.clipPath =
'';
try {
animation.cancel();
} catch ( e ) {
// 무시
}
};
}
/**
* 그룹 토글
*/
function toggleGroup( toggle ) {
var name =
getToggleName(
toggle
);
if ( !name ) {
return false;
}
var targets =
getTargets(
name
);
if (
!targets ||
!targets.length
) {
return false;
}
var multipleTargets =
targets.length > 1;
var opened =
false;
var i;
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,
name,
multipleTargets
);
opened =
true;
} else {
collapse(
target,
element
);
}
}
if (
targets.length === 1
) {
updateAria(
name,
opened
);
}
return true;
}
/**
* 클릭
*/
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
);
/**
* 초기 display 저장
*/
function initialize(
root
) {
var collapsibles =
root.querySelectorAll(
'.mw-collapsible'
);
var i;
for (
i = 0;
i < collapsibles.length;
i++
) {
rememberDisplay(
collapsibles[ i ]
);
}
}
$( function () {
initialize(
document
);
} );
if (
mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
if (
$content &&
$content[ 0 ]
) {
initialize(
$content[ 0 ]
);
}
}
);
}
}( jQuery, mediaWiki ) );