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

편집 요약 없음
편집 요약 없음
1번째 줄: 1번째 줄:
/**
* MediaWiki 커스텀 펼치기 / 접기 애니메이션
*
* 지원:
* - 일반 mw-collapsible-content
* - mw-collapsible 자체가 display:none 되는 구조
* - 동일 ID 2개 교대형 구조
* - flex 형태 복수 토글
* - 중첩 table / rowspan / infobox
*
* 특징:
* - opacity 없음
* - 내부 table 자체의 height는 변경하지 않음
* - wrapper의 height + clip-path만 애니메이션
*/
( function ( $, mw ) {
( function ( $, mw ) {
'use strict';
'use strict';


var DURATION = 180;
var DURATION = 180;
var EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';


var EASING =
var VIEWPORT_CLASS = 'mw-collapse-viewport';
'cubic-bezier(0.4, 0, 0.2, 1)';
var CONTENT_CLASS = 'mw-collapse-content';


var DATA_DISPLAY =
'mwCustomCollapseDisplay';


var DATA_ANIMATION =
'mwCustomCollapseAnimation';
/**
* mw-customtoggle-XXX
* →
* XXX
*/
function getToggleName( toggle ) {
function getToggleName( toggle ) {
var prefix =
var prefix = 'mw-customtoggle-';
'mw-customtoggle-';
var classes = toggle.classList;
 
var classes =
toggle.classList;
 
var i;
var i;


for (
for ( i = 0; i < classes.length; i++ ) {
i = 0;
if ( classes[ i ].indexOf( prefix ) === 0 ) {
i < classes.length;
return classes[ i ].substring( prefix.length );
i++
) {
if (
classes[ i ].indexOf(
prefix
) === 0
) {
return classes[ i ].substring(
prefix.length
);
}
}
}
}
64번째 줄: 24번째 줄:




/**
* attribute selector escape
*/
function escapeAttribute( value ) {
function escapeAttribute( value ) {
return String( value )
return String( value )
.replace(
.replace( /\\/g, '\\\\' )
/\\/g,
.replace( /"/g, '\\"' );
'\\\\'
)
.replace(
/"/g,
'\\"'
);
}
}




/**
* 같은 이름의 모든 target
*/
function getTargets( name ) {
function getTargets( name ) {
var id =
var id = 'mw-customcollapsible-' + name;
'mw-customcollapsible-' +
name;


return document.querySelectorAll(
return document.querySelectorAll(
'[id="' +
'[id="' + escapeAttribute( id ) + '"]'
escapeAttribute( id ) +
);
'"]'
}
 
 
function isCollapsed( target ) {
return (
target.classList.contains( 'mw-collapsed' ) ||
window.getComputedStyle( target ).display === 'none'
);
);
}
}
97번째 줄: 49번째 줄:


/**
/**
* 직계 mw-collapsible-content 검색
* target 내부에 viewport/content 구조 생성
*
* target
* └─ viewport
*    └─ content
*      └─ 기존 내용 전체
*/
*/
function getDirectContent( target ) {
function ensureViewport( target ) {
var children =
target.children;


var directChildren = target.children;
var i;
var i;


for (
for ( i = 0; i < directChildren.length; i++ ) {
i = 0;
i < children.length;
i++
) {
if (
if (
children[ i ].classList &&
directChildren[ i ].classList &&
children[ i ].classList.contains(
directChildren[ i ].classList.contains(
'mw-collapsible-content'
VIEWPORT_CLASS
)
)
) {
) {
return children[ i ];
return directChildren[ i ];
}
}
}
}


return null;
 
}
var viewport = document.createElement( 'div' );
var content = document.createElement( 'div' );
 
viewport.className = VIEWPORT_CLASS;
content.className = CONTENT_CLASS;




/**
/*
* 실제 애니메이션 요소
* 중요:
*
* 기존 내용 전체를 content로 이동
* mw-collapsible-content가 있으면 그것,
*/
* 없으면 target 자체.
while ( target.firstChild ) {
*
content.appendChild(
* 이번 방식에서는 DOM wrapper를 새로 만들지 않음.
target.firstChild
*/
function getAnimatedElement( target ) {
var content =
getDirectContent(
target
);
);
}


if ( content ) {
return content;
}


return target;
viewport.appendChild( content );
}
target.appendChild( viewport );




/**
viewport.style.overflow = 'hidden';
* 현재 접힌 상태
*/
function isCollapsed(
target,
element
) {
if (
target.classList.contains(
'mw-collapsed'
)
) {
return true;
}


if (
window.getComputedStyle(
target
).display === 'none'
) {
return true;
}


return (
return viewport;
window.getComputedStyle(
element
).display === 'none'
);
}
}




/**
function getContent( viewport ) {
* 현재 display 기억
var children = viewport.children;
*/
var i;
function rememberDisplay( element ) {
var display =
window.getComputedStyle(
element
).display;


if (
for ( i = 0; i < children.length; i++ ) {
display &&
if (
display !== 'none'
children[ i ].classList &&
) {
children[ i ].classList.contains(
$( element ).data(
CONTENT_CLASS
DATA_DISPLAY,
)
display
) {
);
return children[ i ];
}
}
}
return null;
}
}




/**
function getNaturalDisplay( target ) {
* 펼칠 때 display 결정
*/
function getOpenDisplay( element ) {
var stored =
$( element ).data(
DATA_DISPLAY
);
 
if ( stored ) {
return stored;
}
 


if (
if (
element.style.display &&
target.dataset.mwCollapseDisplay
element.style.display !== 'none'
) {
) {
return element.style.display;
return target.dataset.mwCollapseDisplay;
}
}




switch ( element.tagName ) {
var style =
case 'TABLE':
window.getComputedStyle( target );
return 'table';


case 'TBODY':
return 'table-row-group';


case 'THEAD':
if (
return 'table-header-group';
style.display &&
style.display !== 'none'
) {
target.dataset.mwCollapseDisplay =
style.display;


case 'TFOOT':
return style.display;
return 'table-footer-group';
}


case 'TR':
return 'table-row';


case 'TD':
if (
case 'TH':
target.style.width === '100%'
return 'table-cell';
) {
 
return 'block';
case 'LI':
return 'list-item';
}
}




/*
* 현재 제공하신 정보상자에서는
* mw-customcollapsible-1이 div이므로 block.
*/
return 'block';
return 'block';
}
}




/**
function rememberDisplay( target ) {
* 진행 중인 Web Animation 취소
 
*/
var display =
function cancelAnimation( element ) {
window.getComputedStyle(
var animation =
target
$( element ).data(
).display;
DATA_ANIMATION
);


if ( animation ) {
try {
animation.cancel();
} catch ( e ) {
// 무시
}


$( element ).removeData(
if (
DATA_ANIMATION
display &&
);
display !== 'none'
) {
target.dataset.mwCollapseDisplay =
display;
}
}
}
}




/**
function expand( target ) {
* target 내부의 해당 토글 찾기
*/
function getOwnToggle(
target,
name
) {
var className =
'mw-customtoggle-' +
name;


var toggles =
var viewport =
target.getElementsByClassName(
ensureViewport(
className
target
);
);


if ( toggles.length ) {
var content =
return toggles[ 0 ];
getContent(
}
viewport
 
return null;
}
 
 
/**
* ARIA 상태
*/
function updateAria(
name,
expanded
) {
var className =
'mw-customtoggle-' +
name;
 
var toggles =
document.getElementsByClassName(
className
);
);


var i;


for (
if ( !content ) {
i = 0;
return;
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개 교대형에서
* target 바깥쪽만 표시
* 두 토글 글자가 겹치는 현상 방지
*/
*/
if ( hideOwnToggle ) {
target.style.display =
ownToggle =
getNaturalDisplay(
getOwnToggle(
target
target,
);
name
);
 
if ( ownToggle ) {
ownToggle.style.visibility =
'hidden';
}
}
 


target.classList.remove(
target.classList.remove(
375번째 줄: 206번째 줄:


/*
/*
* target 자체가 display:none이면
* content 자체는 항상 auto height.
* 먼저 화면에 복구
*/
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에는 height를 절대 설정하지 않음.
* 내부 TABLE 높이는 건드리지 않음.
*/
*/
element.style.height =
content.style.height = 'auto';
'auto';
 
element.style.overflow =
'hidden';


element.style.clipPath =
'inset(0 0 0 0)';


viewport.style.height = 'auto';
viewport.style.overflow = 'hidden';


var endHeight =
element.scrollHeight;


var finalHeight =
content.getBoundingClientRect()
.height;


/*
* wrapper의 레이아웃 높이만 0으로 설정.
*
* 내부 table은 온전한 크기로 계산된 상태.
*/
element.style.height =
'0px';


element.style.clipPath =
viewport.style.height = '0px';
'inset(0 0 100% 0)';




void element.offsetHeight;
void viewport.offsetHeight;




var animation =
var animation =
element.animate(
viewport.animate(
[
[
{
{
height:
height: '0px'
'0px',
 
clipPath:
'inset(0 0 100% 0)'
},
},
{
{
height:
height:
endHeight +
finalHeight +
'px',
'px'
 
clipPath:
'inset(0 0 0% 0)'
}
}
],
],
470번째 줄: 251번째 줄:
}
}
);
);
$( element ).data(
DATA_ANIMATION,
animation
);




481번째 줄: 256번째 줄:
function () {
function () {


$( element ).removeData(
viewport.style.height =
DATA_ANIMATION
);
 
 
/*
* 최종 상태는 CSS/HTML에 맡김
*/
element.style.height =
'';
'';


element.style.overflow =
viewport.style.overflow =
'';
'';


element.style.clipPath =
animation.cancel();
'';
 
 
if ( ownToggle ) {
ownToggle.style.visibility =
'';
}
 
 
try {
animation.cancel();
} catch ( e ) {
// 무시
}
};
};
}
}




/**
function collapse( target ) {
* 접기
*/
function collapse(
target,
element
) {
cancelAnimation(
element
);
 


rememberDisplay(
rememberDisplay(
530번째 줄: 273번째 줄:
);
);


rememberDisplay(
 
element
var viewport =
);
ensureViewport(
target
);
 
var content =
getContent(
viewport
);
 
 
if ( !content ) {
return;
}




/*
* 내부 table은 그대로 둠.
* viewport 높이만 줄임.
*/
var startHeight =
var startHeight =
element.getBoundingClientRect()
content.getBoundingClientRect()
.height;
.height;




/*
viewport.style.height =
* 내부 table 자체는 그대로 유지.
*/
element.style.height =
startHeight +
startHeight +
'px';
'px';


element.style.overflow =
viewport.style.overflow =
'hidden';
'hidden';


element.style.clipPath =
'inset(0 0 0% 0)';


 
void viewport.offsetHeight;
void element.offsetHeight;




var animation =
var animation =
element.animate(
viewport.animate(
[
[
{
{
height:
height:
startHeight +
startHeight +
'px',
'px'
 
clipPath:
'inset(0 0 0% 0)'
},
},
{
{
height:
height:
'0px',
'0px'
 
clipPath:
'inset(0 0 100% 0)'
}
}
],
],
587번째 줄: 334번째 줄:
}
}
);
);
$( element ).data(
DATA_ANIMATION,
animation
);




animation.onfinish =
animation.onfinish =
function () {
function () {
$( element ).removeData(
DATA_ANIMATION
);


target.classList.add(
target.classList.add(
607번째 줄: 343번째 줄:
);
);


target.style.display =
'none';


/*
* target 자체가 content인 구조
*/
if (
element === target
) {
target.style.display =
'none';


} else {
viewport.style.height =
 
/*
* 일반
* .mw-collapsible-content
*/
element.style.display =
'none';
}
 
 
element.style.height =
'';
'';


element.style.overflow =
viewport.style.overflow =
'';
'';


element.style.clipPath =
'';


 
animation.cancel();
try {
animation.cancel();
} catch ( e ) {
// 무시
}
};
};
}
}




/**
* 그룹 토글
*/
function toggleGroup( toggle ) {
function toggleGroup( toggle ) {
var name =
var name =
getToggleName(
getToggleName(
658번째 줄: 368번째 줄:


if ( !name ) {
if ( !name ) {
return false;
return;
}
}


667번째 줄: 377번째 줄:
);
);


if (
!targets ||
!targets.length
) {
return false;
}
var multipleTargets =
targets.length > 1;
var opened =
false;


var i;
var i;
690번째 줄: 386번째 줄:
i++
i++
) {
) {
var target =
var target =
targets[ i ];
targets[ i ];


var element =
getAnimatedElement(
target
);


var collapsed =
if (
isCollapsed(
isCollapsed(
target,
target
element
)
);
) {
 
 
if ( collapsed ) {
expand(
expand(
target,
target
element,
name,
multipleTargets
);
);
opened =
true;


} else {
} else {
collapse(
collapse(
target,
target
element
);
);
}
}
}
}
if (
targets.length === 1
) {
updateAria(
name,
opened
);
}
return true;
}
}




