편집 요약 없음 |
편집 요약 없음 |
||
| 1번째 줄: | 1번째 줄: | ||
console.log( 'Common.js loaded' ); | |||
); | |||
/** | /** | ||
* MediaWiki 커스텀 펼치기 / 접기 애니메이션 | * MediaWiki 커스텀 펼치기 / 접기 애니메이션 | ||
2026년 8월 20일 (목) 14:29 판
console.log( 'Common.js loaded' );
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원:
* - mw-customtoggle-XXX
* - mw-customcollapsible-XXX
* - 일반 mw-collapsible-content
* - 동일 ID 2개 교대형 구조
* - flex 내부 여러 토글
* - 중첩 표
*
* 효과:
* - 높이 애니메이션만 적용
* - opacity 없음
*/
( function ( $ ) {
'use strict';
var DURATION = 180;
var EASING = 'swing';
var VIEWPORT_CLASS =
'mw-custom-collapse-viewport';
var INNER_CLASS =
'mw-custom-collapse-viewport-inner';
var DATA_DISPLAY =
'mwCustomCollapseOriginalDisplay';
/**
* 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;
}
/**
* 현재 접힘 상태
*/
function isCollapsed( target ) {
if (
target.classList.contains(
'mw-collapsed'
)
) {
return true;
}
return (
window.getComputedStyle(
target
).display === 'none'
);
}
/**
* display 상태 기억
*/
function rememberDisplay( target ) {
var display =
window.getComputedStyle(
target
).display;
if (
display &&
display !== 'none'
) {
$( target ).data(
DATA_DISPLAY,
display
);
}
}
/**
* 펼칠 때 target의 display 복원
*/
function getOpenDisplay( target ) {
var saved =
$( target ).data(
DATA_DISPLAY
);
if ( saved ) {
return saved;
}
/*
* HTML에 원래 display 값이 남아 있다면
* 그 값을 우선 사용
*/
if (
target.style.display &&
target.style.display !== 'none'
) {
return target.style.display;
}
/*
* TABLE 계열
*/
switch ( target.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-collapsible은 대부분 DIV
*/
return 'block';
}
/**
* 애니메이션용 viewport 검색
*/
function getViewport( target ) {
var children =
target.children;
var i;
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
VIEWPORT_CLASS
)
) {
return children[ i ];
}
}
return null;
}
/**
* 애니메이션용 viewport 생성
*
* 내부 TABLE 자체에는 height를 적용하지 않고
* viewport 높이만 변화시킴.
*/
function ensureViewport( target ) {
var existing =
getViewport(
target
);
if ( existing ) {
return existing;
}
var viewport =
document.createElement(
'div'
);
var inner =
document.createElement(
'div'
);
viewport.className =
VIEWPORT_CLASS;
inner.className =
INNER_CLASS;
/*
* MediaWiki 표준 구조라면
* mw-collapsible-content의 내용만 이동
*/
var content =
getDirectContent(
target
);
if ( content ) {
while (
content.firstChild
) {
inner.appendChild(
content.firstChild
);
}
viewport.appendChild(
inner
);
content.appendChild(
viewport
);
return viewport;
}
/*
* mw-collapsible-content가 없는 구조
*
* target 내부 전체를 viewport 안으로 이동
*/
while (
target.firstChild
) {
inner.appendChild(
target.firstChild
);
}
viewport.appendChild(
inner
);
target.appendChild(
viewport
);
return viewport;
}
/**
* viewport inner
*/
function getViewportInner(
viewport
) {
var children =
viewport.children;
var i;
for (
i = 0;
i < children.length;
i++
) {
if (
children[ i ].classList &&
children[ i ].classList.contains(
INNER_CLASS
)
) {
return children[ i ];
}
}
return null;
}
/**
* target 안의 자기 토글 검색
*
* 동일 ID 2개 교대형에서 사용.
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;
var elements =
target.getElementsByClassName(
className
);
if (
elements.length
) {
return elements[ 0 ];
}
return null;
}
/**
* 펼치기
*/
function expand(
target,
name,
multipleTargets
) {
var $target =
$( target );
/*
* 진행 중인 target animation 중단
*/
$target.stop(
true,
true
);
target.classList.remove(
'mw-collapsed'
);
/*
* 바깥 target 먼저 표시
*/
target.style.display =
getOpenDisplay(
target
);
var viewport =
ensureViewport(
target
);
var inner =
getViewportInner(
viewport
);
if ( !inner ) {
return;
}
var ownToggle =
null;
/*
* 동일 ID가 2개인 교대형 구조:
*
* 접히는 버튼과 펼쳐지는 버튼이
* 애니메이션 중 겹치지 않도록 처리.
*/
if ( multipleTargets ) {
ownToggle =
getOwnToggle(
target,
name
);
if ( ownToggle ) {
ownToggle.style.visibility =
'hidden';
}
}
var $viewport =
$( viewport );
$viewport.stop(
true,
true
);
viewport.style.display =
'block';
viewport.style.overflow =
'hidden';
viewport.style.height =
'auto';
/*
* 내부 table은 건드리지 않고
* inner의 실제 높이를 측정
*/
var targetHeight =
inner.getBoundingClientRect()
.height;
viewport.style.height =
'0px';
/*
* 강제 reflow
*/
void viewport.offsetHeight;
$viewport.animate(
{
height:
targetHeight
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
viewport.style.height =
'';
viewport.style.overflow =
'';
if (
ownToggle
) {
ownToggle.style.visibility =
'';
}
}
}
);
}
/**
* 접기
*/
function collapse(
target
) {
rememberDisplay(
target
);
var viewport =
ensureViewport(
target
);
var inner =
getViewportInner(
viewport
);
if ( !inner ) {
return;
}
var $viewport =
$( viewport );
$viewport.stop(
true,
true
);
var currentHeight =
viewport.getBoundingClientRect()
.height;
/*
* 혹시 viewport 높이가 0으로 잡히는 경우
* inner 실제 높이 사용
*/
if (
currentHeight <= 0
) {
currentHeight =
inner.getBoundingClientRect()
.height;
}
viewport.style.height =
currentHeight +
'px';
viewport.style.overflow =
'hidden';
void viewport.offsetHeight;
$viewport.animate(
{
height:
0
},
{
duration:
DURATION,
easing:
EASING,
complete:
function () {
target.classList.add(
'mw-collapsed'
);
target.style.display =
'none';
viewport.style.height =
'';
viewport.style.overflow =
'';
}
}
);
}
/**
* 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 toggleGroup(
toggle
) {
var name =
getToggleName(
toggle
);
if ( !name ) {
return false;
}
var targets =
getTargets(
name
);
if (
!targets.length
) {
return false;
}
var multipleTargets =
targets.length > 1;
var i;
var opened =
false;
/*
* 클릭 직전의 상태를 먼저 기록
*
* 각 요소를 순차 수정하다가
* 다음 요소의 상태 판별이 꼬이지 않도록 함.
*/
var states =
[];
for (
i = 0;
i < targets.length;
i++
) {
states.push(
isCollapsed(
targets[ i ]
)
);
}
/*
* 상태 변경
*/
for (
i = 0;
i < targets.length;
i++
) {
if (
states[ i ]
) {
expand(
targets[ i ],
name,
multipleTargets
);
opened =
true;
} else {
collapse(
targets[ i ]
);
}
}
/*
* 일반 target 하나짜리만
* aria-expanded 갱신.
*/
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 elements =
root.querySelectorAll(
'.mw-collapsible'
);
var i;
for (
i = 0;
i < elements.length;
i++
) {
if (
window.getComputedStyle(
elements[ i ]
).display !== 'none'
) {
rememberDisplay(
elements[ i ]
);
}
}
}
$( function () {
initialize(
document
);
} );
/*
* MediaWiki AJAX / 동적 콘텐츠
*/
if (
window.mw &&
mw.hook
) {
mw.hook(
'wikipage.content'
).add(
function ( $content ) {
if (
$content &&
$content[ 0 ]
) {
initialize(
$content[ 0 ]
);
}
}
);
}
}( jQuery ) );