미디어위키:Common.js: 두 판 사이의 차이

편집 요약 없음
태그: 되돌려진 기여
편집 요약 없음
태그: 되돌려진 기여
1번째 줄: 1번째 줄:
/**
/**
  * MediaWiki 커스텀 펼치기/접기 슬라이드 애니메이션
  * MediaWiki 커스텀 펼치기/접기 애니메이션
*
* 틀 구조 수정 불필요.
  *
  *
  * 지원 구조:
  * 지원 구조:
  *
  *
* 1)
  * <div id="mw-customcollapsible-XXX" class="mw-collapsible">
  * <div id="mw-customcollapsible-XXX">
  *    <div class="mw-customtoggle-XXX mw-customtoggle">
  *    <div class="mw-customtoggle-XXX mw-customtoggle">버튼</div>
  *         [ 펼치기 · 접기 ]
  *     <table>내용</table>
  *     </div>
  * </div>
  *
  *
* 2)
  *    <table>...</table>
* <div id="mw-customcollapsible-XXX">
  *    <div class="mw-customtoggle-XXX mw-customtoggle">버튼</div>
  * </div>
  * </div>
* <table>내용</table>
  *
  *
  * 둘 다 자동 처리
  * 토글 버튼은 항상 남겨두고
* 버튼 뒤의 내용만 슬라이드합니다.
  */
  */


23번째 줄: 22번째 줄:


var DURATION = 300;
var DURATION = 300;
var READY_CLASS = 'custom-collapse-ready';
var OPEN_CLASS = 'custom-collapse-open';
var ANIMATING_CLASS = 'custom-collapse-animating';
var CONTENT_CLASS = 'custom-collapse-content';
/* ==============================
  toggle 이름 추출
  ============================== */


function getToggleName( toggle ) {
function getToggleName( toggle ) {
41번째 줄: 30번째 줄:
for ( i = 0; i < classes.length; i++ ) {
for ( i = 0; i < classes.length; i++ ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
if ( classes[ i ].indexOf( prefix ) === 0 ) {
return classes[ i ].substring(
return classes[ i ].substring( prefix.length );
prefix.length
);
}
}
}
}
49번째 줄: 36번째 줄:
return null;
return null;
}
}
/* ==============================
  연결된 collapsible 찾기
  ============================== */


function getTarget( toggle ) {
function getTarget( toggle ) {
67번째 줄: 49번째 줄:
}
}


/*
* 토글 버튼 다음에 있는 실제 내용들을 찾음.
*
* 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응.
*/
function getInternalContents( target, toggle ) {
var contents = [];
var node = toggle.nextElementSibling;


/* ==============================
while ( node ) {
  다음 custom collapsible인지 검사
contents.push( node );
  ============================== */
node = node.nextElementSibling;
 
function isNextCollapsible( element ) {
if ( !element || !element.id ) {
return false;
}
}


return (
return contents;
element.id.indexOf(
'mw-customcollapsible-'
) === 0
);
}
}


 
/*
/* ==============================
* 남성 아나운서처럼 target 바깥 바로 다음에
  실제 내용 wrapper 만들기
* table이 있는 경우를 위한 처리.
  ============================== */
*/
 
