|
|
| 1번째 줄: |
1번째 줄: |
| /** | | /** |
| * MediaWiki 커스텀 펼치기/접기 애니메이션 | | * 식별자에 대응하는 실제 collapsible 대상 찾기 |
| * | | * |
| * 지원: | | * 동일한 id가 여러 번 사용되는 나무위키식/리브레위키식 |
| | * 둘러보기 틀 구조도 지원. |
| * | | * |
| * .mw-customtoggle-이름 | | * 예: |
| * ↕
| |
| * .mw-customcollapsible-이름
| |
| * | | * |
| * 예: | | * 1. 상단 버튼용 |
| | * <div class="mw-collapsible" |
| | * id="mw-customcollapsible-XXX"> |
| | * <div class="mw-customtoggle-XXX">...</div> |
| | * </div> |
| | * |
| | * 2. 실제 펼침 영역 |
| | * <div class="mw-collapsible mw-collapsed" |
| | * id="mw-customcollapsible-XXX"> |
| | * <div class="mw-customtoggle-XXX">...</div> |
| | * <table>...</table> |
| | * </div> |
| * | | * |
| * .mw-customtoggle-KSC_LEADER | | * → 2번을 선택 |
| * .mw-customcollapsible-KSC_LEADER
| |
| */ | | */ |
| | | function getTarget( name ) { |
| ( function () {
| | if ( !name ) { |
| 'use strict'; | |
| | |
| var DURATION = 300;
| |
| var EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
| |
| | |
| var ANIMATING_CLASS =
| |
| 'mw-custom-collapse-animating';
| |
| | |
| var INITIALIZED_CLASS =
| |
| 'mw-custom-collapse-initialized';
| |
| | |
| | |
| /**
| |
| * 토글의 식별자 추출
| |
| *
| |
| * mw-customtoggle-KSC_LEADER
| |
| * → KSC_LEADER
| |
| */
| |
| 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; | | return null; |
| } | | } |
|
| |
|
| | var id = |
| | 'mw-customcollapsible-' + name; |
|
| |
|
| /** | | /* |
| * CSS selector escape | | * getElementById()를 사용하지 않는다. |
| | * |
| | * 같은 ID가 여러 개 존재하는 위키 문법을 |
| | * 지원하기 위해 attribute selector로 전부 가져온다. |
| */ | | */ |
| function escapeSelector( value ) { | | var candidates = |
| | | document.querySelectorAll( |
| if ( | | '[id="' + |
| window.CSS && | | escapeAttribute( id ) + |
| typeof window.CSS.escape === 'function'
| | '"], .mw-customcollapsible-' + |
| ) {
| | escapeSelector( name ) |
| return window.CSS.escape( value ); | |
| }
| |
| | |
| return value.replace(
| |
| /([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, | |
| '\\$1' | |
| ); | | ); |
| }
| |
|
| |
|
| | | if ( !candidates.length ) { |
| /** | | return null; |
| * 토글 이름에 대응하는 대상 찾기
| |
| */
| |
| function getTarget( name ) {
| |
| | |
| if ( !name ) {
| |
| return null;
| |
| }
| |
| | |
| var selector =
| |
| '.mw-customcollapsible-' +
| |
| escapeSelector( name );
| |
| | |
| var target =
| |
| document.querySelector( selector );
| |
| | |
| if ( target ) {
| |
| return target;
| |
| }
| |
| | |
| return document.getElementById( | |
| 'mw-customcollapsible-' + name
| |
| );
| |
| } | | } |
|
| |
|
| | | if ( candidates.length === 1 ) { |
| /** | | return candidates[ 0 ]; |
| * 실제 접히는 콘텐츠 찾기
| |
| */
| |
| function getContent( target ) {
| |
| | |
| var content = target.querySelector(
| |
| ':scope > .mw-collapsible-content'
| |
| );
| |
| | |
| if ( content ) {
| |
| return content;
| |
| }
| |
| | |
| content = target.querySelector(
| |
| '.mw-collapsible-content'
| |
| );
| |
| | |
| if ( content ) {
| |
| return content;
| |
| }
| |
| | |
| return target; | |
| } | | } |
|
| |
|
| | var bestTarget = null; |
| | var bestScore = -1; |
| | var i; |
|
| |
|
| /** | | for ( i = 0; i < candidates.length; i++ ) { |
| * 요소의 자연스러운 display 값
| | var candidate = candidates[ i ]; |
| */
| | var score = 0; |
| function getNaturalDisplay( element ) {
| |
| | |
| 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';
| |
| | |
| default:
| |
| return 'block';
| |
| }
| |
| }
| |
| | |
| | |
| /**
| |
| * 현재 접힌 상태인지 확인
| |
| */
| |
| function isCollapsed( target, content ) {
| |
|
| |
|
| | /* |
| | * 실제 펼침 영역은 보통 mw-collapsed를 가지고 시작 |
| | */ |
| if ( | | if ( |
| target.classList.contains( | | candidate.classList.contains( |
| 'mw-collapsed' | | 'mw-collapsed' |
| ) | | ) |
| ) { | | ) { |
| return true; | | score += 1000; |
| } | | } |
|
| |
| var style =
| |
| window.getComputedStyle( content );
| |
|
| |
| return (
| |
| style.display === 'none' ||
| |
| content.hidden === true
| |
| );
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 현재 실행 중인 애니메이션 취소
| |
| */
| |
| function cancelCurrentAnimation( content ) {
| |
|
| |
| if ( content._mwCollapseAnimation ) {
| |
|
| |
| try {
| |
| content._mwCollapseAnimation.cancel();
| |
| } catch ( e ) {
| |
| // 무시
| |
| }
| |
|
| |
| content._mwCollapseAnimation = null;
| |
| }
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 펼치기
| |
| */
| |
| function expand(
| |
| target,
| |
| content,
| |
| toggle
| |
| ) {
| |
|
| |
| cancelCurrentAnimation( content );
| |
|
| |
| content.classList.add(
| |
| ANIMATING_CLASS
| |
| );
| |
|
| |
| /*
| |
| * MediaWiki의 접힘 클래스 먼저 제거
| |
| */
| |
| target.classList.remove(
| |
| 'mw-collapsed'
| |
| );
| |
|
| |
| content.hidden = false;
| |
|
| |
|
| |
| /*
| |
| * display:none 해제
| |
| */
| |
| content.style.display =
| |
| getNaturalDisplay( content );
| |
|
| |
| content.style.overflow = 'hidden';
| |
|
| |
| content.style.height = 'auto';
| |
|
| |
|
| |
| /*
| |
| * 실제 최종 높이 계산
| |
| */
| |
| var targetHeight =
| |
| content.scrollHeight;
| |
|
| |
|
| |
| /*
| |
| * 애니메이션 시작 상태
| |
| */
| |
| content.style.height = '0px';
| |
| content.style.opacity = '0';
| |
|
| |
|
| |
|
| /* | | /* |
| * 강제 reflow | | * MediaWiki가 생성한 실제 콘텐츠 |
| */ | | */ |
| void content.offsetHeight;
| |
|
| |
|
| |
| var animation = content.animate(
| |
| [
| |
| {
| |
| height: '0px',
| |
| opacity: 0
| |
| },
| |
| {
| |
| height:
| |
| targetHeight + 'px',
| |
| opacity: 1
| |
| }
| |
| ],
| |
| {
| |
| duration: DURATION,
| |
| easing: EASING,
| |
| fill: 'forwards'
| |
| }
| |
| );
| |
|
| |
|
| |
| content._mwCollapseAnimation =
| |
| animation;
| |
|
| |
|
| |
| animation.onfinish = function () {
| |
|
| |
| content._mwCollapseAnimation =
| |
| null;
| |
|
| |
| content.classList.remove(
| |
| ANIMATING_CLASS
| |
| );
| |
|
| |
|
| |
| /*
| |
| * 애니메이션 스타일 정리
| |
| */
| |
| content.style.height = '';
| |
| content.style.overflow = '';
| |
| content.style.opacity = '';
| |
|
| |
| /*
| |
| * display는 제거해서
| |
| * 기존 위키 스타일에 맡김
| |
| */
| |
| content.style.display = '';
| |
|
| |
|
| |
| toggle.setAttribute(
| |
| 'aria-expanded',
| |
| 'true'
| |
| );
| |
|
| |
|
| |
| target.setAttribute(
| |
| 'aria-expanded',
| |
| 'true'
| |
| );
| |
| };
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 접기
| |
| */
| |
| function collapse(
| |
| target,
| |
| content,
| |
| toggle
| |
| ) {
| |
|
| |
| cancelCurrentAnimation( content );
| |
|
| |
| content.classList.add(
| |
| ANIMATING_CLASS
| |
| );
| |
|
| |
|
| |
| /*
| |
| * 현재 화면상 실제 높이
| |
| */
| |
| var startHeight =
| |
| content.getBoundingClientRect().height;
| |
|
| |
|
| |
| /*
| |
| * display 유지
| |
| */
| |
| content.style.display =
| |
| getNaturalDisplay( content );
| |
|
| |
| content.style.overflow =
| |
| 'hidden';
| |
|
| |
| content.style.height =
| |
| startHeight + 'px';
| |
|
| |
| content.style.opacity = '1';
| |
|
| |
|
| |
| /*
| |
| * 강제 reflow
| |
| */
| |
| void content.offsetHeight;
| |
|
| |
|
| |
| var animation = content.animate(
| |
| [
| |
| {
| |
| height:
| |
| startHeight + 'px',
| |
| opacity: 1
| |
| },
| |
| {
| |
| height: '0px',
| |
| opacity: 0
| |
| }
| |
| ],
| |
| {
| |
| duration: DURATION,
| |
| easing: EASING,
| |
| fill: 'forwards'
| |
| }
| |
| );
| |
|
| |
|
| |
| content._mwCollapseAnimation =
| |
| animation;
| |
|
| |
|
| |
| animation.onfinish = function () {
| |
|
| |
| content._mwCollapseAnimation =
| |
| null;
| |
|
| |
| target.classList.add(
| |
| 'mw-collapsed'
| |
| );
| |
|
| |
| content.classList.remove(
| |
| ANIMATING_CLASS
| |
| );
| |
|
| |
|
| |
| /*
| |
| * MediaWiki 기본 접힘 상태와
| |
| * 동일하게 숨김
| |
| */
| |
| content.style.display =
| |
| 'none';
| |
|
| |
| content.style.height = '';
| |
| content.style.overflow = '';
| |
| content.style.opacity = '';
| |
|
| |
|
| |
| toggle.setAttribute(
| |
| 'aria-expanded',
| |
| 'false'
| |
| );
| |
|
| |
|
| |
| target.setAttribute(
| |
| 'aria-expanded',
| |
| 'false'
| |
| );
| |
| };
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 실제 토글
| |
| */
| |
| function toggleCollapsible( toggle ) {
| |
|
| |
| var name =
| |
| getToggleName( toggle );
| |
|
| |
| if ( !name ) {
| |
| return false;
| |
| }
| |
|
| |
|
| |
| var target =
| |
| getTarget( name );
| |
|
| |
| if ( !target ) {
| |
| return false;
| |
| }
| |
|
| |
|
| |
| var content =
| |
| getContent( target );
| |
|
| |
| if ( !content ) {
| |
| return false;
| |
| }
| |
|
| |
|
| |
| if ( | | if ( |
| isCollapsed( | | candidate.querySelector( |
| target, | | '.mw-collapsible-content' |
| content
| |
| ) | | ) |
| ) { | | ) { |
| | | score += 500; |
| expand( | |
| target,
| |
| content,
| |
| toggle
| |
| );
| |
| | |
| } else {
| |
| | |
| collapse(
| |
| target,
| |
| content,
| |
| toggle
| |
| );
| |
| } | | } |
|
| |
|
| | | /* |
| return true; | | * 실제 내용에 표가 존재하는 경우 |
| }
| | */ |
| | |
| | |
| /**
| |
| * 토글 초기화
| |
| */
| |
| function initializeToggle( toggle ) {
| |
| | |
| if ( | | if ( |
| toggle.classList.contains( | | candidate.querySelector( |
| INITIALIZED_CLASS | | 'table' |
| ) | | ) |
| ) { | | ) { |
| return; | | score += 200; |
| }
| |
| | |
| | |
| var name =
| |
| getToggleName( toggle );
| |
| | |
| if ( !name ) {
| |
| return;
| |
| }
| |
| | |
| | |
| var target =
| |
| getTarget( name );
| |
| | |
| if ( !target ) {
| |
| return;
| |
| } | | } |
|
| |
|
| |
| var content =
| |
| getContent( target );
| |
|
| |
|
| |
| toggle.classList.add(
| |
| INITIALIZED_CLASS
| |
| );
| |
|
| |
|
| |
|
| /* | | /* |
| * 접근성 | | * wikitable |
| */ | | */ |
| if ( | | if ( |
| !toggle.hasAttribute( 'role' ) | | candidate.querySelector( |
| | '.wikitable' |
| | ) |
| ) { | | ) { |
| toggle.setAttribute( | | score += 200; |
| 'role',
| |
| 'button'
| |
| );
| |
| } | | } |
|
| |
|
| | /* |
| | * 내부 요소가 많을수록 실제 내용 영역일 가능성이 높음 |
| | */ |
| | score += |
| | candidate.querySelectorAll( '*' ).length; |
|
| |
|
| if ( | | if ( score > bestScore ) { |
| !toggle.hasAttribute( 'tabindex' )
| | bestScore = score; |
| ) {
| | bestTarget = candidate; |
| toggle.setAttribute( | |
| 'tabindex',
| |
| '0'
| |
| ); | |
| } | | } |
|
| |
|
| |
| var collapsed =
| |
| isCollapsed(
| |
| target,
| |
| content
| |
| );
| |
|
| |
|
| |
| toggle.setAttribute(
| |
| 'aria-expanded',
| |
| collapsed ?
| |
| 'false' :
| |
| 'true'
| |
| );
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 페이지 내 토글 초기화
| |
| */
| |
| function initializeAll( root ) {
| |
|
| |
| var toggles =
| |
| root.querySelectorAll(
| |
| '[class*="mw-customtoggle-"]'
| |
| );
| |
|
| |
| var i;
| |
|
| |
| for (
| |
| i = 0;
| |
| i < toggles.length;
| |
| i++
| |
| ) {
| |
| initializeToggle(
| |
| toggles[ i ]
| |
| );
| |
| }
| |
| }
| |
|
| |
|
| |
| /**
| |
| * 클릭
| |
| *
| |
| * MediaWiki jquery.makeCollapsible보다
| |
| * 먼저 가로채기 위해 capture=true
| |
| */
| |
| document.addEventListener(
| |
| 'click',
| |
| function ( event ) {
| |
|
| |
| var toggle =
| |
| event.target.closest(
| |
| '.mw-customtoggle, [class*="mw-customtoggle-"]'
| |
| );
| |
|
| |
| if ( !toggle ) {
| |
| return;
| |
| }
| |
|
| |
|
| |
| if (
| |
| !getToggleName( toggle )
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
|
| |
| event.preventDefault();
| |
|
| |
| event.stopPropagation();
| |
|
| |
| event.stopImmediatePropagation();
| |
|
| |
|
| |
| toggleCollapsible(
| |
| toggle
| |
| );
| |
| },
| |
| true
| |
| );
| |
|
| |
|
| |
| /**
| |
| * Enter / Space
| |
| */
| |
| document.addEventListener(
| |
| 'keydown',
| |
| function ( event ) {
| |
|
| |
| if (
| |
| event.key !== 'Enter' &&
| |
| event.key !== ' '
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
|
| |
| var toggle =
| |
| event.target.closest(
| |
| '.mw-customtoggle, [class*="mw-customtoggle-"]'
| |
| );
| |
|
| |
| if ( !toggle ) {
| |
| return;
| |
| }
| |
|
| |
|
| |
| if (
| |
| !getToggleName( toggle )
| |
| ) {
| |
| return;
| |
| }
| |
|
| |
|
| |
| event.preventDefault();
| |
|
| |
| event.stopPropagation();
| |
|
| |
| event.stopImmediatePropagation();
| |
|
| |
|
| |
| toggleCollapsible(
| |
| toggle
| |
| );
| |
| },
| |
| true
| |
| );
| |
|
| |
|
| |
| /**
| |
| * 최초 로드
| |
| */
| |
| if (
| |
| document.readyState ===
| |
| 'loading'
| |
| ) {
| |
|
| |
| document.addEventListener(
| |
| 'DOMContentLoaded',
| |
| function () {
| |
| initializeAll(
| |
| document
| |
| );
| |
| }
| |
| );
| |
|
| |
| } else {
| |
|
| |
| initializeAll(
| |
| document
| |
| );
| |
| }
| |
|
| |
|
| |
| /**
| |
| * MediaWiki 동적 콘텐츠 대응
| |
| */
| |
| if (
| |
| window.mw &&
| |
| mw.hook
| |
| ) {
| |
|
| |
| mw.hook(
| |
| 'wikipage.content'
| |
| ).add(
| |
| function ( $content ) {
| |
|
| |
| if (
| |
| $content &&
| |
| $content[ 0 ]
| |
| ) {
| |
|
| |
| initializeAll(
| |
| $content[ 0 ]
| |
| );
| |
| }
| |
| }
| |
| );
| |
| } | | } |
|
| |
|
| }() ); | | return bestTarget; |
| | } |