/**
* 클릭
*/
document.addEventListener(
document.addEventListener(
'click',
'click',
function ( event ) {
function ( event ) {
var toggle =
var toggle =
event.target.closest(
event.target.closest(
766번째 줄: 434번째 줄:


event.preventDefault();
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
event.stopImmediatePropagation();
event.stopImmediatePropagation();


780번째 줄: 446번째 줄:




/**
* Enter / Space
*/
document.addEventListener(
document.addEventListener(
'keydown',
'keydown',
function ( event ) {
function ( event ) {
if (
if (
event.key !== 'Enter' &&
event.key !== 'Enter' &&
801번째 줄: 465번째 줄:


if ( !toggle ) {
if ( !toggle ) {
return;
}
if (
!getToggleName(
toggle
)
) {
return;
return;
}
}
815번째 줄: 470번째 줄:


event.preventDefault();
event.preventDefault();
event.stopPropagation();
event.stopPropagation();
event.stopImmediatePropagation();
event.stopImmediatePropagation();


827번째 줄: 480번째 줄:
true
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 ) );
}( jQuery, mediaWiki ) );

2026년 8월 20일 (목) 14:24 판

( function ( $, mw ) {
	'use strict';

	var DURATION = 180;
	var EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';

	var VIEWPORT_CLASS = 'mw-collapse-viewport';
	var CONTENT_CLASS = 'mw-collapse-content';


	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 escapeAttribute( value ) {
		return String( value )
			.replace( /\\/g, '\\\\' )
			.replace( /"/g, '\\"' );
	}


	function getTargets( name ) {
		var id = 'mw-customcollapsible-' + name;

		return document.querySelectorAll(
			'[id="' + escapeAttribute( id ) + '"]'
		);
	}


	function isCollapsed( target ) {
		return (
			target.classList.contains( 'mw-collapsed' ) ||
			window.getComputedStyle( target ).display === 'none'
		);
	}


	/**
	 * target 내부에 viewport/content 구조 생성
	 *
	 * target
	 * └─ viewport
	 *    └─ content
	 *       └─ 기존 내용 전체
	 */
	function ensureViewport( target ) {

		var directChildren = target.children;
		var i;

		for ( i = 0; i < directChildren.length; i++ ) {
			if (
				directChildren[ i ].classList &&
				directChildren[ i ].classList.contains(
					VIEWPORT_CLASS
				)
			) {
				return directChildren[ i ];
			}
		}


		var viewport = document.createElement( 'div' );
		var content = document.createElement( 'div' );

		viewport.className = VIEWPORT_CLASS;
		content.className = CONTENT_CLASS;


		/*
		 * 중요:
		 * 기존 내용 전체를 content로 이동
		 */
		while ( target.firstChild ) {
			content.appendChild(
				target.firstChild
			);
		}


		viewport.appendChild( content );
		target.appendChild( viewport );


		viewport.style.overflow = 'hidden';


		return viewport;
	}


	function getContent( viewport ) {
		var children = viewport.children;
		var i;

		for ( i = 0; i < children.length; i++ ) {
			if (
				children[ i ].classList &&
				children[ i ].classList.contains(
					CONTENT_CLASS
				)
			) {
				return children[ i ];
			}
		}

		return null;
	}


	function getNaturalDisplay( target ) {

		if (
			target.dataset.mwCollapseDisplay
		) {
			return target.dataset.mwCollapseDisplay;
		}


		var style =
			window.getComputedStyle( target );


		if (
			style.display &&
			style.display !== 'none'
		) {
			target.dataset.mwCollapseDisplay =
				style.display;

			return style.display;
		}


		if (
			target.style.width === '100%'
		) {
			return 'block';
		}


		return 'block';
	}


	function rememberDisplay( target ) {

		var display =
			window.getComputedStyle(
				target
			).display;


		if (
			display &&
			display !== 'none'
		) {
			target.dataset.mwCollapseDisplay =
				display;
		}
	}


	function expand( target ) {

		var viewport =
			ensureViewport(
				target
			);

		var content =
			getContent(
				viewport
			);


		if ( !content ) {
			return;
		}


		/*
		 * target 바깥쪽만 표시
		 */
		target.style.display =
			getNaturalDisplay(
				target
			);

		target.classList.remove(
			'mw-collapsed'
		);


		/*
		 * content 자체는 항상 auto height.
		 *
		 * 내부 table에는 height를 절대 설정하지 않음.
		 */
		content.style.height = 'auto';


		viewport.style.height = 'auto';
		viewport.style.overflow = 'hidden';


		var finalHeight =
			content.getBoundingClientRect()
				.height;


		viewport.style.height = '0px';


		void viewport.offsetHeight;


		var animation =
			viewport.animate(
				[
					{
						height: '0px'
					},
					{
						height:
							finalHeight +
							'px'
					}
				],
				{
					duration:
						DURATION,

					easing:
						EASING,

					fill:
						'forwards'
				}
			);


		animation.onfinish =
			function () {

				viewport.style.height =
					'';

				viewport.style.overflow =
					'';

				animation.cancel();
			};
	}


	function collapse( target ) {

		rememberDisplay(
			target
		);


		var viewport =
			ensureViewport(
				target
			);

		var content =
			getContent(
				viewport
			);


		if ( !content ) {
			return;
		}


		/*
		 * 내부 table은 그대로 둠.
		 * viewport 높이만 줄임.
		 */
		var startHeight =
			content.getBoundingClientRect()
				.height;


		viewport.style.height =
			startHeight +
			'px';

		viewport.style.overflow =
			'hidden';


		void viewport.offsetHeight;


		var animation =
			viewport.animate(
				[
					{
						height:
							startHeight +
							'px'
					},
					{
						height:
							'0px'
					}
				],
				{
					duration:
						DURATION,

					easing:
						EASING,

					fill:
						'forwards'
				}
			);


		animation.onfinish =
			function () {

				target.classList.add(
					'mw-collapsed'
				);

				target.style.display =
					'none';


				viewport.style.height =
					'';

				viewport.style.overflow =
					'';


				animation.cancel();
			};
	}


	function toggleGroup( toggle ) {

		var name =
			getToggleName(
				toggle
			);


		if ( !name ) {
			return;
		}


		var targets =
			getTargets(
				name
			);


		var i;


		for (
			i = 0;
			i < targets.length;
			i++
		) {

			var target =
				targets[ i ];


			if (
				isCollapsed(
					target
				)
			) {
				expand(
					target
				);

			} else {
				collapse(
					target
				);
			}
		}
	}


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


	document.addEventListener(
		'keydown',
		function ( event ) {

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


			var toggle =
				event.target.closest(
					'.mw-customtoggle'
				);


			if ( !toggle ) {
				return;
			}


			event.preventDefault();
			event.stopPropagation();
			event.stopImmediatePropagation();


			toggleGroup(
				toggle
			);
		},
		true
	);


}( jQuery, mediaWiki ) );