function getExternalContents( target ) {
function createContentWrapper( target, toggle ) {
var contents = [];
 
var node = target.nextElementSibling;
/*
* 이미 만들었으면 재사용
*/
var existing =
target.querySelector(
':scope > .' + CONTENT_CLASS
);
 
if ( existing ) {
return existing;
}
 
 
/* --------------------------------
  경우 1:
  내용이 target 내부에 있음
  -------------------------------- */
 
var internalNodes = [];
var node = toggle.nextSibling;


while ( node ) {
while ( node ) {
internalNodes.push( node );
node = node.nextSibling;
}
if ( internalNodes.length ) {
var internalWrapper =
document.createElement( 'div' );
internalWrapper.className =
CONTENT_CLASS;
internalNodes.forEach(
function ( child ) {
internalWrapper.appendChild(
child
);
}
);
target.appendChild(
internalWrapper
);
return internalWrapper;
}
/* --------------------------------
  경우 2:
  내용이 target 다음 형제에 있음
  -------------------------------- */
var parent = target.parentNode;
if ( !parent ) {
return null;
}
var externalWrapper =
document.createElement( 'div' );
externalWrapper.className =
CONTENT_CLASS;
var sibling =
target.nextSibling;
var foundContent = false;
while ( sibling ) {
var nextSibling =
sibling.nextSibling;
/*
/*
* 다음 customcollapsible을 만나면
* 다음 펼치기/접기 영역이 시작되면 종료
* 현재 영역 종료
*/
*/
if (
if (
sibling.nodeType === 1 &&
node.id &&
isNextCollapsible( sibling )
node.id.indexOf( 'mw-customcollapsible-' ) === 0
) {
) {
break;
break;
}
}


/*
contents.push( node );
* 공백 텍스트는 무시
*/
if (
sibling.nodeType === 3 &&
!sibling.textContent.trim()
) {
sibling = nextSibling;
continue;
}


/*
/*
* script / style은 건드리지 않음
* 현재 구조에서는 표 하나면 충분하므로
* 첫 실제 요소만 대상으로 함
*/
*/
if (
break;
sibling.nodeType === 1 &&
(
sibling.tagName === 'SCRIPT' ||
sibling.tagName === 'STYLE'
)
) {
break;
}
 
externalWrapper.appendChild(
sibling
);
 
foundContent = true;
 
sibling = nextSibling;
}
 
if ( !foundContent ) {
return null;
}
}


parent.insertBefore(
return contents;
externalWrapper,
sibling
);
 
return externalWrapper;
}
}


 
function getContents( target, toggle ) {
/* ==============================
var internal = getInternalContents(
  즉시 펼쳐진 상태 설정
target,
  ============================== */
toggle
 
function setOpenImmediately(
target,
toggle,
content
) {
target.classList.remove(
'mw-collapsed'
);
);


target.classList.add(
if ( internal.length ) {
OPEN_CLASS
return internal;
);
}
 
content.style.display = '';
content.style.height = '';
content.style.overflow = '';


toggle.setAttribute(
return getExternalContents( target );
'aria-expanded',
'true'
);
}
}


function isOpen( toggle, contents ) {
if (
toggle.getAttribute( 'aria-expanded' ) ===
'false'
) {
return false;
}


/* ==============================
if ( !contents.length ) {
  즉시 접힌 상태 설정
return false;
  ============================== */
}


function setClosedImmediately(
return (
target,
window.getComputedStyle(
toggle,
contents[ 0 ]
content
).display !== 'none'
) {
target.classList.add(
'mw-collapsed'
);
);
}


target.classList.remove(
/*
OPEN_CLASS
* 펼치기
);
*/
 
function openContents( toggle, contents ) {
content.style.display = 'none';
content.style.height = '';
content.style.overflow = '';
 
toggle.setAttribute(
toggle.setAttribute(
'aria-expanded',
'aria-expanded',
'false'
'true'
);
);
}


contents.forEach( function ( element ) {
if ( element._collapseAnimation ) {
element._collapseAnimation.cancel();
}


/* ==============================
/*
  펼치기 애니메이션
* 먼저 원래 display 복구
  ============================== */
*/
element.style.display = '';


function openContent(
/*
target,
* CSS 때문에 여전히 none인 경우
toggle,
* 태그에 맞는 display 사용
content
*/
) {
if (
if (
window.getComputedStyle( element ).display ===
content.classList.contains(
'none'
ANIMATING_CLASS
) {
)
if ( element.tagName === 'TABLE' ) {
) {
element.style.display = 'table';
return;
} else {
}
element.style.display = 'block';
}
}


content.classList.add(
var endHeight = element.scrollHeight;
ANIMATING_CLASS
);


target.classList.remove(
element.style.overflow = 'hidden';
'mw-collapsed'
);


content.style.display = 'block';
var animation = element.animate(
content.style.overflow = 'hidden';
[
content.style.height = '0px';
{
 
height: '0px'
/*
},
* 브라우저가 0px 상태를 먼저
{
* 그리도록 한 프레임 기다림
height: endHeight + 'px'
*/
window.requestAnimationFrame(
function () {
 
var endHeight =
content.scrollHeight;
 
content.style.transition =
'height ' +
DURATION +
'ms ease';
 
/*
* 한 프레임을 더 나누어
* height transition이 확실하게 실행되게 함
*/
window.requestAnimationFrame(
function () {
content.style.height =
endHeight + 'px';
}
}
);
],
}
{
);
duration: DURATION,
easing: 'ease',
fill: 'forwards'
}
);


window.setTimeout(
element._collapseAnimation =
function () {
animation;


content.classList.remove(
animation.onfinish = function () {
ANIMATING_CLASS
element.style.height = '';
);
element.style.overflow = '';
element.style.display = '';


target.classList.add(
element._collapseAnimation =
OPEN_CLASS
null;
);


content.style.transition = '';
animation.cancel();
content.style.height = '';
};
content.style.overflow = '';
} );
content.style.display = '';
 
toggle.setAttribute(
'aria-expanded',
'true'
);
 
},
DURATION + 40
);
}
}


 
/*
/* ==============================
* 접기
  접기 애니메이션
*/
  ============================== */
function closeContents( toggle, contents ) {
 
toggle.setAttribute(
function closeContent(
'aria-expanded',
target,
'false'
toggle,
content
) {
if (
content.classList.contains(
ANIMATING_CLASS
)
) {
return;
}
 
content.classList.add(
ANIMATING_CLASS
);
);


var startHeight =
contents.forEach( function ( element ) {
content.getBoundingClientRect()
if ( element._collapseAnimation ) {
.height;
element._collapseAnimation.cancel();
}


if ( startHeight <= 0 ) {
var startHeight =
content.classList.remove(
element.getBoundingClientRect().height;
ANIMATING_CLASS
);


setClosedImmediately(
if ( startHeight <= 0 ) {
target,
element.style.display = 'none';
toggle,
return;
content
);
 
return;
}
 
content.style.overflow = 'hidden';
content.style.height =
startHeight + 'px';
 
content.style.transition =
'height ' +
DURATION +
'ms ease';
 
/*
* 현재 높이를 브라우저에 먼저 반영
*/
content.offsetHeight;
 
window.requestAnimationFrame(
function () {
content.style.height =
'0px';
}
}
);


window.setTimeout(
element.style.overflow = 'hidden';
function () {


content.classList.remove(
var animation = element.animate(
ANIMATING_CLASS
[
);
{
 
height:
target.classList.remove(
startHeight + 'px'
OPEN_CLASS
},
);
{
 
height: '0px'
target.classList.add(
}
'mw-collapsed'
],
);
{
 
duration: DURATION,
content.style.display =
easing: 'ease',
'none';
fill: 'forwards'
}
);


content.style.transition =
element._collapseAnimation =
'';
animation;


content.style.height = '';
animation.onfinish = function () {
content.style.overflow = '';
element.style.display = 'none';
element.style.height = '';
element.style.overflow = '';


toggle.setAttribute(
element._collapseAnimation =
'aria-expanded',
null;
'false'
);


},
animation.cancel();
DURATION + 40
};
);
} );
}
}


 
function handleClick( event ) {
/* ==============================
var toggle = event.currentTarget;
  토글 실행
var target = getTarget( toggle );
  ============================== */
 
function toggleContent( toggle ) {
var target =
getTarget( toggle );


if ( !target ) {
if ( !target ) {
472번째 줄: 264번째 줄:
}
}


var content =
var contents = getContents(
target._customCollapseContent;
target,
toggle
);


if ( !content ) {
/*
content =
* 대상을 못 찾았다면 MediaWiki 기본 기능을
createContentWrapper(
* 막지 않고 그대로 둔다.
target,
*/
toggle
if ( !contents.length ) {
);
 
target._customCollapseContent =
content;
}
 
if ( !content ) {
return;
return;
}
}


var isOpen =
/*
toggle.getAttribute(
* 이제 대상이 확실하게 있을 때만
'aria-expanded'
* MediaWiki 기본 즉시 접기/펼치기를 막음.
) !== 'false' &&
*/
!target.classList.contains(
event.preventDefault();
'mw-collapsed'
event.stopPropagation();
);
event.stopImmediatePropagation();


if ( isOpen ) {
if ( isOpen( toggle, contents ) ) {
closeContent(
closeContents(
target,
toggle,
toggle,
content
contents
);
);
} else {
} else {
openContent(
openContents(
target,
toggle,
toggle,
content
contents
);
);
}
}
}
}


 
function handleKeydown( event ) {
/* ==============================
  초기화
  ============================== */
 
function initializeToggle( toggle ) {
var target =
getTarget( toggle );
 
if ( !target ) {
return;
}
 
if (
if (
target.classList.contains(
event.key !== 'Enter' &&
READY_CLASS
event.key !== ' '
)
) {
) {
return;
return;
}
}


var content =
handleClick( event );
createContentWrapper(
target,
toggle
);
 
if ( !content ) {
return;
}
 
target._customCollapseContent =
content;
 
target.classList.add(
READY_CLASS
);
 
/*
* 최초 상태 판별
*/
var initiallyClosed =
target.classList.contains(
'mw-collapsed'
) ||
toggle.getAttribute(
'aria-expanded'
) === 'false';
 
if ( initiallyClosed ) {
 
setClosedImmediately(
target,
toggle,
content
);
 
} else {
 
setOpenImmediately(
target,
toggle,
content
);
 
}
}
}


 
function initialize() {
function initializeAll() {
var toggles =
var toggles =
document.querySelectorAll(
document.querySelectorAll(
588번째 줄: 315번째 줄:
);
);


toggles.forEach(
toggles.forEach( function ( toggle ) {
function ( toggle ) {
 
if (
getToggleName(
toggle
)
) {
initializeToggle(
toggle
);
}
 
}
);
}
 
 
/* ==============================
  클릭 이벤트
  ============================== */
 
document.addEventListener(
'click',
function ( event ) {
 
var toggle =
event.target.closest(
'.mw-customtoggle'
);
 
if ( !toggle ) {
return;
}
 
if (
if (
!getToggleName(
!getToggleName( toggle ) ||
toggle
toggle.dataset.collapseAnimationReady === '1'
)
) {
) {
return;
return;
}
}
toggle.dataset.collapseAnimationReady =
'1';


/*
/*
* MediaWiki 기본 즉시 토글 차단
* document capture가 아니라
* 실제 버튼 자체에 직접 등록
*/
*/
event.preventDefault();
toggle.addEventListener(
event.stopPropagation();
'click',
event.stopImmediatePropagation();
handleClick
 
toggleContent(
toggle
);
);


},
toggle.addEventListener(
true
'keydown',
);
handleKeydown
 
 
/* ==============================
  키보드
  ============================== */
 
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();
 
toggleContent(
toggle
);
);
} );
}


},
if ( document.readyState === 'loading' ) {
true
);
 
 
/* ==============================
  페이지 로드
  ============================== */
 
if (
document.readyState ===
'loading'
) {
 
document.addEventListener(
document.addEventListener(
'DOMContentLoaded',
'DOMContentLoaded',
initializeAll
initialize
);
);
} else {
} else {
 
initialize();
initializeAll();
 
}
}


 
if ( window.mw && mw.hook ) {
/*
mw.hook( 'wikipage.content' ).add(
* MediaWiki 동적 콘텐츠 대응
*/
if (
window.mw &&
mw.hook
) {
 
mw.hook(
'wikipage.content'
).add(
function () {
function () {
initializeAll();
initialize();
}
}
);
);
}
}


}() );
}() );

2026년 8월 20일 (목) 13:26 판

/**
 * MediaWiki 커스텀 펼치기/접기 애니메이션
 *
 * 틀 구조 수정 불필요.
 *
 * 지원 구조:
 *
 * <div id="mw-customcollapsible-XXX" class="mw-collapsible">
 *     <div class="mw-customtoggle-XXX mw-customtoggle">
 *         [ 펼치기 · 접기 ]
 *     </div>
 *
 *     <table>...</table>
 * </div>
 *
 * 토글 버튼은 항상 남겨두고
 * 버튼 뒤의 내용만 슬라이드합니다.
 */

( function () {
	'use strict';

	var DURATION = 300;

	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;
	}

	function getTarget( toggle ) {
		var name = getToggleName( toggle );

		if ( !name ) {
			return null;
		}

		return document.getElementById(
			'mw-customcollapsible-' + name
		);
	}

	/*
	 * 토글 버튼 다음에 있는 실제 내용들을 찾음.
	 *
	 * 여성 아나운서처럼 같은 div 안에 table이 있는 경우 대응.
	 */
	function getInternalContents( target, toggle ) {
		var contents = [];
		var node = toggle.nextElementSibling;

		while ( node ) {
			contents.push( node );
			node = node.nextElementSibling;
		}

		return contents;
	}

	/*
	 * 남성 아나운서처럼 target 바깥 바로 다음에
	 * table이 있는 경우를 위한 처리.
	 */
	function getExternalContents( target ) {
		var contents = [];
		var node = target.nextElementSibling;

		while ( node ) {
			/*
			 * 다음 펼치기/접기 영역이 시작되면 종료
			 */
			if (
				node.id &&
				node.id.indexOf( 'mw-customcollapsible-' ) === 0
			) {
				break;
			}

			contents.push( node );

			/*
			 * 현재 구조에서는 표 하나면 충분하므로
			 * 첫 실제 요소만 대상으로 함
			 */
			break;
		}

		return contents;
	}

	function getContents( target, toggle ) {
		var internal = getInternalContents(
			target,
			toggle
		);

		if ( internal.length ) {
			return internal;
		}

		return getExternalContents( target );
	}

	function isOpen( toggle, contents ) {
		if (
			toggle.getAttribute( 'aria-expanded' ) ===
			'false'
		) {
			return false;
		}

		if ( !contents.length ) {
			return false;
		}

		return (
			window.getComputedStyle(
				contents[ 0 ]
			).display !== 'none'
		);
	}

	/*
	 * 펼치기
	 */
	function openContents( toggle, contents ) {
		toggle.setAttribute(
			'aria-expanded',
			'true'
		);

		contents.forEach( function ( element ) {
			if ( element._collapseAnimation ) {
				element._collapseAnimation.cancel();
			}

			/*
			 * 먼저 원래 display 복구
			 */
			element.style.display = '';

			/*
			 * CSS 때문에 여전히 none인 경우
			 * 태그에 맞는 display 사용
			 */
			if (
				window.getComputedStyle( element ).display ===
				'none'
			) {
				if ( element.tagName === 'TABLE' ) {
					element.style.display = 'table';
				} else {
					element.style.display = 'block';
				}
			}

			var endHeight = element.scrollHeight;

			element.style.overflow = 'hidden';

			var animation = element.animate(
				[
					{
						height: '0px'
					},
					{
						height: endHeight + 'px'
					}
				],
				{
					duration: DURATION,
					easing: 'ease',
					fill: 'forwards'
				}
			);

			element._collapseAnimation =
				animation;

			animation.onfinish = function () {
				element.style.height = '';
				element.style.overflow = '';
				element.style.display = '';

				element._collapseAnimation =
					null;

				animation.cancel();
			};
		} );
	}

	/*
	 * 접기
	 */
	function closeContents( toggle, contents ) {
		toggle.setAttribute(
			'aria-expanded',
			'false'
		);

		contents.forEach( function ( element ) {
			if ( element._collapseAnimation ) {
				element._collapseAnimation.cancel();
			}

			var startHeight =
				element.getBoundingClientRect().height;

			if ( startHeight <= 0 ) {
				element.style.display = 'none';
				return;
			}

			element.style.overflow = 'hidden';

			var animation = element.animate(
				[
					{
						height:
							startHeight + 'px'
					},
					{
						height: '0px'
					}
				],
				{
					duration: DURATION,
					easing: 'ease',
					fill: 'forwards'
				}
			);

			element._collapseAnimation =
				animation;

			animation.onfinish = function () {
				element.style.display = 'none';
				element.style.height = '';
				element.style.overflow = '';

				element._collapseAnimation =
					null;

				animation.cancel();
			};
		} );
	}

	function handleClick( event ) {
		var toggle = event.currentTarget;
		var target = getTarget( toggle );

		if ( !target ) {
			return;
		}

		var contents = getContents(
			target,
			toggle
		);

		/*
		 * 대상을 못 찾았다면 MediaWiki 기본 기능을
		 * 막지 않고 그대로 둔다.
		 */
		if ( !contents.length ) {
			return;
		}

		/*
		 * 이제 대상이 확실하게 있을 때만
		 * MediaWiki 기본 즉시 접기/펼치기를 막음.
		 */
		event.preventDefault();
		event.stopPropagation();
		event.stopImmediatePropagation();

		if ( isOpen( toggle, contents ) ) {
			closeContents(
				toggle,
				contents
			);
		} else {
			openContents(
				toggle,
				contents
			);
		}
	}

	function handleKeydown( event ) {
		if (
			event.key !== 'Enter' &&
			event.key !== ' '
		) {
			return;
		}

		handleClick( event );
	}

	function initialize() {
		var toggles =
			document.querySelectorAll(
				'.mw-customtoggle'
			);

		toggles.forEach( function ( toggle ) {
			if (
				!getToggleName( toggle ) ||
				toggle.dataset.collapseAnimationReady === '1'
			) {
				return;
			}

			toggle.dataset.collapseAnimationReady =
				'1';

			/*
			 * document capture가 아니라
			 * 실제 버튼 자체에 직접 등록
			 */
			toggle.addEventListener(
				'click',
				handleClick
			);

			toggle.addEventListener(
				'keydown',
				handleKeydown
			);
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener(
			'DOMContentLoaded',
			initialize
		);
	} else {
		initialize();
	}

	if ( window.mw && mw.hook ) {
		mw.hook( 'wikipage.content' ).add(
			function () {
				initialize();
			}
		);
	}

}() );