미디어위키:Gadget-core.js: 두 판 사이의 차이
둘러보기로 이동
검색으로 이동
새 문서: →========================= MediaWiki:Gadget-core.js =========================: (function () { 'use strict'; document.documentElement.classList.add('pvz-desktop-booting'); var PVZ_SEARCH_TIMER = null; var PVZ_SEARCH_REQUEST_ID = 0; var PVZ_SELECTED_SUGGESTION = -1; var PVZ_SUGGESTIONS = []; var PVZ_LAST_SEARCH_QUERY = ''; function pvzReady(callback) { function run() { if (window.mw && mw.loader && mw.loader.using) { mw.loader.using('media... |
편집 요약 없음 태그: 모바일 편집 모바일 웹 편집 |
||
| 1번째 줄: | 1번째 줄: | ||
/* ===== core-js.txt ===== */ | |||
/* ========================= | /* ========================= | ||
MediaWiki:Gadget-core.js | MediaWiki:Gadget-core.js | ||
| 2,159번째 줄: | 2,160번째 줄: | ||
pvzReady(pvzInit); | pvzReady(pvzInit); | ||
}()); | |||
/* ===== mobile-js.txt ===== */ | |||
/* ========================= | |||
MediaWiki:Gadget-mobile.js | |||
Timeless 모바일 알림·리본·본문 프레임 재정리 v125 | |||
- v124 기준 | |||
- 검색바 1280px 이상 판정을 visualViewport가 아닌 레이아웃 viewport 기준으로 변경 | |||
- 아이패드 가로 화면 핀치 확대 시 검색바가 100% 폭으로 풀리는 문제 수정 | |||
- 검색바 왼쪽 돋보기 아이콘 제거 | |||
- 검색 문구를 '위키에서 검색하세요'로 변경 | |||
- 검색 제안에서 입력 문자열과 일치하는 부분만 굵게 강조 | |||
- MediaWiki API 기반 검색 제안과 문서 대표 이미지 표시 유지 | |||
- 한글 IME 입력·키보드 탐색·바깥 클릭 닫기 대응 유지 | |||
- 화면 회전·리사이즈·본문 폭 변화 시 검색바 폭 자동 재계산 | |||
- 사용자 환경 기반 초기 테마 적용 유지 | |||
- Timeless 상단 패널 absolute 문서 좌표 방식 유지 | |||
- 모바일 목차 .mf-section-0 대응 유지 | |||
- footer 아래 실제 여백 감지 기능 유지 | |||
========================= */ | |||
(function () { | |||
'use strict'; | |||
document.documentElement.classList.add('pvz-timeless-mobile-booting'); | |||
function pvzArray(list) { | |||
return Array.prototype.slice.call(list || []); | |||
} | |||
function pvzIsTimeless() { | |||
var skinName = ''; | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
skinName = mw.config.get('skin') || mw.config.get('wgDefaultSkin') || ''; | |||
} | |||
return skinName === 'timeless' || | |||
(document.body && document.body.classList && document.body.classList.contains('skin-timeless')); | |||
} | |||
function pvzMarkReady() { | |||
document.documentElement.classList.add('pvz-timeless-mobile-ready'); | |||
document.documentElement.classList.add('pvz-timeless-mobile-booted'); | |||
if (document.body) { | |||
document.body.classList.add('pvz-timeless-mobile-ready'); | |||
} | |||
} | |||
function pvzSkipHeading(heading) { | |||
if (!heading) return true; | |||
if (heading.closest && heading.closest('.pvz-custom-toc, .toc, #toc, .navbox, .metadata, .catlinks, .mw-references-wrap')) return true; | |||
if (heading.classList && heading.classList.contains('pvz-custom-toc-title')) return true; | |||
return !(heading.textContent || '').replace(/\s+/g, ' ').trim(); | |||
} | |||
function pvzHeadingText(heading) { | |||
var clone = heading.cloneNode(true); | |||
var text; | |||
clone.querySelectorAll('.mw-editsection, .mw-headline-anchor, .reference, sup').forEach(function (node) { | |||
node.remove(); | |||
}); | |||
text = (clone.textContent || '').replace(/\s+/g, ' ').trim(); | |||
return text || '문단'; | |||
} | |||
function pvzHeadingId(heading, text, index) { | |||
var headline = heading.querySelector && heading.querySelector('.mw-headline'); | |||
var anchor = headline || heading; | |||
var id = anchor.id; | |||
if (!id) { | |||
id = 'pvz-heading-' + index + '-' + encodeURIComponent(text).replace(/%/g, ''); | |||
anchor.id = id; | |||
} | |||
return id; | |||
} | |||
function pvzNormalizeText(text) { | |||
return (text || '').replace(/\s+/g, ' ').trim(); | |||
} | |||
function pvzLinkKey(link) { | |||
var href = link && link.getAttribute ? link.getAttribute('href') || '' : ''; | |||
var text = pvzNormalizeText(link ? link.textContent : ''); | |||
return href + '|' + text; | |||
} | |||
function pvzFindExistingLink(config) { | |||
var links = pvzArray(document.querySelectorAll('a[href]')); | |||
var idPatterns = config.ids || []; | |||
var textPatterns = config.text || []; | |||
var hrefPatterns = config.href || []; | |||
var i; | |||
var link; | |||
var href; | |||
var text; | |||
var id; | |||
for (i = 0; i < links.length; i += 1) { | |||
link = links[i]; | |||
id = link.id || (link.parentNode && link.parentNode.id) || ''; | |||
if (id && idPatterns.some(function (pattern) { | |||
return pattern.test(id); | |||
})) { | |||
return link; | |||
} | |||
} | |||
for (i = 0; i < links.length; i += 1) { | |||
link = links[i]; | |||
href = link.getAttribute('href') || ''; | |||
if (href && hrefPatterns.some(function (pattern) { | |||
return pattern.test(href); | |||
})) { | |||
return link; | |||
} | |||
} | |||
for (i = 0; i < links.length; i += 1) { | |||
link = links[i]; | |||
text = pvzNormalizeText(link.textContent); | |||
if (text && textPatterns.some(function (pattern) { | |||
return pattern.test(text); | |||
})) { | |||
return link; | |||
} | |||
} | |||
return null; | |||
} | |||
function pvzMakeFallbackUrl(type) { | |||
var pageName = ''; | |||
var title = ''; | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
pageName = mw.config.get('wgPageName') || ''; | |||
title = mw.config.get('wgTitle') || pageName || ''; | |||
} | |||
if (!window.mw || !mw.util || typeof mw.util.getUrl !== 'function') { | |||
return ''; | |||
} | |||
if (type === 'upload') return mw.util.getUrl('Special:Upload'); | |||
if (type === 'shorturl') return mw.util.getUrl('Special:UrlShortener'); | |||
if (type === 'whatlinkshere') return mw.util.getUrl('Special:WhatLinksHere/' + pageName); | |||
if (type === 'recentchangeslinked') return mw.util.getUrl('Special:RecentChangesLinked/' + pageName); | |||
if (type === 'printable') return mw.util.getUrl(pageName, { printable: 'yes' }); | |||
if (type === 'permalink') { | |||
if (mw.config.get('wgRevisionId')) { | |||
return mw.util.getUrl(pageName, { oldid: mw.config.get('wgRevisionId') }); | |||
} | |||
return ''; | |||
} | |||
if (type === 'info') return mw.util.getUrl(pageName, { action: 'info' }); | |||
if (type === 'history') return mw.util.getUrl(pageName, { action: 'history' }); | |||
/* 권한성 문서 도구는 기본 링크가 실제로 존재할 때만 노출한다. */ | |||
if (type === 'move') return ''; | |||
if (type === 'delete') return ''; | |||
if (type === 'protect') return ''; | |||
return title ? mw.util.getUrl(title) : ''; | |||
} | |||
function pvzToolEntry(label, type, matchConfig) { | |||
var found = pvzFindExistingLink(matchConfig || {}); | |||
var href = found ? found.getAttribute('href') : pvzMakeFallbackUrl(type); | |||
if (!href) return null; | |||
return { | |||
label: label, | |||
href: href, | |||
source: found || null | |||
}; | |||
} | |||
function pvzMakeSpecialUrl(specialName) { | |||
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') { | |||
return mw.util.getUrl('Special:' + specialName); | |||
} | |||
return '/wiki/Special:' + specialName; | |||
} | |||
function pvzMakePageUrl(pageName) { | |||
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') { | |||
return mw.util.getUrl(pageName); | |||
} | |||
return '/wiki/' + String(pageName || '').replace(/\s/g, '_'); | |||
} | |||
function pvzPageName() { | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
return mw.config.get('wgPageName') || ''; | |||
} | |||
return ''; | |||
} | |||
function pvzMainPageName() { | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
return mw.config.get('wgMainPageTitle') || '식물 vs 좀비 위키'; | |||
} | |||
return '식물 vs 좀비 위키'; | |||
} | |||
function pvzToolEntryWithFallback(label, type, matchConfig, fallbackHref) { | |||
var found = pvzFindExistingLink(matchConfig || {}); | |||
var href = found ? found.getAttribute('href') : fallbackHref; | |||
if (!href) return null; | |||
return { | |||
label: label, | |||
href: href, | |||
source: found || null | |||
}; | |||
} | |||
function pvzToolEntryFromNativeOnly(label, matchConfig) { | |||
var found = pvzFindExistingLink(matchConfig || {}); | |||
if (!found) return null; | |||
return { | |||
label: label, | |||
href: found.getAttribute('href'), | |||
source: found | |||
}; | |||
} | |||
function pvzCreateToolsSvgIcon() { | |||
var span = document.createElement('span'); | |||
var svgNS = 'http://www.w3.org/2000/svg'; | |||
var svg = document.createElementNS(svgNS, 'svg'); | |||
var path = document.createElementNS(svgNS, 'path'); | |||
span.className = 'pvz-custom-mobile-tools-icon'; | |||
span.setAttribute('aria-hidden', 'true'); | |||
svg.setAttribute('viewBox', '0 0 24 24'); | |||
svg.setAttribute('focusable', 'false'); | |||
path.setAttribute('d', 'M21.2 18.6 13.9 11.3c.7-1.9.3-4.1-1.2-5.6-1.6-1.6-4-2-6-1.1l3.5 3.5-2.1 2.1-3.6-3.5c-.8 2-.4 4.4 1.2 6 1.5 1.5 3.7 1.9 5.6 1.2l7.3 7.3c.3.3.8.3 1.1 0l1.5-1.5c.3-.3.3-.8 0-1.1Z'); | |||
svg.appendChild(path); | |||
span.appendChild(svg); | |||
return span; | |||
} | |||
function pvzCreateToolsToggle(shell) { | |||
var button = document.createElement('button'); | |||
var text = document.createElement('span'); | |||
button.type = 'button'; | |||
button.className = 'pvz-custom-mobile-tools-toggle'; | |||
button.setAttribute('aria-controls', 'pvz-custom-mobile-tools-list'); | |||
button.setAttribute('aria-expanded', 'false'); | |||
button.title = '도구'; | |||
text.className = 'pvz-custom-mobile-tools-toggle-text'; | |||
text.textContent = '도구'; | |||
button.appendChild(pvzCreateToolsSvgIcon()); | |||
button.appendChild(text); | |||
button.addEventListener('click', function (event) { | |||
var collapsed; | |||
event.preventDefault(); | |||
collapsed = !shell.classList.contains('is-collapsed'); | |||
shell.classList.toggle('is-collapsed', collapsed); | |||
shell.dataset.pvzManualToggle = '1'; | |||
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true'); | |||
}); | |||
return button; | |||
} | |||
function pvzApplyToolsCompactState(shell) { | |||
var button; | |||
var isCompact; | |||
if (!shell) return; | |||
button = shell.querySelector('.pvz-custom-mobile-tools-toggle'); | |||
isCompact = !!(window.matchMedia && window.matchMedia('(max-width: 760px)').matches); | |||
/* 모바일에서도 보조창은 기본적으로 보이게 둔다. | |||
사용자가 직접 도구 버튼을 눌렀을 때만 접힌 상태를 유지한다. */ | |||
if (!isCompact || shell.dataset.pvzManualToggle !== '1') { | |||
shell.classList.remove('is-collapsed'); | |||
} | |||
if (button) { | |||
button.setAttribute('aria-expanded', shell.classList.contains('is-collapsed') ? 'false' : 'true'); | |||
} | |||
} | |||
function pvzFindLogoBlockInside(anchor) { | |||
var candidates; | |||
var i; | |||
var node; | |||
if (!anchor || !anchor.querySelectorAll) return null; | |||
candidates = pvzArray(anchor.querySelectorAll( | |||
'#p-logo, .mw-logo, .mw-wiki-logo, #mw-wiki-logo, ' + | |||
'a.mw-wiki-logo, a[title*="식물 vs 좀비 위키"], a[aria-label*="식물 vs 좀비 위키"], ' + | |||
'a[href*="식물_vs_좀비_위키"], a[href*="Main_Page"]' | |||
)); | |||
for (i = 0; i < candidates.length; i += 1) { | |||
node = candidates[i]; | |||
while (node && node.parentNode && node.parentNode !== anchor) { | |||
if ( | |||
node.matches && | |||
node.matches('#p-logo, .mw-logo, .mw-wiki-logo, #mw-wiki-logo, .mw-portlet, nav, header, div, a') | |||
) { | |||
if (node.parentNode === anchor) return node; | |||
} | |||
node = node.parentNode; | |||
} | |||
if (node && node.parentNode === anchor) return node; | |||
} | |||
return null; | |||
} | |||
function pvzInsertToolsBelowLogo(anchor, shell) { | |||
var logoBlock; | |||
if (!anchor || !shell) return; | |||
logoBlock = pvzFindLogoBlockInside(anchor); | |||
if (logoBlock && logoBlock.parentNode === anchor) { | |||
if (logoBlock.nextSibling) { | |||
anchor.insertBefore(shell, logoBlock.nextSibling); | |||
} else { | |||
anchor.appendChild(shell); | |||
} | |||
return; | |||
} | |||
anchor.appendChild(shell); | |||
} | |||
function pvzCreateToolsSection(title, entries, sectionKey) { | |||
var section; | |||
var heading; | |||
var list; | |||
entries = (entries || []).filter(Boolean); | |||
if (!entries.length) return null; | |||
section = document.createElement('section'); | |||
section.className = 'pvz-custom-mobile-tools-section'; | |||
if (sectionKey) section.setAttribute('data-pvz-tools-section', sectionKey); | |||
heading = document.createElement('h2'); | |||
heading.className = 'pvz-custom-mobile-tools-heading'; | |||
heading.textContent = title; | |||
list = document.createElement('ul'); | |||
list.className = 'pvz-custom-mobile-tools-list'; | |||
entries.forEach(function (entry) { | |||
var item = document.createElement('li'); | |||
var link = document.createElement('a'); | |||
var text; | |||
item.className = 'pvz-custom-mobile-tools-item'; | |||
link.href = entry.href || '#'; | |||
if (entry.className) link.className = entry.className; | |||
if (entry.themeMode) { | |||
link.setAttribute('data-pvz-timeless-theme-mode', entry.themeMode); | |||
link.setAttribute('role', 'button'); | |||
} | |||
if (entry.icon) { | |||
link.appendChild(pvzCreateInlineSvgIcon(entry.icon)); | |||
text = document.createElement('span'); | |||
text.textContent = entry.label; | |||
link.appendChild(text); | |||
} else { | |||
link.textContent = entry.label; | |||
} | |||
if (entry.source) { | |||
if (entry.source.title) link.title = entry.source.title; | |||
if (entry.source.target) link.target = entry.source.target; | |||
} | |||
item.appendChild(link); | |||
list.appendChild(item); | |||
}); | |||
section.appendChild(heading); | |||
section.appendChild(list); | |||
return section; | |||
} | |||
function pvzFindTimelessToolsAnchor() { | |||
var selectors = [ | |||
'#mw-site-navigation', | |||
'#mw-related-navigation', | |||
'#mw-content', | |||
'#content', | |||
'#bodyContent', | |||
'.mw-body' | |||
]; | |||
var i; | |||
var node; | |||
for (i = 0; i < selectors.length; i += 1) { | |||
node = document.querySelector('body.skin-timeless ' + selectors[i]) || | |||
document.querySelector(selectors[i]); | |||
if (node) return node; | |||
} | |||
return document.body; | |||
} | |||
function pvzHideNativeTimelessToolPortlets() { | |||
var candidates = pvzArray(document.querySelectorAll( | |||
'body.skin-timeless #mw-site-navigation .mw-portlet, ' + | |||
'body.skin-timeless #mw-related-navigation .mw-portlet, ' + | |||
'body.skin-timeless #mw-site-navigation [id^="p-"], ' + | |||
'body.skin-timeless #mw-related-navigation [id^="p-"], ' + | |||
'body.skin-timeless nav[id^="p-"], ' + | |||
'body.skin-timeless div[id^="p-"], ' + | |||
'body.skin-timeless #catlinks, ' + | |||
'body.skin-timeless .catlinks' | |||
)); | |||
candidates.forEach(function (node) { | |||
var text; | |||
var id; | |||
if (!node || !node.classList) return; | |||
if (node.classList.contains('pvz-custom-mobile-tools') || node.classList.contains('pvz-custom-mobile-tools-shell') || node.classList.contains('pvz-timeless-wide-rail')) return; | |||
if (node.id === 'p-logo') return; | |||
if (node.closest && node.closest('.pvz-custom-mobile-tools-shell')) return; | |||
text = pvzNormalizeText(node.textContent); | |||
id = node.id || ''; | |||
if ( | |||
/p-(navigation|namespaces|views|variants|personal|tb|wiki-tools|wikitools|page-tools|actions|cactions|more|tb-page|tb-more|managewiki|management|admin)/i.test(id) || | |||
/둘러보기|Navigation|관리|Manage|위키 도구|Wiki tools|Site tools|문서 도구|Page tools|Document tools|더 보기|More/.test(text) || | |||
/대문|최근 바뀜|임의 문서|미디어위키 도움말|특수 문서 목록|Main page|Recent changes|Random page|Help|Special pages/.test(text) || | |||
/이 위키의 핵심 설정|이 위키의 확장 기능|이 위키의 이름공간|이 위키의 권한|이 위키의 추가 설정|이 위키의 백업/.test(text) || | |||
(/파일 올리기|Upload file/.test(text) && /축약된 URL|Short URL|Get shortened URL/.test(text)) || | |||
(/삭제|Delete/.test(text) && /이동|Move/.test(text) && /보호|Protect/.test(text)) || | |||
(/여기를 가리키는 문서|What links here/.test(text) && /문서 정보|Page information/.test(text)) | |||
) { | |||
node.classList.add('pvz-native-tools-hidden'); | |||
} | |||
}); | |||
pvzArray(document.querySelectorAll( | |||
'body.skin-timeless #mw-site-navigation, ' + | |||
'body.skin-timeless #mw-related-navigation' | |||
)).forEach(function (container) { | |||
var hasVisibleNonCustom = false; | |||
pvzArray(container.children).forEach(function (child) { | |||
if (!child || !child.classList) return; | |||
if (child.classList.contains('pvz-custom-mobile-tools-shell') || child.classList.contains('pvz-timeless-wide-rail')) return; | |||
if (child.id === 'p-logo' || child.matches('.mw-logo, .mw-wiki-logo, #mw-wiki-logo, header')) return; | |||
if (!child.classList.contains('pvz-native-tools-hidden')) { | |||
if ((child.textContent || '').replace(/\s+/g, '').length > 0) { | |||
hasVisibleNonCustom = true; | |||
} | |||
} | |||
}); | |||
if (!hasVisibleNonCustom) { | |||
container.classList.add('pvz-native-tools-container-cleaned'); | |||
} | |||
}); | |||
} | |||
function pvzRemoveTimelessColorStripes() { | |||
pvzArray(document.querySelectorAll( | |||
'body.skin-timeless #mw-header-hack, ' + | |||
'body.skin-timeless .color-bar, ' + | |||
'body.skin-timeless .header-color-bar, ' + | |||
'body.skin-timeless .mw-header-color-bar, ' + | |||
'body.skin-timeless #mw-header-hack .color-left, ' + | |||
'body.skin-timeless #mw-header-hack .color-middle, ' + | |||
'body.skin-timeless #mw-header-hack .color-right, ' + | |||
'body.skin-timeless #mw-header-container .color-left, ' + | |||
'body.skin-timeless #mw-header-container .color-middle, ' + | |||
'body.skin-timeless #mw-header-container .color-right, ' + | |||
'body.skin-timeless #mw-content-container .color-left, ' + | |||
'body.skin-timeless #mw-content-container .color-middle, ' + | |||
'body.skin-timeless #mw-content-container .color-right, ' + | |||
'body.skin-timeless [class~="color-left"], ' + | |||
'body.skin-timeless [class~="color-middle"], ' + | |||
'body.skin-timeless [class~="color-right"], ' + | |||
'body.skin-timeless [class*="color-left"], ' + | |||
'body.skin-timeless [class*="color-middle"], ' + | |||
'body.skin-timeless [class*="color-right"]' | |||
)).forEach(function (node) { | |||
if (node && node.parentNode) node.parentNode.removeChild(node); | |||
}); | |||
} | |||
function pvzCategoryEntriesForCustomBox() { | |||
var entries = []; | |||
var seen = {}; | |||
pvzArray(document.querySelectorAll('#catlinks a[href], .catlinks a[href]')).forEach(function (link) { | |||
var label = pvzNormalizeText(link.textContent); | |||
var href = link.getAttribute('href'); | |||
var key; | |||
if (!label || !href) return; | |||
if (/^(분류|Categories?|Category)$/i.test(label)) return; | |||
key = href + '|' + label; | |||
if (seen[key]) return; | |||
seen[key] = true; | |||
entries.push({ | |||
label: label, | |||
href: href, | |||
source: link | |||
}); | |||
}); | |||
return entries; | |||
} | |||
/* ========================= | |||
커스텀 검색바 시작 | |||
- 기존 Timeless/MediaWiki 헤더 검색창 대신 독립 검색 UI를 사용한다. | |||
- 실제 검색은 Special:Search로 제출한다. | |||
- 검색 제안은 MediaWiki API opensearch를 사용한다. | |||
- 1280px 이상에서는 실제 #mw-content의 위치와 폭을 그대로 따라간다. | |||
========================= */ | |||
var pvzCustomSearchInputTimer = null; | |||
var pvzCustomSearchRequestId = 0; | |||
var pvzCustomSearchActiveIndex = -1; | |||
var pvzCustomSearchComposing = false; | |||
var pvzCustomSearchEventsBound = false; | |||
var pvzCustomSearchViewportBound = false; | |||
var pvzCustomSearchResizeObserver = null; | |||
var pvzCustomSearchWidthFrame = 0; | |||
var pvzSearchThumbnailCache = Object.create(null); | |||
function pvzCreateSearchSvgIcon() { | |||
var span = document.createElement('span'); | |||
var svgNS = 'http://www.w3.org/2000/svg'; | |||
var svg = document.createElementNS(svgNS, 'svg'); | |||
var circle = document.createElementNS(svgNS, 'circle'); | |||
var line = document.createElementNS(svgNS, 'path'); | |||
span.className = 'pvz-custom-search-icon'; | |||
span.setAttribute('aria-hidden', 'true'); | |||
svg.setAttribute('viewBox', '0 0 24 24'); | |||
svg.setAttribute('focusable', 'false'); | |||
circle.setAttribute('cx', '10.8'); | |||
circle.setAttribute('cy', '10.8'); | |||
circle.setAttribute('r', '6.6'); | |||
circle.setAttribute('fill', 'none'); | |||
circle.setAttribute('stroke', 'currentColor'); | |||
circle.setAttribute('stroke-width', '2.2'); | |||
line.setAttribute('d', 'M15.7 15.7 21 21'); | |||
line.setAttribute('fill', 'none'); | |||
line.setAttribute('stroke', 'currentColor'); | |||
line.setAttribute('stroke-width', '2.2'); | |||
line.setAttribute('stroke-linecap', 'round'); | |||
svg.appendChild(circle); | |||
svg.appendChild(line); | |||
span.appendChild(svg); | |||
return span; | |||
} | |||
function pvzCustomSearchElements() { | |||
return { | |||
root: document.getElementById('pvz-custom-searchbox'), | |||
form: document.getElementById('pvz-custom-searchbox-form'), | |||
input: document.getElementById('pvz-custom-searchbox-input'), | |||
suggestions: document.getElementById('pvz-custom-searchbox-suggestions') | |||
}; | |||
} | |||
function pvzClearCustomSearchSuggestions() { | |||
var elements = pvzCustomSearchElements(); | |||
pvzCustomSearchActiveIndex = -1; | |||
if (elements.suggestions) { | |||
elements.suggestions.textContent = ''; | |||
elements.suggestions.hidden = true; | |||
elements.suggestions.classList.remove('is-open'); | |||
} | |||
if (elements.input) { | |||
elements.input.setAttribute('aria-expanded', 'false'); | |||
elements.input.removeAttribute('aria-activedescendant'); | |||
} | |||
if (document.body) { | |||
document.body.classList.remove('pvz-custom-search-suggestions-open'); | |||
} | |||
} | |||
function pvzCustomSearchSuggestionLinks() { | |||
var elements = pvzCustomSearchElements(); | |||
if (!elements.suggestions) return []; | |||
return pvzArray( | |||
elements.suggestions.querySelectorAll('.pvz-custom-search-suggestion') | |||
); | |||
} | |||
function pvzSetCustomSearchActiveIndex(index) { | |||
var elements = pvzCustomSearchElements(); | |||
var links = pvzCustomSearchSuggestionLinks(); | |||
if (!links.length || !elements.input) { | |||
pvzCustomSearchActiveIndex = -1; | |||
return; | |||
} | |||
if (index < 0) index = links.length - 1; | |||
if (index >= links.length) index = 0; | |||
pvzCustomSearchActiveIndex = index; | |||
links.forEach(function (link, currentIndex) { | |||
var active = currentIndex === index; | |||
link.classList.toggle('is-active', active); | |||
link.setAttribute('aria-selected', active ? 'true' : 'false'); | |||
}); | |||
elements.input.setAttribute( | |||
'aria-activedescendant', | |||
links[index].id | |||
); | |||
if (typeof links[index].scrollIntoView === 'function') { | |||
links[index].scrollIntoView({ | |||
block: 'nearest' | |||
}); | |||
} | |||
} | |||
function pvzLoadSearchThumbnail(title) { | |||
if (!title) return Promise.resolve(''); | |||
if (Object.prototype.hasOwnProperty.call(pvzSearchThumbnailCache, title)) { | |||
return pvzSearchThumbnailCache[title]; | |||
} | |||
if (!window.mw || !mw.loader) { | |||
return Promise.resolve(''); | |||
} | |||
pvzSearchThumbnailCache[title] = mw.loader.using('mediawiki.api').then(function () { | |||
var api = new mw.Api(); | |||
return api.get({ | |||
action: 'query', | |||
prop: 'pageimages', | |||
piprop: 'thumbnail', | |||
pithumbsize: 80, | |||
titles: title, | |||
formatversion: 2 | |||
}); | |||
}).then(function (data) { | |||
var pages = data && data.query && data.query.pages; | |||
var page = pages && pages[0]; | |||
return page && page.thumbnail && page.thumbnail.source ? | |||
page.thumbnail.source : | |||
''; | |||
}).catch(function () { | |||
return ''; | |||
}); | |||
return pvzSearchThumbnailCache[title]; | |||
} | |||
function pvzAppendHighlightedSearchText(container, text, query) { | |||
var source = String(text || ''); | |||
var needle = String(query || ''); | |||
var sourceLower; | |||
var needleLower; | |||
var cursor = 0; | |||
var index; | |||
var strong; | |||
container.textContent = ''; | |||
if (!needle) { | |||
container.textContent = source; | |||
return; | |||
} | |||
sourceLower = source.toLocaleLowerCase(); | |||
needleLower = needle.toLocaleLowerCase(); | |||
while (cursor < source.length) { | |||
index = sourceLower.indexOf(needleLower, cursor); | |||
if (index === -1) { | |||
container.appendChild( | |||
document.createTextNode(source.slice(cursor)) | |||
); | |||
break; | |||
} | |||
if (index > cursor) { | |||
container.appendChild( | |||
document.createTextNode(source.slice(cursor, index)) | |||
); | |||
} | |||
strong = document.createElement('strong'); | |||
strong.className = 'pvz-custom-search-suggestion-match'; | |||
strong.textContent = source.slice(index, index + needle.length); | |||
container.appendChild(strong); | |||
cursor = index + needle.length; | |||
} | |||
if (!source.length) { | |||
container.textContent = ''; | |||
} | |||
} | |||
function pvzRenderCustomSearchSuggestions(query, data, requestId) { | |||
var elements = pvzCustomSearchElements(); | |||
var titles; | |||
var urls; | |||
if ( | |||
requestId !== pvzCustomSearchRequestId || | |||
!elements.suggestions || | |||
!elements.input | |||
) { | |||
return; | |||
} | |||
if (pvzNormalizeText(elements.input.value) !== query) return; | |||
titles = Array.isArray(data && data[1]) ? data[1] : []; | |||
urls = Array.isArray(data && data[3]) ? data[3] : []; | |||
elements.suggestions.textContent = ''; | |||
pvzCustomSearchActiveIndex = -1; | |||
if (!titles.length) { | |||
pvzClearCustomSearchSuggestions(); | |||
return; | |||
} | |||
titles.slice(0, 7).forEach(function (title, index) { | |||
var link = document.createElement('a'); | |||
var thumb = document.createElement('span'); | |||
var label = document.createElement('span'); | |||
var href = urls[index] || pvzMakePageUrl(title); | |||
link.id = 'pvz-custom-search-option-' + index; | |||
link.className = 'pvz-custom-search-suggestion'; | |||
link.href = href; | |||
link.setAttribute('role', 'option'); | |||
link.setAttribute('aria-selected', 'false'); | |||
thumb.className = 'pvz-custom-search-suggestion-thumb'; | |||
thumb.setAttribute('aria-hidden', 'true'); | |||
label.className = 'pvz-custom-search-suggestion-label'; | |||
pvzAppendHighlightedSearchText(label, title, query); | |||
link.appendChild(thumb); | |||
link.appendChild(label); | |||
elements.suggestions.appendChild(link); | |||
pvzLoadSearchThumbnail(title).then(function (source) { | |||
var image; | |||
if (!source || !thumb.isConnected) return; | |||
image = document.createElement('img'); | |||
image.src = source; | |||
image.alt = ''; | |||
image.loading = 'lazy'; | |||
image.decoding = 'async'; | |||
thumb.appendChild(image); | |||
thumb.classList.add('has-image'); | |||
}); | |||
}); | |||
elements.suggestions.hidden = false; | |||
elements.suggestions.classList.add('is-open'); | |||
elements.input.setAttribute('aria-expanded', 'true'); | |||
if (document.body) { | |||
document.body.classList.add('pvz-custom-search-suggestions-open'); | |||
} | |||
} | |||
function pvzRequestCustomSearchSuggestions(query) { | |||
var requestId; | |||
query = pvzNormalizeText(query); | |||
if (query.length < 1) { | |||
pvzClearCustomSearchSuggestions(); | |||
return; | |||
} | |||
if (!window.mw || !mw.loader) { | |||
pvzClearCustomSearchSuggestions(); | |||
return; | |||
} | |||
requestId = ++pvzCustomSearchRequestId; | |||
mw.loader.using('mediawiki.api').then(function () { | |||
var api = new mw.Api(); | |||
return api.get({ | |||
action: 'opensearch', | |||
search: query, | |||
limit: 7, | |||
namespace: 0, | |||
redirects: 'resolve', | |||
format: 'json' | |||
}); | |||
}).then(function (data) { | |||
pvzRenderCustomSearchSuggestions(query, data, requestId); | |||
}).catch(function () { | |||
if (requestId === pvzCustomSearchRequestId) { | |||
pvzClearCustomSearchSuggestions(); | |||
} | |||
}); | |||
} | |||
function pvzQueueCustomSearchSuggestions() { | |||
var elements = pvzCustomSearchElements(); | |||
window.clearTimeout(pvzCustomSearchInputTimer); | |||
if (!elements.input || pvzCustomSearchComposing) return; | |||
pvzCustomSearchInputTimer = window.setTimeout(function () { | |||
pvzRequestCustomSearchSuggestions(elements.input.value); | |||
}, 170); | |||
} | |||
function pvzFindCustomSearchMainContent() { | |||
return ( | |||
document.querySelector('body.skin-timeless #mw-content') || | |||
document.querySelector('body.skin-timeless #content') || | |||
document.querySelector('body.skin-timeless .mw-body') | |||
); | |||
} | |||
function pvzSyncCustomSearchWidth() { | |||
var elements = pvzCustomSearchElements(); | |||
var header = document.getElementById('mw-header-container'); | |||
var content; | |||
var headerRect; | |||
var contentRect; | |||
var layoutViewportWidth = Math.round( | |||
document.documentElement.clientWidth || | |||
window.innerWidth || | |||
0 | |||
); | |||
if (!elements.root || !header) return; | |||
elements.root.classList.remove('is-main-width'); | |||
elements.root.style.removeProperty('--pvz-custom-search-left'); | |||
elements.root.style.removeProperty('--pvz-custom-search-width'); | |||
if (layoutViewportWidth < 1280) return; | |||
content = pvzFindCustomSearchMainContent(); | |||
if (!content || typeof content.getBoundingClientRect !== 'function') return; | |||
headerRect = header.getBoundingClientRect(); | |||
contentRect = content.getBoundingClientRect(); | |||
if (contentRect.width <= 0) return; | |||
elements.root.style.setProperty( | |||
'--pvz-custom-search-left', | |||
Math.round(contentRect.left - headerRect.left) + 'px' | |||
); | |||
elements.root.style.setProperty( | |||
'--pvz-custom-search-width', | |||
Math.round(contentRect.width) + 'px' | |||
); | |||
elements.root.classList.add('is-main-width'); | |||
} | |||
function pvzQueueCustomSearchWidthSync() { | |||
window.cancelAnimationFrame(pvzCustomSearchWidthFrame); | |||
pvzCustomSearchWidthFrame = window.requestAnimationFrame(function () { | |||
pvzSyncCustomSearchWidth(); | |||
}); | |||
} | |||
function pvzBindCustomSearchViewportSync() { | |||
var content; | |||
if (pvzCustomSearchViewportBound) { | |||
pvzQueueCustomSearchWidthSync(); | |||
return; | |||
} | |||
pvzCustomSearchViewportBound = true; | |||
window.addEventListener('resize', pvzQueueCustomSearchWidthSync, { | |||
passive: true | |||
}); | |||
if ( | |||
window.screen && | |||
window.screen.orientation && | |||
typeof window.screen.orientation.addEventListener === 'function' | |||
) { | |||
window.screen.orientation.addEventListener( | |||
'change', | |||
pvzQueueCustomSearchWidthSync | |||
); | |||
} | |||
if ( | |||
window.visualViewport && | |||
typeof window.visualViewport.addEventListener === 'function' | |||
) { | |||
window.visualViewport.addEventListener( | |||
'resize', | |||
pvzQueueCustomSearchWidthSync, | |||
{ passive: true } | |||
); | |||
} | |||
content = pvzFindCustomSearchMainContent(); | |||
if (window.ResizeObserver && content) { | |||
pvzCustomSearchResizeObserver = new ResizeObserver(function () { | |||
pvzQueueCustomSearchWidthSync(); | |||
}); | |||
pvzCustomSearchResizeObserver.observe(content); | |||
} | |||
} | |||
function pvzBindCustomSearchEvents() { | |||
var elements = pvzCustomSearchElements(); | |||
if ( | |||
pvzCustomSearchEventsBound || | |||
!elements.form || | |||
!elements.input || | |||
!elements.suggestions | |||
) { | |||
return; | |||
} | |||
pvzCustomSearchEventsBound = true; | |||
elements.input.addEventListener('compositionstart', function () { | |||
pvzCustomSearchComposing = true; | |||
}); | |||
elements.input.addEventListener('compositionend', function () { | |||
pvzCustomSearchComposing = false; | |||
pvzQueueCustomSearchSuggestions(); | |||
}); | |||
elements.input.addEventListener('input', function () { | |||
if (!pvzCustomSearchComposing) { | |||
pvzQueueCustomSearchSuggestions(); | |||
} | |||
}); | |||
elements.input.addEventListener('focus', function () { | |||
if (pvzNormalizeText(elements.input.value)) { | |||
pvzQueueCustomSearchSuggestions(); | |||
} | |||
}); | |||
elements.input.addEventListener('keydown', function (event) { | |||
var links = pvzCustomSearchSuggestionLinks(); | |||
if (event.key === 'ArrowDown' && links.length) { | |||
event.preventDefault(); | |||
pvzSetCustomSearchActiveIndex(pvzCustomSearchActiveIndex + 1); | |||
return; | |||
} | |||
if (event.key === 'ArrowUp' && links.length) { | |||
event.preventDefault(); | |||
pvzSetCustomSearchActiveIndex(pvzCustomSearchActiveIndex - 1); | |||
return; | |||
} | |||
if ( | |||
event.key === 'Enter' && | |||
pvzCustomSearchActiveIndex >= 0 && | |||
links[pvzCustomSearchActiveIndex] | |||
) { | |||
event.preventDefault(); | |||
window.location.href = links[pvzCustomSearchActiveIndex].href; | |||
return; | |||
} | |||
if (event.key === 'Escape') { | |||
pvzClearCustomSearchSuggestions(); | |||
elements.input.blur(); | |||
} | |||
}); | |||
elements.form.addEventListener('submit', function (event) { | |||
if (!pvzNormalizeText(elements.input.value)) { | |||
event.preventDefault(); | |||
elements.input.focus(); | |||
} | |||
}); | |||
elements.suggestions.addEventListener('pointerdown', function (event) { | |||
var link = event.target && event.target.closest ? | |||
event.target.closest('.pvz-custom-search-suggestion') : | |||
null; | |||
if (!link) return; | |||
link.classList.add('is-pressed'); | |||
window.setTimeout(function () { | |||
if (link.classList) link.classList.remove('is-pressed'); | |||
}, 140); | |||
}); | |||
document.addEventListener('pointerdown', function (event) { | |||
var root = document.getElementById('pvz-custom-searchbox'); | |||
if (!root || root.contains(event.target)) return; | |||
pvzClearCustomSearchSuggestions(); | |||
}, { passive: true }); | |||
} | |||
function pvzBuildCustomSearch() { | |||
var header = document.getElementById('mw-header-container'); | |||
var existing = document.getElementById('pvz-custom-searchbox'); | |||
var root; | |||
var form; | |||
var field; | |||
var input; | |||
var button; | |||
var suggestions; | |||
if (!header) return null; | |||
if (existing) { | |||
pvzSyncCustomSearchWidth(); | |||
return existing; | |||
} | |||
root = document.createElement('div'); | |||
root.id = 'pvz-custom-searchbox'; | |||
root.className = 'pvz-custom-searchbox'; | |||
root.setAttribute('role', 'search'); | |||
form = document.createElement('form'); | |||
form.id = 'pvz-custom-searchbox-form'; | |||
form.className = 'pvz-custom-searchbox-form'; | |||
form.method = 'get'; | |||
form.action = pvzMakeSpecialUrl('Search'); | |||
field = document.createElement('div'); | |||
field.className = 'pvz-custom-searchbox-field'; | |||
input = document.createElement('input'); | |||
input.id = 'pvz-custom-searchbox-input'; | |||
input.className = 'pvz-custom-searchbox-input'; | |||
input.type = 'search'; | |||
input.name = 'search'; | |||
input.placeholder = '위키에서 검색하세요'; | |||
input.autocomplete = 'off'; | |||
input.spellcheck = false; | |||
input.setAttribute('autocapitalize', 'off'); | |||
input.setAttribute('enterkeyhint', 'search'); | |||
input.setAttribute('aria-label', '위키에서 검색하세요'); | |||
input.setAttribute('aria-autocomplete', 'list'); | |||
input.setAttribute('aria-controls', 'pvz-custom-searchbox-suggestions'); | |||
input.setAttribute('aria-expanded', 'false'); | |||
button = document.createElement('button'); | |||
button.className = 'pvz-custom-searchbox-submit'; | |||
button.type = 'submit'; | |||
button.setAttribute('aria-label', '검색'); | |||
button.appendChild(pvzCreateSearchSvgIcon()); | |||
suggestions = document.createElement('div'); | |||
suggestions.id = 'pvz-custom-searchbox-suggestions'; | |||
suggestions.className = 'pvz-custom-searchbox-suggestions'; | |||
suggestions.setAttribute('role', 'listbox'); | |||
suggestions.setAttribute('aria-label', '검색 제안'); | |||
suggestions.hidden = true; | |||
field.appendChild(input); | |||
field.appendChild(button); | |||
form.appendChild(field); | |||
root.appendChild(form); | |||
root.appendChild(suggestions); | |||
header.appendChild(root); | |||
if (document.body) { | |||
document.body.classList.add('pvz-custom-search-ready'); | |||
} | |||
pvzBindCustomSearchEvents(); | |||
pvzBindCustomSearchViewportSync(); | |||
pvzQueueCustomSearchWidthSync(); | |||
window.setTimeout(pvzQueueCustomSearchWidthSync, 120); | |||
window.setTimeout(pvzQueueCustomSearchWidthSync, 420); | |||
return root; | |||
} | |||
/* 커스텀 검색바 끝 */ | |||
function pvzCreateInlineSvgIcon(kind) { | |||
var span = document.createElement('span'); | |||
var svgNS = 'http://www.w3.org/2000/svg'; | |||
var svg = document.createElementNS(svgNS, 'svg'); | |||
var path = document.createElementNS(svgNS, 'path'); | |||
var paths = { | |||
menu: 'M3 6h18v2.4H3V6Zm0 5h18v2.4H3V11Zm0 5h18v2.4H3V16Z', | |||
tools: 'M21.2 18.6 13.9 11.3c.7-1.9.3-4.1-1.2-5.6-1.6-1.6-4-2-6-1.1l3.5 3.5-2.1 2.1-3.6-3.5c-.8 2-.4 4.4 1.2 6 1.5 1.5 3.7 1.9 5.6 1.2l7.3 7.3c.3.3.8.3 1.1 0l1.5-1.5c.3-.3.3-.8 0-1.1Z', | |||
document: 'M6 2h9l5 5v15H6V2Zm8 1.8V8h4.2L14 3.8ZM8.5 12h7v1.8h-7V12Zm0 4h7v1.8h-7V16Z', | |||
view: 'M6 2h9l5 5v15H6V2Zm8 1.8V8h4.2L14 3.8ZM8.5 12h7v1.8h-7V12Zm0 4h7v1.8h-7V16Z', | |||
talk: 'M4 4h16v12H9.2L4 20V4Zm3 3v2h10V7H7Zm0 4v2h7v-2H7Z', | |||
edit: 'M4 17.2V21h3.8L18.9 9.9l-3.8-3.8L4 17.2Zm16.2-9.6-1.8 1.8-3.8-3.8 1.8-1.8a1.2 1.2 0 0 1 1.7 0l2.1 2.1a1.2 1.2 0 0 1 0 1.7Z', | |||
history: 'M12 4a8 8 0 1 1-7.4 5H2l4-4 4 4H6.8A5.9 5.9 0 1 0 12 6V4Zm1 4v4l3 2-1.1 1.7-3.4-2.2A1 1 0 0 1 11 12V8h2Z', | |||
watch: 'm12 2 2.9 6.2 6.6 1-4.8 4.7 1.2 6.7-5.9-3.2-5.9 3.2 1.2-6.7-4.8-4.8 6.6-.9L12 2Z', | |||
more: 'M6 10a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z', | |||
alerts: 'M12 2a5 5 0 0 0-5 5v2.1c0 1.1-.4 2.2-1.1 3L4.3 14a1 1 0 0 0 .7 1.7h14a1 1 0 0 0 .7-1.7l-1.6-1.9A4.8 4.8 0 0 1 17 9.1V7a5 5 0 0 0-5-5Zm0 20a3 3 0 0 0 2.8-2H9.2A3 3 0 0 0 12 22Z', | |||
account: 'M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-5.4 0-9 2.7-9 6v1h18v-1c0-3.3-3.6-6-9-6Z', | |||
sun: 'M12 4V1h2v3h-2Zm0 19v-3h2v3h-2ZM4.2 5.6 2.1 3.5 3.5 2.1l2.1 2.1-1.4 1.4Zm16.3 16.3-2.1-2.1 1.4-1.4 2.1 2.1-1.4 1.4ZM1 13v-2h3v2H1Zm19 0v-2h3v2h-3ZM3.5 21.9l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1ZM19.8 5.6l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1ZM13 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z', | |||
moon: 'M21 14.8A8.5 8.5 0 0 1 9.2 3a7 7 0 1 0 8.8 8.8A8.4 8.4 0 0 1 21 14.8Z' | |||
}; | |||
span.className = 'pvz-timeless-action-icon pvz-timeless-action-icon-' + kind; | |||
span.setAttribute('aria-hidden', 'true'); | |||
svg.setAttribute('viewBox', '0 0 24 24'); | |||
svg.setAttribute('focusable', 'false'); | |||
path.setAttribute('d', paths[kind] || paths.menu); | |||
svg.appendChild(path); | |||
span.appendChild(svg); | |||
return span; | |||
} | |||
function pvzClampNumber(value, min, max) { | |||
if (max < min) return min; | |||
return Math.max(min, Math.min(value, max)); | |||
} | |||
function pvzFindTimelessPanelButton(name) { | |||
var selector = '.pvz-timeless-action-button[data-pvz-timeless-panel="' + name + '"], ' + | |||
'.pvz-timeless-page-action[data-pvz-timeless-panel="' + name + '"]'; | |||
var buttons = pvzArray(document.querySelectorAll(selector)); | |||
var i; | |||
var rect; | |||
for (i = 0; i < buttons.length; i += 1) { | |||
if (!buttons[i] || typeof buttons[i].getBoundingClientRect !== 'function') continue; | |||
rect = buttons[i].getBoundingClientRect(); | |||
if (rect.width > 0 && rect.height > 0) return buttons[i]; | |||
} | |||
return buttons[0] || null; | |||
} | |||
function pvzGetTimelessViewport() { | |||
var vv = window.visualViewport; | |||
var width = window.innerWidth || document.documentElement.clientWidth || 0; | |||
var height = window.innerHeight || document.documentElement.clientHeight || 0; | |||
if (!vv || typeof vv.width !== 'number' || typeof vv.height !== 'number') { | |||
return { | |||
left: 0, | |||
top: 0, | |||
width: width, | |||
height: height, | |||
centerX: width / 2 | |||
}; | |||
} | |||
return { | |||
left: typeof vv.offsetLeft === 'number' ? vv.offsetLeft : 0, | |||
top: typeof vv.offsetTop === 'number' ? vv.offsetTop : 0, | |||
width: vv.width || width, | |||
height: vv.height || height, | |||
centerX: (typeof vv.offsetLeft === 'number' ? vv.offsetLeft : 0) + ((vv.width || width) / 2) | |||
}; | |||
} | |||
var pvzTimelessViewportWidth = 0; | |||
var pvzTimelessViewportHeight = 0; | |||
var pvzTimelessPanelRefreshTimers = []; | |||
var pvzTimelessPanelCloseTimers = new WeakMap(); | |||
function pvzResetTimelessPanelGeometry(panel) { | |||
if (!panel || panel.nodeType !== 1) return; | |||
panel.removeAttribute('data-pvz-panel-fixed-at-open'); | |||
panel.removeAttribute('data-pvz-panel-document-locked'); | |||
panel.removeAttribute('data-pvz-panel-document-top'); | |||
panel.removeAttribute('data-pvz-panel-document-left'); | |||
panel.removeAttribute('data-pvz-panel-document-width'); | |||
[ | |||
'position', | |||
'top', | |||
'left', | |||
'right', | |||
'width', | |||
'max-width', | |||
'max-height', | |||
'overflow', | |||
'transform', | |||
'transform-origin', | |||
'z-index' | |||
].forEach(function (property) { | |||
panel.style.removeProperty(property); | |||
}); | |||
} | |||
function pvzClearTimelessPanelCloseTimer(panel) { | |||
var timer; | |||
if (!panel) return; | |||
timer = pvzTimelessPanelCloseTimers.get(panel); | |||
if (!timer) return; | |||
window.clearTimeout(timer); | |||
pvzTimelessPanelCloseTimers.delete(panel); | |||
} | |||
function pvzScheduleTimelessPanelGeometryCleanup(panel) { | |||
var timer; | |||
if (!panel) return; | |||
pvzClearTimelessPanelCloseTimer(panel); | |||
timer = window.setTimeout(function () { | |||
pvzTimelessPanelCloseTimers.delete(panel); | |||
if (!panel.classList.contains('is-open')) { | |||
pvzResetTimelessPanelGeometry(panel); | |||
} | |||
}, 240); | |||
pvzTimelessPanelCloseTimers.set(panel, timer); | |||
} | |||
function pvzPositionTimelessPanel(name) { | |||
var panel = document.getElementById('pvz-timeless-panel-' + name); | |||
var button = pvzFindTimelessPanelButton(name); | |||
var viewport; | |||
var buttonRect; | |||
var panelRect; | |||
var panelWidth; | |||
var center; | |||
var minLeft; | |||
var maxLeft; | |||
var left; | |||
var top; | |||
var scrollX; | |||
var scrollY; | |||
if (!panel || !button || typeof button.getBoundingClientRect !== 'function') return; | |||
viewport = pvzGetTimelessViewport(); | |||
buttonRect = button.getBoundingClientRect(); | |||
panelRect = panel.getBoundingClientRect(); | |||
panelWidth = panelRect.width || 236; | |||
panelWidth = Math.min(panelWidth, Math.max(0, viewport.width - 16)); | |||
center = buttonRect.left + (buttonRect.width / 2); | |||
minLeft = viewport.left + 8; | |||
maxLeft = viewport.left + viewport.width - panelWidth - 8; | |||
left = pvzClampNumber(center - (panelWidth / 2), minLeft, maxLeft); | |||
top = viewport.top + buttonRect.bottom + 6; | |||
scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0; | |||
scrollY = window.pageYOffset || document.documentElement.scrollTop || 0; | |||
panel.style.setProperty('position', 'absolute', 'important'); | |||
panel.style.setProperty('top', Math.round(top + scrollY) + 'px', 'important'); | |||
panel.style.setProperty('left', Math.round(left + scrollX) + 'px', 'important'); | |||
panel.style.setProperty('right', 'auto', 'important'); | |||
panel.style.setProperty('max-width', 'calc(100vw - 12px)', 'important'); | |||
panel.style.setProperty('max-height', 'none', 'important'); | |||
panel.style.setProperty('overflow', 'visible', 'important'); | |||
panel.style.setProperty('transform', 'none', 'important'); | |||
} | |||
function pvzRepositionOpenTimelessPanel() { | |||
var open = document.querySelector('.pvz-timeless-sheet.is-open'); | |||
var name; | |||
if (!open || !open.id) return; | |||
name = open.id.replace(/^pvz-timeless-panel-/, ''); | |||
if (name) pvzPositionTimelessPanel(name); | |||
} | |||
function pvzQueueTimelessPanelViewportRefresh() { | |||
pvzTimelessPanelRefreshTimers.forEach(function (timer) { | |||
window.clearTimeout(timer); | |||
}); | |||
pvzTimelessPanelRefreshTimers = []; | |||
window.requestAnimationFrame(function () { | |||
pvzRepositionOpenTimelessPanel(); | |||
}); | |||
[140, 420].forEach(function (delay) { | |||
pvzTimelessPanelRefreshTimers.push( | |||
window.setTimeout(function () { | |||
pvzRepositionOpenTimelessPanel(); | |||
}, delay) | |||
); | |||
}); | |||
} | |||
function pvzHandleTimelessViewportResize() { | |||
var viewport = pvzGetTimelessViewport(); | |||
var width = Math.round(viewport.width || window.innerWidth || 0); | |||
var height = Math.round(viewport.height || window.innerHeight || 0); | |||
var widthChanged; | |||
var heightChanged; | |||
var orientationChanged; | |||
if (!pvzTimelessViewportWidth || !pvzTimelessViewportHeight) { | |||
pvzTimelessViewportWidth = width; | |||
pvzTimelessViewportHeight = height; | |||
return; | |||
} | |||
widthChanged = Math.abs(width - pvzTimelessViewportWidth); | |||
heightChanged = Math.abs(height - pvzTimelessViewportHeight); | |||
orientationChanged = | |||
(width > height) !== (pvzTimelessViewportWidth > pvzTimelessViewportHeight); | |||
pvzTimelessViewportWidth = width; | |||
pvzTimelessViewportHeight = height; | |||
if (orientationChanged || widthChanged >= 80 || heightChanged >= 120) { | |||
pvzQueueTimelessPanelViewportRefresh(); | |||
} | |||
} | |||
function pvzCloseTimelessPanels() { | |||
document.documentElement.classList.remove('pvz-timeless-panel-open'); | |||
if (document.body) document.body.classList.remove('pvz-timeless-panel-open'); | |||
pvzArray(document.querySelectorAll('.pvz-timeless-sheet')).forEach(function (panel) { | |||
panel.classList.remove('is-open'); | |||
pvzScheduleTimelessPanelGeometryCleanup(panel); | |||
}); | |||
pvzArray(document.querySelectorAll('.pvz-timeless-action-button, .pvz-timeless-page-action[data-pvz-timeless-panel]')).forEach(function (button) { | |||
button.classList.remove('is-active'); | |||
button.setAttribute('aria-expanded', 'false'); | |||
}); | |||
} | |||
function pvzOpenTimelessPanel(name) { | |||
var panel = document.getElementById('pvz-timeless-panel-' + name); | |||
if (!panel) return; | |||
if (name === 'alerts') pvzRefreshTimelessAlertsPanel(); | |||
pvzCloseTimelessPanels(); | |||
pvzClearTimelessPanelCloseTimer(panel); | |||
pvzResetTimelessPanelGeometry(panel); | |||
pvzPositionTimelessPanel(name); | |||
document.documentElement.classList.add('pvz-timeless-panel-open'); | |||
if (document.body) document.body.classList.add('pvz-timeless-panel-open'); | |||
panel.classList.add('is-open'); | |||
pvzArray(document.querySelectorAll('.pvz-timeless-action-button, .pvz-timeless-page-action[data-pvz-timeless-panel]')).forEach(function (button) { | |||
var active = button.getAttribute('data-pvz-timeless-panel') === name; | |||
button.classList.toggle('is-active', active); | |||
button.setAttribute('aria-expanded', active ? 'true' : 'false'); | |||
}); | |||
} | |||
function pvzToggleTimelessPanel(name) { | |||
var panel = document.getElementById('pvz-timeless-panel-' + name); | |||
if (panel && panel.classList.contains('is-open')) { | |||
pvzCloseTimelessPanels(); | |||
return; | |||
} | |||
pvzOpenTimelessPanel(name); | |||
} | |||
function pvzCreateTimelessActionButton(name, label, iconKind) { | |||
var button = document.createElement('button'); | |||
button.type = 'button'; | |||
button.className = 'pvz-timeless-action-button pvz-timeless-action-' + name; | |||
button.setAttribute('data-pvz-timeless-panel', name); | |||
button.setAttribute('aria-label', label); | |||
button.setAttribute('aria-expanded', 'false'); | |||
button.title = label; | |||
button.appendChild(pvzCreateInlineSvgIcon(iconKind)); | |||
return button; | |||
} | |||
function pvzGetTalkPageUrl() { | |||
var pageName = pvzPageName(); | |||
var title = ''; | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
title = mw.config.get('wgTitle') || pageName; | |||
} | |||
if (!pageName) return pvzMakePageUrl('Talk:' + title); | |||
if (/^토론:|^Talk:/i.test(pageName)) return pvzMakePageUrl(pageName); | |||
if (pageName.indexOf(':') === -1) return pvzMakePageUrl('토론:' + title); | |||
return pvzMakePageUrl('토론:' + title); | |||
} | |||
function pvzIsCurrentPageAction(kind) { | |||
var action = 'view'; | |||
var ns = null; | |||
if (window.mw && mw.config && typeof mw.config.get === 'function') { | |||
action = mw.config.get('wgAction') || 'view'; | |||
ns = mw.config.get('wgNamespaceNumber'); | |||
} | |||
if (kind === 'view') { | |||
return action === 'view' && !(typeof ns === 'number' && ns % 2 === 1); | |||
} | |||
if (kind === 'talk') { | |||
return typeof ns === 'number' && ns % 2 === 1; | |||
} | |||
if (kind === 'edit') { | |||
return action === 'edit' || action === 'submit' || action === 'visualeditor'; | |||
} | |||
if (kind === 'history') { | |||
return action === 'history'; | |||
} | |||
return false; | |||
} | |||
function pvzCreatePageActionElement(kind, label, href, extraClass) { | |||
var el; | |||
if (href) { | |||
el = document.createElement('a'); | |||
el.href = href; | |||
} else { | |||
el = document.createElement('button'); | |||
el.type = 'button'; | |||
} | |||
el.className = 'pvz-timeless-page-action pvz-timeless-page-action-' + (extraClass || kind); | |||
if (pvzIsCurrentPageAction(kind)) { | |||
el.classList.add('is-current'); | |||
el.setAttribute('aria-current', 'page'); | |||
} | |||
el.setAttribute('aria-label', label); | |||
el.title = label; | |||
el.appendChild(pvzCreateInlineSvgIcon(kind)); | |||
el.querySelector('.pvz-timeless-action-icon').classList.add('pvz-timeless-page-action-icon'); | |||
return el; | |||
} | |||
function pvzCreatePagePanelButton(kind, label, panelName, extraClass) { | |||
var button = pvzCreatePageActionElement(kind, label, '', extraClass); | |||
button.setAttribute('data-pvz-timeless-panel', panelName); | |||
button.setAttribute('aria-expanded', 'false'); | |||
return button; | |||
} | |||
function pvzPageActionUrl(type) { | |||
var found; | |||
var pageName = pvzPageName(); | |||
if (type === 'view') { | |||
found = pvzFindExistingLink({ | |||
ids: [/ca-view/i, /ca-nstab-main/i, /ca-nstab-project/i], | |||
text: [/^문서$/, /^Read$/i, /^Page$/i] | |||
}); | |||
return found ? found.getAttribute('href') : pvzMakePageUrl(pageName); | |||
} | |||
if (type === 'talk') { | |||
found = pvzFindExistingLink({ | |||
ids: [/ca-talk/i, /ca-talkpage/i], | |||
text: [/^토론$/, /^Discussion$/i, /^Talk$/i] | |||
}); | |||
return found ? found.getAttribute('href') : pvzGetTalkPageUrl(); | |||
} | |||
if (type === 'edit') { | |||
found = pvzFindExistingLink({ | |||
ids: [/ca-edit/i, /ca-ve-edit/i, /ca-viewsource/i], | |||
href: [/action=(edit|submit)/i, /veaction=edit/i], | |||
text: [/^편집$/, /^원본 편집$/, /^Edit$/i, /^View source$/i] | |||
}); | |||
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=edit' : ''); | |||
} | |||
if (type === 'history') { | |||
found = pvzFindExistingLink({ | |||
ids: [/ca-history/i], | |||
href: [/action=history/i], | |||
text: [/^역사$/, /^문서 역사$/, /^View history$/i, /^History$/i] | |||
}); | |||
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=history' : ''); | |||
} | |||
if (type === 'watch') { | |||
found = pvzFindExistingLink({ | |||
ids: [/ca-watch/i, /ca-unwatch/i, /ca-watchlist/i], | |||
href: [/action=(watch|unwatch)/i, /Special:(Watchlist|주시문서)/i, /특수:(주시문서|Watchlist)/i], | |||
text: [/주시문서/, /즐겨찾기/, /^Watch$/i, /^Unwatch$/i] | |||
}); | |||
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=watch' : pvzMakeSpecialUrl('Watchlist')); | |||
} | |||
return ''; | |||
} | |||
function pvzEnsureTimelessPageActions() { | |||
var old = document.getElementById('pvz-timeless-page-actions'); | |||
var heading = document.getElementById('firstHeading') || | |||
document.querySelector('.mw-first-heading, .firstHeading'); | |||
var wrap = document.createElement('nav'); | |||
var left = document.createElement('div'); | |||
var right = document.createElement('div'); | |||
var watchUrl = pvzPageActionUrl('watch'); | |||
if (!heading || !heading.parentNode) return; | |||
if (old && old.parentNode) old.parentNode.removeChild(old); | |||
wrap.id = 'pvz-timeless-page-actions'; | |||
wrap.className = 'pvz-timeless-page-actions'; | |||
wrap.setAttribute('aria-label', '문서 작업'); | |||
left.className = 'pvz-timeless-page-actions-left'; | |||
right.className = 'pvz-timeless-page-actions-right'; | |||
left.appendChild(pvzCreatePageActionElement('view', '문서', pvzPageActionUrl('view'), 'view')); | |||
left.appendChild(pvzCreatePageActionElement('talk', '토론', pvzPageActionUrl('talk'), 'talk')); | |||
right.appendChild(pvzCreatePageActionElement('edit', '편집', pvzPageActionUrl('edit'), 'edit')); | |||
right.appendChild(pvzCreatePageActionElement('history', '역사', pvzPageActionUrl('history'), 'history')); | |||
if (watchUrl) { | |||
right.appendChild(pvzCreatePageActionElement('watch', '즐겨찾기', watchUrl, 'watch')); | |||
} | |||
right.appendChild(pvzCreatePagePanelButton('more', '더보기', 'more', 'more')); | |||
wrap.appendChild(left); | |||
wrap.appendChild(right); | |||
if (heading.nextSibling) { | |||
heading.parentNode.insertBefore(wrap, heading.nextSibling); | |||
} else { | |||
heading.parentNode.appendChild(wrap); | |||
} | |||
} | |||
function pvzEnsureTimelessActionbar() { | |||
var bar = document.getElementById('pvz-timeless-mobile-actions'); | |||
var host = document.querySelector('body.skin-timeless #mw-header-container') || | |||
document.getElementById('mw-header-container') || | |||
document.querySelector('body.skin-timeless #mw-header') || | |||
document.body; | |||
if (!host) return bar; | |||
if (!bar) { | |||
bar = document.createElement('div'); | |||
bar.id = 'pvz-timeless-mobile-actions'; | |||
bar.className = 'pvz-timeless-mobile-actions'; | |||
bar.setAttribute('aria-label', '식좀위키 모바일 빠른 메뉴'); | |||
} | |||
bar.innerHTML = ''; | |||
bar.appendChild(pvzCreateTimelessActionButton('browse', '둘러보기', 'menu')); | |||
bar.appendChild(pvzCreateTimelessActionButton('settings', '위키 도구', 'tools')); | |||
bar.appendChild(pvzCreateTimelessActionButton('document', '문서 도구', 'document')); | |||
bar.appendChild(pvzCreateTimelessActionButton('alerts', '알림', 'alerts')); | |||
bar.appendChild(pvzCreateTimelessActionButton('account', '계정', 'account')); | |||
if (bar.parentNode !== host) { | |||
host.appendChild(bar); | |||
} | |||
var title = document.getElementById('pvz-timeless-mobile-title'); | |||
if (!title) { | |||
title = document.createElement('a'); | |||
title.id = 'pvz-timeless-mobile-title'; | |||
title.href = pvzMakePageUrl(pvzMainPageName()); | |||
title.textContent = '식물 vs 좀비 위키'; | |||
title.setAttribute('aria-label', '식물 vs 좀비 위키 대문으로 이동'); | |||
} | |||
if (title.parentNode !== host) { | |||
host.appendChild(title); | |||
} | |||
return bar; | |||
} | |||
function pvzCreateTimelessPanel(name, label, sections) { | |||
var old = document.getElementById('pvz-timeless-panel-' + name); | |||
var panel = document.createElement('section'); | |||
var body = document.createElement('div'); | |||
if (old && old.parentNode) old.parentNode.removeChild(old); | |||
panel.id = 'pvz-timeless-panel-' + name; | |||
panel.className = 'pvz-timeless-sheet pvz-timeless-sheet-' + name; | |||
panel.setAttribute('aria-label', label); | |||
body.className = 'pvz-timeless-sheet-body'; | |||
(sections || []).filter(Boolean).forEach(function (section) { | |||
body.appendChild(section); | |||
}); | |||
panel.appendChild(body); | |||
document.body.appendChild(panel); | |||
} | |||
function pvzEnsureTimelessPanelBackdrop() { | |||
var old = document.getElementById('pvz-timeless-panel-backdrop'); | |||
if (old) return old; | |||
old = document.createElement('div'); | |||
old.id = 'pvz-timeless-panel-backdrop'; | |||
old.className = 'pvz-timeless-panel-backdrop'; | |||
document.body.appendChild(old); | |||
return old; | |||
} | |||
function pvzStoredColorMode() { | |||
try { | |||
return localStorage.getItem('pvz-timeless-color-mode') || ''; | |||
} catch (e) { | |||
return ''; | |||
} | |||
} | |||
function pvzCurrentColorMode() { | |||
var stored = pvzStoredColorMode(); | |||
var root = document.documentElement; | |||
if (stored === 'light' || stored === 'dark') return stored; | |||
if (root && root.classList && ( | |||
root.classList.contains('theme-dark') || | |||
root.classList.contains('dark') || | |||
root.classList.contains('client-dark-mode') || | |||
root.classList.contains('mw-theme-night') || | |||
root.classList.contains('skin-theme-clientpref-night') | |||
)) { | |||
return 'dark'; | |||
} | |||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |||
return 'dark'; | |||
} | |||
return 'light'; | |||
} | |||
function pvzApplyTimelessColorMode(mode, persist) { | |||
var root = document.documentElement; | |||
var body = document.body; | |||
var add; | |||
var remove; | |||
if (mode !== 'light' && mode !== 'dark') mode = 'light'; | |||
if (typeof persist === 'undefined') persist = true; | |||
if (persist) { | |||
try { | |||
localStorage.setItem('pvz-timeless-color-mode', mode); | |||
} catch (e) {} | |||
} | |||
add = mode === 'dark' | |||
? ['theme-dark', 'dark', 'client-dark-mode', 'mw-theme-night', 'skin-theme-clientpref-night'] | |||
: ['theme-light', 'light', 'client-light-mode', 'skin-theme-clientpref-day']; | |||
remove = mode === 'dark' | |||
? ['theme-light', 'light', 'client-light-mode', 'skin-theme-clientpref-day'] | |||
: ['theme-dark', 'dark', 'client-dark-mode', 'mw-theme-night', 'skin-theme-clientpref-night']; | |||
[root, body].forEach(function (node) { | |||
if (!node || !node.classList) return; | |||
remove.forEach(function (name) { node.classList.remove(name); }); | |||
add.forEach(function (name) { node.classList.add(name); }); | |||
}); | |||
} | |||
var pvzSystemThemeMedia = null; | |||
var pvzSystemThemeListenerBound = false; | |||
function pvzHasStoredColorMode() { | |||
var stored = pvzStoredColorMode(); | |||
return stored === 'light' || stored === 'dark'; | |||
} | |||
function pvzApplyInitialTimelessColorMode() { | |||
var stored = pvzStoredColorMode(); | |||
var mode = pvzCurrentColorMode(); | |||
pvzApplyTimelessColorMode(mode, stored === 'light' || stored === 'dark'); | |||
} | |||
function pvzBindSystemThemeListener() { | |||
if ( | |||
pvzSystemThemeListenerBound || | |||
!window.matchMedia | |||
) { | |||
return; | |||
} | |||
pvzSystemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); | |||
pvzSystemThemeListenerBound = true; | |||
function handleSystemThemeChange(event) { | |||
if (pvzHasStoredColorMode()) return; | |||
pvzApplyTimelessColorMode(event.matches ? 'dark' : 'light', false); | |||
window.setTimeout(function () { | |||
if (typeof pvzBuildCustomMobileTools === 'function') { | |||
pvzBuildCustomMobileTools(); | |||
} | |||
}, 0); | |||
} | |||
if (typeof pvzSystemThemeMedia.addEventListener === 'function') { | |||
pvzSystemThemeMedia.addEventListener('change', handleSystemThemeChange); | |||
} else if (typeof pvzSystemThemeMedia.addListener === 'function') { | |||
pvzSystemThemeMedia.addListener(handleSystemThemeChange); | |||
} | |||
} | |||
function pvzThemeToggleEntry() { | |||
var current = pvzCurrentColorMode(); | |||
if (current === 'dark') { | |||
return { | |||
label: '라이트 모드', | |||
href: '#', | |||
icon: 'sun', | |||
className: 'pvz-timeless-theme-toggle', | |||
themeMode: 'light' | |||
}; | |||
} | |||
return { | |||
label: '다크 모드', | |||
href: '#', | |||
icon: 'moon', | |||
className: 'pvz-timeless-theme-toggle', | |||
themeMode: 'dark' | |||
}; | |||
} | |||
/* ========================= | |||
계정 패널 사용자 정보 시작 | |||
- 현재 로그인 사용자의 명시적 직책, 편집 횟수, 가입일을 Action API에서 불러온다. | |||
========================= */ | |||
var pvzAccountInfoPromise = null; | |||
function pvzAccountRoleLabel(group) { | |||
var labels = { | |||
'bureaucrat': '사무관', | |||
'interface-admin': '인터페이스 관리자', | |||
'sysop': '관리자', | |||
'patroller': '순찰자', | |||
'rollbacker': '되돌리기 사용자', | |||
'autopatrolled': '자동 순찰 면제 사용자', | |||
'bot': '봇', | |||
'confirmed': '인증된 사용자', | |||
'checkuser': '검사관', | |||
'suppress': '기록보호자', | |||
'oversight': '기록보호자', | |||
'steward': '스튜어드', | |||
'translator': '번역자', | |||
'translationadmin': '번역 관리자' | |||
}; | |||
return labels[group] || group; | |||
} | |||
function pvzAccountExplicitRoles(userinfo) { | |||
var memberships = userinfo && userinfo.groupmemberships; | |||
var groups = userinfo && userinfo.groups; | |||
var names = []; | |||
if (Array.isArray(memberships) && memberships.length) { | |||
memberships.forEach(function (membership) { | |||
if (membership && membership.group) names.push(membership.group); | |||
}); | |||
} else if (Array.isArray(groups)) { | |||
groups.forEach(function (group) { | |||
if (group && group !== '*' && group !== 'user' && group !== 'autoconfirmed') { | |||
names.push(group); | |||
} | |||
}); | |||
} | |||
names = names.filter(function (group, index, list) { | |||
return group && list.indexOf(group) === index; | |||
}); | |||
if (!names.length) return '일반 사용자'; | |||
return names.map(pvzAccountRoleLabel).join(' · '); | |||
} | |||
function pvzFormatAccountRegistrationDate(value) { | |||
var date; | |||
var formatter; | |||
var parts; | |||
var data = {}; | |||
if (!value) return '확인할 수 없음'; | |||
date = new Date(value); | |||
if (isNaN(date.getTime())) return '확인할 수 없음'; | |||
try { | |||
formatter = new Intl.DateTimeFormat('ko-KR', { | |||
timeZone: 'Asia/Seoul', | |||
year: 'numeric', | |||
month: 'numeric', | |||
day: 'numeric', | |||
weekday: 'short' | |||
}); | |||
parts = formatter.formatToParts(date); | |||
parts.forEach(function (part) { | |||
if (part.type !== 'literal') data[part.type] = part.value; | |||
}); | |||
return ( | |||
data.year + '년 ' + | |||
data.month + '월 ' + | |||
data.day + '일 (' + | |||
data.weekday + ')' | |||
); | |||
} catch (e) { | |||
return date.getFullYear() + '년 ' + | |||
(date.getMonth() + 1) + '월 ' + | |||
date.getDate() + '일'; | |||
} | |||
} | |||
function pvzLoadAccountInfo() { | |||
if (pvzAccountInfoPromise) return pvzAccountInfoPromise; | |||
if (!window.mw || !mw.loader || !mw.config || !mw.config.get('wgUserName')) { | |||
return Promise.resolve(null); | |||
} | |||
pvzAccountInfoPromise = mw.loader.using('mediawiki.api').then(function () { | |||
var api = new mw.Api(); | |||
return api.get({ | |||
action: 'query', | |||
meta: 'userinfo', | |||
uiprop: 'groups|groupmemberships|editcount|registrationdate', | |||
formatversion: 2 | |||
}); | |||
}).then(function (data) { | |||
return data && data.query ? data.query.userinfo : null; | |||
}).catch(function () { | |||
return null; | |||
}); | |||
return pvzAccountInfoPromise; | |||
} | |||
function pvzCreateAccountInfoRow(label, value) { | |||
var row = document.createElement('div'); | |||
var key = document.createElement('span'); | |||
var text = document.createElement('span'); | |||
row.className = 'pvz-timeless-account-info-row'; | |||
key.className = 'pvz-timeless-account-info-label'; | |||
key.textContent = label; | |||
text.className = 'pvz-timeless-account-info-value'; | |||
text.textContent = value; | |||
row.appendChild(key); | |||
row.appendChild(text); | |||
return row; | |||
} | |||
function pvzCreateAccountInfoBlock(username) { | |||
var box = document.createElement('div'); | |||
box.className = 'pvz-timeless-account-info'; | |||
box.setAttribute('aria-label', username + ' 계정 정보'); | |||
box.appendChild(pvzCreateAccountInfoRow('직책', '불러오는 중…')); | |||
box.appendChild(pvzCreateAccountInfoRow('편집', '불러오는 중…')); | |||
box.appendChild(pvzCreateAccountInfoRow('가입일', '불러오는 중…')); | |||
pvzLoadAccountInfo().then(function (userinfo) { | |||
var rows; | |||
var editcount; | |||
if (!box.isConnected || !userinfo) return; | |||
rows = box.querySelectorAll('.pvz-timeless-account-info-value'); | |||
editcount = Number(userinfo.editcount); | |||
if (rows[0]) rows[0].textContent = pvzAccountExplicitRoles(userinfo); | |||
if (rows[1]) { | |||
rows[1].textContent = isNaN(editcount) ? | |||
'확인할 수 없음' : | |||
editcount.toLocaleString('ko-KR') + '회'; | |||
} | |||
if (rows[2]) rows[2].textContent = pvzFormatAccountRegistrationDate(userinfo.registrationdate); | |||
}); | |||
return box; | |||
} | |||
/* 계정 패널 사용자 정보 끝 */ | |||
function pvzAccountSection() { | |||
var username = window.mw && mw.config && typeof mw.config.get === 'function' ? mw.config.get('wgUserName') : null; | |||
var entries; | |||
var section; | |||
var list; | |||
if (username) { | |||
entries = [ | |||
{ label: username, href: pvzMakePageUrl('User:' + username) }, | |||
{ label: '사용자 토론', href: pvzMakePageUrl('User talk:' + username) }, | |||
{ label: '주시문서 목록', href: pvzMakeSpecialUrl('Watchlist') }, | |||
{ label: '기여', href: pvzMakeSpecialUrl('Contributions/' + username) }, | |||
{ label: '환경설정', href: pvzMakeSpecialUrl('Preferences') }, | |||
{ label: '로그아웃', href: pvzMakeSpecialUrl('UserLogout') }, | |||
pvzThemeToggleEntry() | |||
]; | |||
} else { | |||
entries = [ | |||
{ label: '로그인', href: pvzMakeSpecialUrl('UserLogin') }, | |||
{ label: '회원 가입', href: pvzMakeSpecialUrl('CreateAccount') }, | |||
pvzThemeToggleEntry() | |||
]; | |||
} | |||
section = pvzCreateToolsSection('계정', entries, 'account'); | |||
if (section && username) { | |||
list = section.querySelector('.pvz-custom-mobile-tools-list'); | |||
section.insertBefore(pvzCreateAccountInfoBlock(username), list || null); | |||
} | |||
return section; | |||
} | |||
function pvzNativeNotificationCount() { | |||
var nodes = pvzArray(document.querySelectorAll( | |||
'.mw-echo-notifications-badge, .mw-echo-notification-badge, ' + | |||
'#pt-notifications-alert .mw-echo-notifications-badge, ' + | |||
'#pt-notifications-notice .mw-echo-notifications-badge, ' + | |||
'#pt-notifications-alert [data-counter-num], ' + | |||
'#pt-notifications-notice [data-counter-num]' | |||
)); | |||
var count = 0; | |||
nodes.forEach(function (node) { | |||
var raw = ''; | |||
var value; | |||
if (!node) return; | |||
raw = node.getAttribute('data-counter-num') || node.getAttribute('data-counter-text') || node.getAttribute('aria-label') || node.textContent || ''; | |||
value = parseInt(String(raw).replace(/[^0-9]/g, ''), 10); | |||
if (!isNaN(value)) count += value; | |||
}); | |||
return count; | |||
} | |||
function pvzCollectNotificationEntries() { | |||
var entries = []; | |||
var seen = {}; | |||
var selectors = [ | |||
'.mw-echo-ui-notificationItemWidget a[href]', | |||
'.mw-echo-notification a[href]', | |||
'.mw-echo-notifications a[href]', | |||
'.mw-notification a[href]', | |||
'.mw-notifications a[href]', | |||
'#pt-notifications-alert a[href]', | |||
'#pt-notifications-notice a[href]' | |||
]; | |||
pvzArray(document.querySelectorAll(selectors.join(', '))).forEach(function (link) { | |||
var label = pvzNormalizeText(link.textContent || link.getAttribute('title') || link.getAttribute('aria-label') || ''); | |||
var href = link.getAttribute('href'); | |||
var key; | |||
if (!href) return; | |||
if (!label || /^(알림|공지|Notifications?|Alerts?|Notices?)$/i.test(label)) return; | |||
key = href + '|' + label; | |||
if (seen[key]) return; | |||
seen[key] = true; | |||
entries.push({ | |||
label: label, | |||
href: href, | |||
source: link | |||
}); | |||
}); | |||
return entries.slice(0, 8); | |||
} | |||
function pvzCreateAlertEmptySection() { | |||
var section = document.createElement('section'); | |||
var heading = document.createElement('h2'); | |||
var list = document.createElement('ul'); | |||
var item = document.createElement('li'); | |||
var empty = document.createElement('span'); | |||
section.className = 'pvz-custom-mobile-tools-section pvz-timeless-alert-section'; | |||
section.setAttribute('data-pvz-tools-section', 'alerts'); | |||
heading.className = 'pvz-custom-mobile-tools-heading'; | |||
heading.textContent = '알림'; | |||
list.className = 'pvz-custom-mobile-tools-list'; | |||
item.className = 'pvz-custom-mobile-tools-item'; | |||
empty.className = 'pvz-timeless-alert-empty'; | |||
empty.textContent = '새 알림이 없습니다.'; | |||
item.appendChild(empty); | |||
list.appendChild(item); | |||
section.appendChild(heading); | |||
section.appendChild(list); | |||
return section; | |||
} | |||
function pvzAlertsSection() { | |||
var entries = pvzCollectNotificationEntries(); | |||
var count = pvzNativeNotificationCount(); | |||
if (!entries.length && count < 1) { | |||
return pvzCreateAlertEmptySection(); | |||
} | |||
if (!entries.length && count > 0) { | |||
entries.push({ label: '알림 확인하기', href: pvzMakeSpecialUrl('Notifications') }); | |||
} | |||
entries.push({ label: '전체 알림', href: pvzMakeSpecialUrl('Notifications') }); | |||
entries.push({ label: '환경설정', href: pvzMakeSpecialUrl('Preferences') }); | |||
return pvzCreateToolsSection('알림', entries, 'alerts'); | |||
} | |||
function pvzRefreshTimelessAlertsPanel() { | |||
var panel = document.getElementById('pvz-timeless-panel-alerts'); | |||
var body = panel && panel.querySelector('.pvz-timeless-sheet-body'); | |||
if (!body) return; | |||
body.innerHTML = ''; | |||
body.appendChild(pvzAlertsSection()); | |||
} | |||
function pvzBuildTimelessWideRail(side, sections, includeLogo) { | |||
var rail = document.createElement('aside'); | |||
var logo; | |||
var img; | |||
rail.className = 'pvz-timeless-wide-rail pvz-timeless-wide-rail-' + side; | |||
rail.setAttribute('aria-label', side === 'left' ? '왼쪽 보조창' : '오른쪽 보조창'); | |||
if (includeLogo) { | |||
logo = document.createElement('a'); | |||
logo.className = 'pvz-timeless-wide-logo'; | |||
logo.href = pvzMakePageUrl(pvzMainPageName()); | |||
logo.title = '식물 vs 좀비 위키'; | |||
logo.setAttribute('aria-label', '식물 vs 좀비 위키로 이동'); | |||
img = document.createElement('img'); | |||
img.src = '/wiki/Special:Redirect/file/PvZKR_Wiki_Logo2.png'; | |||
img.alt = 'Plants vs. Zombies 위키'; | |||
img.loading = 'eager'; | |||
logo.appendChild(img); | |||
rail.appendChild(logo); | |||
} | |||
(sections || []).filter(Boolean).forEach(function (section) { | |||
rail.appendChild(section); | |||
}); | |||
return rail; | |||
} | |||
function pvzBuildTimelessWideTools(navEntries, wikiEntries, moreEntries, manageEntries, categoryEntries) { | |||
var leftHost = document.querySelector('body.skin-timeless #mw-site-navigation') || document.getElementById('mw-site-navigation'); | |||
var rightHost = document.querySelector('body.skin-timeless #mw-related-navigation') || document.getElementById('mw-related-navigation'); | |||
var leftRail; | |||
var rightRail; | |||
pvzArray(document.querySelectorAll('.pvz-timeless-wide-rail')).forEach(function (node) { | |||
if (node && node.parentNode) node.parentNode.removeChild(node); | |||
}); | |||
if (!leftHost && !rightHost) return; | |||
leftRail = pvzBuildTimelessWideRail('left', [ | |||
pvzCreateToolsSection('둘러보기', navEntries, 'navigation'), | |||
pvzCreateToolsSection('위키 도구', wikiEntries, 'wiki') | |||
], true); | |||
rightRail = pvzBuildTimelessWideRail('right', [ | |||
pvzCreateToolsSection('문서 도구', moreEntries, 'document'), | |||
pvzCreateToolsSection('분류', categoryEntries, 'category'), | |||
pvzCreateToolsSection('위키 관리', manageEntries, 'management') | |||
], false); | |||
if (leftHost) leftHost.appendChild(leftRail); | |||
else document.body.insertBefore(leftRail, document.body.firstChild); | |||
if (rightHost) rightHost.appendChild(rightRail); | |||
else document.body.appendChild(rightRail); | |||
} | |||
function pvzBuildMobileCategoryBox(categorySection) { | |||
var old = document.querySelector('.pvz-mobile-category-box'); | |||
var box; | |||
var content; | |||
if (old && old.parentNode) old.parentNode.removeChild(old); | |||
if (!categorySection) return; | |||
box = document.createElement('div'); | |||
box.className = 'pvz-mobile-category-box'; | |||
box.appendChild(categorySection); | |||
content = document.getElementById('mw-content-text') || document.querySelector('#bodyContent, .mw-body-content, #mw-content, #content'); | |||
if (content && content.parentNode) { | |||
if (content.nextSibling) content.parentNode.insertBefore(box, content.nextSibling); | |||
else content.parentNode.appendChild(box); | |||
} else { | |||
document.body.appendChild(box); | |||
} | |||
} | |||
function pvzBindTimelessPanelEvents() { | |||
if (window.PVZ_TIMELESS_PANEL_EVENTS_BOUND) return; | |||
window.PVZ_TIMELESS_PANEL_EVENTS_BOUND = true; | |||
document.addEventListener('click', function (event) { | |||
var themeToggle = event.target && event.target.closest ? event.target.closest('[data-pvz-timeless-theme-mode]') : null; | |||
var button = event.target && event.target.closest ? event.target.closest('.pvz-timeless-action-button[data-pvz-timeless-panel], .pvz-timeless-page-action[data-pvz-timeless-panel]') : null; | |||
var panel = event.target && event.target.closest ? event.target.closest('.pvz-timeless-sheet') : null; | |||
var backdrop = event.target && event.target.closest ? event.target.closest('#pvz-timeless-panel-backdrop') : null; | |||
if (themeToggle) { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
pvzApplyTimelessColorMode(themeToggle.getAttribute('data-pvz-timeless-theme-mode')); | |||
pvzCloseTimelessPanels(); | |||
window.setTimeout(pvzBuildCustomMobileTools, 0); | |||
return; | |||
} | |||
if (button) { | |||
event.preventDefault(); | |||
event.stopPropagation(); | |||
pvzToggleTimelessPanel(button.getAttribute('data-pvz-timeless-panel')); | |||
return; | |||
} | |||
if (backdrop) { | |||
event.preventDefault(); | |||
pvzCloseTimelessPanels(); | |||
return; | |||
} | |||
if (!panel) pvzCloseTimelessPanels(); | |||
}, true); | |||
document.addEventListener('keydown', function (event) { | |||
if (event.key === 'Escape') pvzCloseTimelessPanels(); | |||
}); | |||
pvzHandleTimelessViewportResize(); | |||
window.addEventListener('resize', function () { | |||
pvzHandleTimelessViewportResize(); | |||
}, { passive: true }); | |||
if ( | |||
window.screen && | |||
window.screen.orientation && | |||
typeof window.screen.orientation.addEventListener === 'function' | |||
) { | |||
window.screen.orientation.addEventListener('change', function () { | |||
pvzQueueTimelessPanelViewportRefresh(); | |||
}); | |||
} else { | |||
window.addEventListener('orientationchange', function () { | |||
pvzQueueTimelessPanelViewportRefresh(); | |||
}, { passive: true }); | |||
} | |||
if (window.visualViewport && typeof window.visualViewport.addEventListener === 'function') { | |||
window.visualViewport.addEventListener('resize', function () { | |||
pvzHandleTimelessViewportResize(); | |||
}, { passive: true }); | |||
/* visualViewport.scroll은 등록하지 않는다. | |||
주소창 접힘/스크롤만으로 패널 좌표를 계속 재계산하지 않는다. */ | |||
} | |||
} | |||
function pvzBuildCustomMobileTools() { | |||
var oldShell = document.querySelector('.pvz-custom-mobile-tools-shell, .pvz-custom-mobile-tools'); | |||
var oldCategory = document.querySelector('.pvz-mobile-category-box'); | |||
var navSection; | |||
var manageSection; | |||
var wikiSection; | |||
var moreSection; | |||
var documentSection; | |||
var alertsSection; | |||
var categorySection; | |||
var createdKeys = {}; | |||
var navEntries; | |||
var manageEntries; | |||
var wikiEntries; | |||
var moreEntries; | |||
var categoryEntries; | |||
function unique(entry) { | |||
var key; | |||
if (!entry) return null; | |||
key = entry.href + '|' + entry.label; | |||
if (createdKeys[key]) return null; | |||
createdKeys[key] = true; | |||
return entry; | |||
} | |||
if (!pvzIsTimeless() || !document.body) return; | |||
document.body.classList.remove('pvz-custom-mobile-tools-ready'); | |||
if (oldShell && oldShell.parentNode) oldShell.parentNode.removeChild(oldShell); | |||
if (oldCategory && oldCategory.parentNode) oldCategory.parentNode.removeChild(oldCategory); | |||
pvzArray(document.querySelectorAll('.pvz-timeless-wide-rail')).forEach(function (node) { | |||
if (node && node.parentNode) node.parentNode.removeChild(node); | |||
}); | |||
navEntries = [ | |||
unique(pvzToolEntryWithFallback('대문', 'mainpage', { | |||
ids: [/n-mainpage/i, /n-main-page/i], | |||
href: [/\/wiki\/(%EC%8B%9D%EB%AC%BC_vs_%EC%A2%80%EB%B9%84_%EC%9C%84%ED%82%A4|식물_vs_좀비_위키|Main_Page)/i], | |||
text: [/^대문$/, /^Main page$/i] | |||
}, pvzMakePageUrl(pvzMainPageName()))), | |||
unique(pvzToolEntryWithFallback('최근 바뀜', 'recentchanges', { | |||
ids: [/n-recentchanges/i], | |||
href: [/Special:(RecentChanges|최근바뀜)/i, /특수:(최근바뀜|RecentChanges)/i], | |||
text: [/^최근 바뀜$/, /^Recent changes$/i] | |||
}, pvzMakeSpecialUrl('RecentChanges'))), | |||
unique(pvzToolEntryWithFallback('미디어위키 도움말', 'help', { | |||
ids: [/n-help/i], | |||
href: [/mediawiki\.org.*Help/i, /Help:Contents/i], | |||
text: [/미디어위키 도움말/, /^도움말$/, /MediaWiki help/i, /^Help$/i] | |||
}, 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents')), | |||
unique(pvzToolEntryWithFallback('특수 문서 목록', 'specialpages', { | |||
ids: [/n-specialpages/i, /t-specialpages/i], | |||
href: [/Special:(SpecialPages|특수문서)/i, /특수:(특수문서|SpecialPages)/i], | |||
text: [/특수 문서 목록/, /Special pages/i] | |||
}, pvzMakeSpecialUrl('SpecialPages'))), | |||
unique(pvzToolEntryWithFallback('무작위 문서', 'random', { | |||
ids: [/n-randompage/i, /n-random/i], | |||
href: [/Special:(Random|RandomPage|임의문서)/i, /특수:(임의문서|Random|RandomPage)/i], | |||
text: [/임의 문서/, /무작위 문서/, /Random page/i] | |||
}, pvzMakeSpecialUrl('Random'))) | |||
].filter(Boolean); | |||
moreEntries = [ | |||
unique(pvzToolEntry('여기를 가리키는 문서', 'whatlinkshere', { | |||
ids: [/t-whatlinkshere/i], | |||
href: [/Special:(WhatLinksHere|가리키는문서)/i, /특수:(가리키는문서|WhatLinksHere)/i], | |||
text: [/여기를 가리키는 문서/, /What links here/i] | |||
})), | |||
unique(pvzToolEntry('가리키는 글의 최근 바뀜', 'recentchangeslinked', { | |||
ids: [/t-recentchangeslinked/i], | |||
href: [/Special:(RecentChangesLinked|가리키는글의최근바뀜)/i, /특수:(가리키는글의최근바뀜|RecentChangesLinked)/i], | |||
text: [/가리키는 글의 최근 바뀜/, /Related changes/i] | |||
})), | |||
unique(pvzToolEntry('고유 링크', 'permalink', { | |||
ids: [/t-permalink/i], | |||
href: [/oldid=/i], | |||
text: [/고유 링크/, /Permanent link/i] | |||
})), | |||
unique(pvzToolEntry('문서 정보', 'info', { | |||
ids: [/t-info/i, /ca-info/i], | |||
href: [/action=info/i], | |||
text: [/문서 정보/, /Page information/i] | |||
})), | |||
unique(pvzToolEntry('문서 기록', 'history', { | |||
ids: [/ca-history/i], | |||
href: [/action=history/i], | |||
text: [/문서 역사/, /문서 기록/, /^역사$/, /^History$/i, /^View history$/i] | |||
})) | |||
].filter(Boolean); | |||
wikiEntries = [ | |||
unique(pvzToolEntry('파일 올리기', 'upload', { | |||
ids: [/t-upload/i, /pt-upload/i], | |||
href: [/Special:(Upload|올리기)/i, /특수:(올리기|Upload)/i], | |||
text: [/^파일 올리기$/, /^Upload file$/i] | |||
})), | |||
unique(pvzToolEntry('축약된 URL 얻기', 'shorturl', { | |||
ids: [/t-urlshortener/i, /t-shorturl/i], | |||
href: [/Special:(UrlShortener|ShortUrl|URLShortener)/i, /특수:(UrlShortener|ShortUrl|축약)/i], | |||
text: [/축약된 URL/, /Short URL/i, /Get shortened URL/i] | |||
})) | |||
].filter(Boolean); | |||
manageEntries = [ | |||
unique(pvzToolEntryWithFallback('특수 문서', 'specialpages', { | |||
href: [/Special:(SpecialPages|특수문서)/i, /특수:(SpecialPages|특수문서)/i], | |||
text: [/^특수 문서$/, /^Special pages$/i] | |||
}, pvzMakeSpecialUrl('SpecialPages'))), | |||
unique(pvzToolEntryWithFallback('핵심 설정 관리', 'managewiki-core', { | |||
href: [/Special:ManageWiki(?:\/core)?(?:[/?#]|$)/i, /특수:ManageWiki(?:\/core)?(?:[/?#]|$)/i], | |||
text: [/이 위키의 핵심 설정을 관리하기/, /Manage this wiki's core settings/i] | |||
}, pvzMakeSpecialUrl('ManageWiki/core'))), | |||
unique(pvzToolEntryWithFallback('확장 기능 관리', 'managewiki-extensions', { | |||
href: [/Special:ManageWiki\/extensions/i, /특수:ManageWiki\/extensions/i], | |||
text: [/이 위키의 확장 기능을 관리하기/, /Manage this wiki's extensions/i] | |||
}, pvzMakeSpecialUrl('ManageWiki/extensions'))), | |||
unique(pvzToolEntryWithFallback('권한 관리', 'managewiki-permissions', { | |||
href: [/Special:ManageWiki\/permissions/i, /특수:ManageWiki\/permissions/i], | |||
text: [/이 위키의 권한 관리/, /Manage this wiki's permissions/i] | |||
}, pvzMakeSpecialUrl('ManageWiki/permissions'))), | |||
unique(pvzToolEntryWithFallback('이름공간 관리', 'managewiki-namespaces', { | |||
href: [/Special:ManageWiki\/namespaces/i, /특수:ManageWiki\/namespaces/i], | |||
text: [/이 위키의 이름공간 관리/, /Manage this wiki's namespaces/i] | |||
}, pvzMakeSpecialUrl('ManageWiki/namespaces'))), | |||
unique(pvzToolEntryWithFallback('백업 관리', 'datadump', { | |||
href: [/Special:(DataDump|ManageWiki\/backup|ManageWiki\/backups)/i, /특수:(DataDump|ManageWiki\/backup|ManageWiki\/backups)/i], | |||
text: [/이 위키의 백업을 관리/, /Manage this wiki's backups/i, /^DataDump$/i] | |||
}, pvzMakeSpecialUrl('DataDump'))) | |||
].filter(Boolean); | |||
categoryEntries = pvzCategoryEntriesForCustomBox().map(unique).filter(Boolean); | |||
navSection = pvzCreateToolsSection('둘러보기', navEntries, 'navigation'); | |||
moreSection = pvzCreateToolsSection('더 보기', moreEntries, 'more'); | |||
documentSection = pvzCreateToolsSection('문서 도구', moreEntries, 'document'); | |||
wikiSection = pvzCreateToolsSection('위키 도구', wikiEntries, 'wiki'); | |||
manageSection = pvzCreateToolsSection('위키 관리', manageEntries, 'management'); | |||
alertsSection = pvzAlertsSection(); | |||
categorySection = pvzCreateToolsSection('분류', categoryEntries, 'category'); | |||
pvzEnsureTimelessPageActions(); | |||
pvzEnsureTimelessActionbar(); | |||
pvzEnsureTimelessPanelBackdrop(); | |||
pvzCreateTimelessPanel('browse', '둘러보기', [navSection]); | |||
pvzCreateTimelessPanel('settings', '위키 도구', [wikiSection, manageSection]); | |||
pvzCreateTimelessPanel('document', '문서 도구', [documentSection]); | |||
pvzCreateTimelessPanel('more', '더 보기', [moreSection]); | |||
pvzCreateTimelessPanel('alerts', '알림', [alertsSection]); | |||
pvzCreateTimelessPanel('account', '계정', [pvzAccountSection()]); | |||
pvzBuildMobileCategoryBox(categorySection); | |||
pvzBuildTimelessWideTools(navEntries, wikiEntries, moreEntries, manageEntries, categoryEntries); | |||
pvzBindTimelessPanelEvents(); | |||
document.body.classList.add('pvz-custom-mobile-tools-ready'); | |||
document.body.classList.add('pvz-timeless-actions-ready'); | |||
pvzHideNativeTimelessToolPortlets(); | |||
pvzRemoveTimelessColorStripes(); | |||
} | |||
function pvzStabilizeCustomToc() { | |||
var toc = document.querySelector('.pvz-custom-toc'); | |||
var list = toc && toc.querySelector('.pvz-custom-toc-list'); | |||
if (!toc || !list || toc.classList.contains('is-collapsed')) return; | |||
toc.classList.add('pvz-toc-reflowing'); | |||
window.requestAnimationFrame(function () { | |||
var width; | |||
toc.classList.remove('pvz-toc-reflowing'); | |||
/* | |||
첫 렌더링 때 Timeless/폰트 로딩/float 계산이 겹치면 | |||
목차 항목의 max-content 폭이 너무 좁게 잡히는 경우가 있다. | |||
offsetWidth를 한 번 읽고 클래스를 붙여 강제로 레이아웃을 재계산한다. | |||
*/ | |||
width = toc.offsetWidth; | |||
toc.style.setProperty('--pvz-toc-measured-width', width + 'px'); | |||
toc.classList.add('pvz-toc-layout-ready'); | |||
}); | |||
} | |||
function pvzQueueTocStabilize() { | |||
pvzStabilizeCustomToc(); | |||
window.setTimeout(pvzStabilizeCustomToc, 80); | |||
window.setTimeout(pvzStabilizeCustomToc, 250); | |||
window.setTimeout(pvzStabilizeCustomToc, 700); | |||
if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') { | |||
document.fonts.ready.then(function () { | |||
pvzStabilizeCustomToc(); | |||
}); | |||
} | |||
} | |||
function pvzCreateCustomToc() { | |||
var contentText = document.getElementById('mw-content-text'); | |||
var parserOutput = contentText && ( | |||
contentText.querySelector('.mw-parser-output') || contentText | |||
); | |||
var leadContainer = parserOutput && ( | |||
parserOutput.querySelector(':scope > .mf-section-0') || | |||
parserOutput.querySelector(':scope > section.mf-section-0') || | |||
parserOutput | |||
); | |||
var existingWrap; | |||
var nativeTocs; | |||
var headings; | |||
var leadParagraph; | |||
var wrap; | |||
var toc; | |||
var head; | |||
var title; | |||
var toggle; | |||
var list; | |||
var counters = [0, 0, 0, 0, 0]; | |||
function getDepth(heading) { | |||
var level = parseInt(heading.tagName.replace('H', ''), 10); | |||
var depth = level - 1; | |||
if (depth < 1) depth = 1; | |||
if (depth > 5) depth = 5; | |||
return depth; | |||
} | |||
function getNumber(depth) { | |||
var i; | |||
var parts = []; | |||
counters[depth - 1] += 1; | |||
for (i = depth; i < counters.length; i += 1) { | |||
counters[i] = 0; | |||
} | |||
for (i = 0; i < depth; i += 1) { | |||
if (!counters[i]) counters[i] = 1; | |||
parts.push(counters[i]); | |||
} | |||
return parts.join('.'); | |||
} | |||
function isExcludedLeadContainer(element) { | |||
if (!element || !element.matches) return false; | |||
return element.matches( | |||
'table, figure, .thumb, .tright, .tleft, .floatright, .floatleft, .floatnone, ' + | |||
'.infobox, .pvz-cp-infobox, .pvz-character-infobox, .character-infobox, ' + | |||
'.navbox, .metadata, .catlinks, .mw-references-wrap, ' + | |||
'.pvz-message, .pvz-custom-toc-wrap, #toc, .toc' | |||
); | |||
} | |||
function isLeadParagraph(element) { | |||
var clone; | |||
var text; | |||
if ( | |||
!element || | |||
element.parentNode !== leadContainer || | |||
element.tagName !== 'P' | |||
) { | |||
return false; | |||
} | |||
if (isExcludedLeadContainer(element)) return false; | |||
if ( | |||
element.querySelector( | |||
'style, script, template, noscript, ' + | |||
'link[rel="mw-deduplicated-inline-style"], ' + | |||
'[data-mw-deduplicate]' | |||
) | |||
) { | |||
return false; | |||
} | |||
clone = element.cloneNode(true); | |||
clone.querySelectorAll( | |||
'figure, .thumb, .mw-file-description, .mw-default-size, ' + | |||
'.mw-image-border, .reference, sup, .mw-editsection, ' + | |||
'style, script, template, noscript, link, [data-mw-deduplicate]' | |||
).forEach(function (node) { | |||
node.remove(); | |||
}); | |||
text = (clone.textContent || '').replace(/\s+/g, ' ').trim(); | |||
if (!text) return false; | |||
if ( | |||
/^(?:\.mw-parser-output\b|@media\b|@supports\b|@layer\b|:root\b)/.test(text) && | |||
/[{};]/.test(text) | |||
) { | |||
return false; | |||
} | |||
return true; | |||
} | |||
function findLeadParagraph() { | |||
var children; | |||
var i; | |||
var child; | |||
if (!leadContainer) return null; | |||
children = pvzArray(leadContainer.children); | |||
for (i = 0; i < children.length; i += 1) { | |||
child = children[i]; | |||
if (isExcludedLeadContainer(child)) continue; | |||
if (isLeadParagraph(child)) { | |||
return child; | |||
} | |||
} | |||
return null; | |||
} | |||
function placeTocAfterLead(tocWrap, paragraph) { | |||
if ( | |||
!tocWrap || | |||
!paragraph || | |||
paragraph.parentNode !== leadContainer | |||
) { | |||
return false; | |||
} | |||
if ( | |||
tocWrap.parentNode === leadContainer && | |||
paragraph.nextSibling === tocWrap | |||
) { | |||
return true; | |||
} | |||
leadContainer.insertBefore(tocWrap, paragraph.nextSibling); | |||
return true; | |||
} | |||
if ( | |||
!parserOutput || | |||
!leadContainer || | |||
parserOutput.querySelector('.pvz-no-custom-toc') | |||
) { | |||
return; | |||
} | |||
leadParagraph = findLeadParagraph(); | |||
if (!leadParagraph) return; | |||
existingWrap = parserOutput.querySelector('.pvz-custom-toc-wrap'); | |||
if (existingWrap) { | |||
placeTocAfterLead(existingWrap, leadParagraph); | |||
pvzQueueTocStabilize(); | |||
return; | |||
} | |||
headings = pvzArray( | |||
parserOutput.querySelectorAll('h2, h3, h4, h5, h6') | |||
).filter(function (heading) { | |||
return !pvzSkipHeading(heading); | |||
}); | |||
if (headings.length < 2) return; | |||
nativeTocs = pvzArray( | |||
parserOutput.querySelectorAll('#toc, .toc') | |||
).filter(function (node) { | |||
return node && !( | |||
node.classList && | |||
node.classList.contains('pvz-custom-toc') | |||
); | |||
}); | |||
nativeTocs.forEach(function (old) { | |||
if (old && old.parentNode) { | |||
old.parentNode.removeChild(old); | |||
} | |||
}); | |||
wrap = document.createElement('div'); | |||
wrap.className = 'pvz-custom-toc-wrap'; | |||
toc = document.createElement('nav'); | |||
toc.className = 'pvz-custom-toc'; | |||
toc.setAttribute('aria-label', '목차'); | |||
head = document.createElement('div'); | |||
head.className = 'pvz-custom-toc-head'; | |||
title = document.createElement('div'); | |||
title.className = 'pvz-custom-toc-title'; | |||
title.textContent = '목차'; | |||
toggle = document.createElement('button'); | |||
toggle.type = 'button'; | |||
toggle.className = 'pvz-custom-toc-toggle'; | |||
toggle.setAttribute('aria-expanded', 'true'); | |||
toggle.textContent = '접기'; | |||
toggle.addEventListener('click', function (event) { | |||
var collapsed; | |||
event.preventDefault(); | |||
collapsed = !toc.classList.contains('is-collapsed'); | |||
toc.classList.toggle('is-collapsed', collapsed); | |||
toggle.setAttribute( | |||
'aria-expanded', | |||
collapsed ? 'false' : 'true' | |||
); | |||
toggle.textContent = collapsed ? '펼치기' : '접기'; | |||
}); | |||
head.appendChild(title); | |||
head.appendChild(toggle); | |||
list = document.createElement('ol'); | |||
list.className = 'pvz-custom-toc-list'; | |||
headings.forEach(function (heading, index) { | |||
var depth = getDepth(heading); | |||
var level = depth + 1; | |||
var text = pvzHeadingText(heading); | |||
var anchor = pvzHeadingId(heading, text, index + 1); | |||
var number = getNumber(depth); | |||
var item = document.createElement('li'); | |||
var link = document.createElement('a'); | |||
var numberSpan = document.createElement('span'); | |||
var textSpan = document.createElement('span'); | |||
item.className = | |||
'pvz-custom-toc-item pvz-custom-toc-level-' + | |||
level + | |||
' pvz-custom-toc-depth-' + | |||
depth; | |||
link.href = '#' + anchor; | |||
numberSpan.className = 'pvz-custom-toc-number'; | |||
numberSpan.textContent = number; | |||
textSpan.className = 'pvz-custom-toc-text'; | |||
textSpan.textContent = text; | |||
link.appendChild(numberSpan); | |||
link.appendChild(textSpan); | |||
item.appendChild(link); | |||
list.appendChild(item); | |||
}); | |||
toc.appendChild(head); | |||
toc.appendChild(list); | |||
wrap.appendChild(toc); | |||
placeTocAfterLead(wrap, leadParagraph); | |||
pvzQueueTocStabilize(); | |||
} | |||
var pvzFooterGapTimer = null; | |||
function pvzGetFooterElement() { | |||
return document.getElementById('mw-footer-container') || | |||
document.getElementById('footer') || | |||
document.querySelector('.mw-footer') || | |||
document.querySelector('footer'); | |||
} | |||
function pvzUpdateFooterBackdropGapClass() { | |||
var footer = pvzGetFooterElement(); | |||
var html = document.documentElement; | |||
var body = document.body; | |||
var rect; | |||
var viewportHeight; | |||
var docHeight; | |||
var hasGap; | |||
if (!html || !body || !footer || !footer.getBoundingClientRect) return; | |||
html.classList.remove('pvz-footer-backdrop-gap-needed'); | |||
body.classList.remove('pvz-footer-backdrop-gap-needed'); | |||
rect = footer.getBoundingClientRect(); | |||
viewportHeight = window.visualViewport && window.visualViewport.height ? | |||
window.visualViewport.height : | |||
window.innerHeight; | |||
docHeight = Math.max( | |||
body.scrollHeight || 0, | |||
body.offsetHeight || 0, | |||
html.clientHeight || 0, | |||
html.scrollHeight || 0, | |||
html.offsetHeight || 0 | |||
); | |||
/* footer container의 실제 바닥이 viewport 아래까지 닿지 못할 때만 켠다. | |||
3px 여유값은 확대/축소 반올림 오차 방지용이다. */ | |||
hasGap = rect.bottom < viewportHeight - 3 && docHeight <= viewportHeight + 6; | |||
if (hasGap) { | |||
html.classList.add('pvz-footer-backdrop-gap-needed'); | |||
body.classList.add('pvz-footer-backdrop-gap-needed'); | |||
} | |||
} | |||
function pvzQueueFooterBackdropGapCheck() { | |||
window.clearTimeout(pvzFooterGapTimer); | |||
pvzFooterGapTimer = window.setTimeout(pvzUpdateFooterBackdropGapClass, 80); | |||
} | |||
function pvzStartFooterBackdropGapWatcher() { | |||
pvzUpdateFooterBackdropGapClass(); | |||
window.setTimeout(pvzUpdateFooterBackdropGapClass, 120); | |||
window.setTimeout(pvzUpdateFooterBackdropGapClass, 400); | |||
window.setTimeout(pvzUpdateFooterBackdropGapClass, 1000); | |||
window.addEventListener('resize', pvzQueueFooterBackdropGapCheck, { passive: true }); | |||
window.addEventListener('orientationchange', pvzQueueFooterBackdropGapCheck, { passive: true }); | |||
window.addEventListener('load', pvzQueueFooterBackdropGapCheck, { passive: true }); | |||
if (window.visualViewport) { | |||
window.visualViewport.addEventListener('resize', pvzQueueFooterBackdropGapCheck, { passive: true }); | |||
window.visualViewport.addEventListener('scroll', pvzQueueFooterBackdropGapCheck, { passive: true }); | |||
} | |||
if (window.MutationObserver && document.body) { | |||
var observer = new MutationObserver(pvzQueueFooterBackdropGapCheck); | |||
observer.observe(document.body, { | |||
childList: true, | |||
subtree: true, | |||
attributes: true, | |||
attributeFilter: ['class', 'style'] | |||
}); | |||
} | |||
} | |||
function pvzInit() { | |||
if (!pvzIsTimeless()) return; | |||
pvzApplyInitialTimelessColorMode(); | |||
pvzBindSystemThemeListener(); | |||
pvzBuildCustomSearch(); | |||
pvzMarkReady(); | |||
pvzRemoveTimelessColorStripes(); | |||
pvzStartFooterBackdropGapWatcher(); | |||
pvzCreateCustomToc(); | |||
pvzBuildCustomMobileTools(); | |||
window.setTimeout(pvzBuildCustomSearch, 120); | |||
window.setTimeout(pvzBuildCustomMobileTools, 300); | |||
window.setTimeout(pvzBuildCustomSearch, 600); | |||
window.setTimeout(pvzBuildCustomMobileTools, 1000); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', pvzInit); | |||
} else { | |||
pvzInit(); | |||
} | |||
if (window.mw && mw.hook) { | |||
mw.hook('wikipage.content').add(function () { | |||
pvzInit(); | |||
}); | |||
} | |||
if (window.addEventListener) { | |||
window.addEventListener('resize', function () { | |||
var shell = document.querySelector('.pvz-custom-mobile-tools-shell'); | |||
pvzApplyToolsCompactState(shell); | |||
}); | |||
} | |||
/* ========================= | |||
v94 쿠키 안내문 portal | |||
- CSS z-index만으로 footer 뒤에서 빠져나오지 못하는 경우를 해결한다. | |||
- 쿠키 안내문 DOM을 footer 밖, body 직속으로 옮겨 최상위 fixed 레이어로 표시한다. | |||
========================= */ | |||
function pvzLooksLikeCookieWarning(el) { | |||
var text; | |||
var idClass; | |||
if (!el || el.nodeType !== 1) return false; | |||
text = (el.textContent || '').replace(/\s+/g, ' ').trim(); | |||
idClass = ((el.id || '') + ' ' + (el.className || '')).toLowerCase(); | |||
if (text.indexOf('쿠키는 저희의 서비스 전달에 도움을 줍니다') !== -1) return true; | |||
if (text.indexOf('저희의 쿠키 사용에 동의') !== -1) return true; | |||
if (text.indexOf('쿠키') !== -1 && text.indexOf('서비스') !== -1 && text.length > 20) return true; | |||
if (idClass.indexOf('cookiewarning') !== -1) return true; | |||
if (idClass.indexOf('cookie-warning') !== -1) return true; | |||
if (idClass.indexOf('cookiewarning') !== -1) return true; | |||
if (idClass.indexOf('mw-cookie') !== -1) return true; | |||
if (idClass.indexOf('ext-cookie') !== -1) return true; | |||
return false; | |||
} | |||
function pvzFindCookieWarningElement() { | |||
var selectors = [ | |||
'#mw-cookiewarning-container', | |||
'.mw-cookiewarning-container', | |||
'#mw-cookiewarning', | |||
'.mw-cookiewarning', | |||
'.mw-cookie-warning', | |||
'#cookieWarning', | |||
'.cookieWarning', | |||
'.cookie-warning', | |||
'.ext-cookie-warning', | |||
'.ext-cookie-warning-container', | |||
'[id*="cookiewarning" i]', | |||
'[class*="cookiewarning" i]', | |||
'[id*="cookie-warning" i]', | |||
'[class*="cookie-warning" i]', | |||
'[id*="cookieWarning" i]', | |||
'[class*="cookieWarning" i]', | |||
'[id*="mw-cookie" i]', | |||
'[class*="mw-cookie" i]', | |||
'[id*="ext-cookie" i]', | |||
'[class*="ext-cookie" i]' | |||
]; | |||
var candidates = []; | |||
var found; | |||
var all; | |||
var i; | |||
var el; | |||
var parent; | |||
selectors.forEach(function (selector) { | |||
try { | |||
pvzArray(document.querySelectorAll(selector)).forEach(function (node) { | |||
if (candidates.indexOf(node) === -1) candidates.push(node); | |||
}); | |||
} catch (e) {} | |||
}); | |||
for (i = 0; i < candidates.length; i += 1) { | |||
if (pvzLooksLikeCookieWarning(candidates[i])) return candidates[i]; | |||
} | |||
all = document.body ? document.body.querySelectorAll('div, aside, section, p, span') : []; | |||
for (i = 0; i < all.length; i += 1) { | |||
el = all[i]; | |||
if (pvzLooksLikeCookieWarning(el)) { | |||
parent = el.closest('#mw-cookiewarning-container, .mw-cookiewarning-container, #mw-cookiewarning, .mw-cookiewarning, .mw-cookie-warning, #cookieWarning, .cookieWarning, .cookie-warning, .ext-cookie-warning, .ext-cookie-warning-container, div, aside, section') || el; | |||
return parent; | |||
} | |||
} | |||
return null; | |||
} | |||
function pvzPortalCookieWarning() { | |||
var el = pvzFindCookieWarningElement(); | |||
if (!el || !document.body) return; | |||
el.classList.add('pvz-cookie-warning-portal'); | |||
if (el.parentNode !== document.body) { | |||
document.body.appendChild(el); | |||
} | |||
} | |||
function pvzInitCookieWarningPortal() { | |||
var observer; | |||
pvzPortalCookieWarning(); | |||
window.setTimeout(pvzPortalCookieWarning, 250); | |||
window.setTimeout(pvzPortalCookieWarning, 1000); | |||
window.setTimeout(pvzPortalCookieWarning, 2500); | |||
if (window.MutationObserver && document.body) { | |||
observer = new MutationObserver(function () { | |||
pvzPortalCookieWarning(); | |||
}); | |||
observer.observe(document.body, { | |||
childList: true, | |||
subtree: true | |||
}); | |||
} | |||
} | |||
pvzInitCookieWarningPortal(); | |||
/* ========================= | |||
액션바 계열 패널 문서 좌표 고정 | |||
- Timeless 상단 sheet는 v122의 전용 위치 처리에서 직접 absolute 좌표를 계산한다. | |||
- 아래 document-lock은 그 외 액션바/드롭다운 패널에만 사용한다. | |||
========================= */ | |||
function pvzGetActionAndTopbarPanels() { | |||
var selectors = [ | |||
'.pvz-timeless-panel.is-open', | |||
'.pvz-mobile-panel.is-open', | |||
'.pvz-timeless-menu.is-open', | |||
'.pvz-timeless-dropdown.is-open', | |||
'.pvz-actionbar-panel.is-open', | |||
'.pvz-action-panel.is-open', | |||
'.pvz-topbar-panel.is-open', | |||
'.pvz-topbar-menu.is-open', | |||
'.pvz-timeless-panel[aria-hidden="false"]', | |||
'.pvz-mobile-panel[aria-hidden="false"]', | |||
'.pvz-timeless-menu[aria-hidden="false"]', | |||
'.pvz-timeless-dropdown[aria-hidden="false"]', | |||
'[data-pvz-panel-open="true"]', | |||
'[data-pvz-sheet-open="true"]', | |||
'[data-pvz-menu-open="true"]' | |||
]; | |||
var panels = []; | |||
selectors.forEach(function (selector) { | |||
try { | |||
pvzArray(document.querySelectorAll(selector)).forEach(function (panel) { | |||
if (panels.indexOf(panel) === -1) panels.push(panel); | |||
}); | |||
} catch (e) {} | |||
}); | |||
return panels; | |||
} | |||
function pvzDocumentLockPanel(panel) { | |||
var rect; | |||
var scrollX; | |||
var scrollY; | |||
var docTop; | |||
var docLeft; | |||
var width; | |||
if (!panel || panel.nodeType !== 1) return; | |||
rect = panel.getBoundingClientRect(); | |||
scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0; | |||
scrollY = window.pageYOffset || document.documentElement.scrollTop || 0; | |||
docTop = rect.top + scrollY; | |||
docLeft = rect.left + scrollX; | |||
width = rect.width || panel.offsetWidth || 0; | |||
panel.dataset.pvzPanelDocumentLocked = 'true'; | |||
panel.dataset.pvzPanelDocumentTop = String(docTop); | |||
panel.dataset.pvzPanelDocumentLeft = String(docLeft); | |||
panel.dataset.pvzPanelDocumentWidth = String(width); | |||
panel.style.setProperty('position', 'absolute', 'important'); | |||
panel.style.setProperty('top', docTop + 'px', 'important'); | |||
panel.style.setProperty('left', docLeft + 'px', 'important'); | |||
panel.style.setProperty('right', 'auto', 'important'); | |||
panel.style.setProperty('width', width + 'px', 'important'); | |||
panel.style.setProperty('max-width', 'calc(100vw - 12px)', 'important'); | |||
panel.style.setProperty('transform', 'none', 'important'); | |||
panel.style.setProperty('z-index', '2147483000', 'important'); | |||
} | |||
function pvzRestoreDocumentLockedPanel(panel) { | |||
var top; | |||
var left; | |||
var width; | |||
if (!panel || panel.nodeType !== 1) return; | |||
if (panel.dataset.pvzPanelDocumentLocked !== 'true') return; | |||
top = panel.dataset.pvzPanelDocumentTop; | |||
left = panel.dataset.pvzPanelDocumentLeft; | |||
width = panel.dataset.pvzPanelDocumentWidth; | |||
panel.style.setProperty('position', 'absolute', 'important'); | |||
if (top) panel.style.setProperty('top', top + 'px', 'important'); | |||
if (left) panel.style.setProperty('left', left + 'px', 'important'); | |||
if (width) panel.style.setProperty('width', width + 'px', 'important'); | |||
panel.style.setProperty('right', 'auto', 'important'); | |||
panel.style.setProperty('transform', 'none', 'important'); | |||
} | |||
function pvzDocumentLockOpenPanels() { | |||
pvzGetActionAndTopbarPanels().forEach(function (panel) { | |||
if (panel.dataset.pvzPanelDocumentLocked === 'true') { | |||
pvzRestoreDocumentLockedPanel(panel); | |||
return; | |||
} | |||
pvzDocumentLockPanel(panel); | |||
}); | |||
} | |||
function pvzRecalculateDocumentLockedPanels() { | |||
pvzGetActionAndTopbarPanels().forEach(function (panel) { | |||
panel.removeAttribute('data-pvz-panel-document-locked'); | |||
panel.removeAttribute('data-pvz-panel-document-top'); | |||
panel.removeAttribute('data-pvz-panel-document-left'); | |||
panel.removeAttribute('data-pvz-panel-document-width'); | |||
panel.style.removeProperty('position'); | |||
panel.style.removeProperty('top'); | |||
panel.style.removeProperty('left'); | |||
panel.style.removeProperty('right'); | |||
panel.style.removeProperty('width'); | |||
panel.style.removeProperty('max-width'); | |||
panel.style.removeProperty('transform'); | |||
window.setTimeout(function () { | |||
pvzDocumentLockPanel(panel); | |||
}, 0); | |||
}); | |||
} | |||
function pvzInitPanelDocumentLock() { | |||
var observer; | |||
pvzDocumentLockOpenPanels(); | |||
if (window.MutationObserver && document.body) { | |||
observer = new MutationObserver(function () { | |||
pvzDocumentLockOpenPanels(); | |||
}); | |||
observer.observe(document.body, { | |||
attributes: true, | |||
childList: true, | |||
subtree: true, | |||
attributeFilter: ['class', 'style', 'aria-hidden', 'data-pvz-panel-open', 'data-pvz-sheet-open', 'data-pvz-menu-open'] | |||
}); | |||
} | |||
/* 스크롤 중에는 좌표를 새로 계산하지 않는다. | |||
기존 코드가 fixed/top 값을 다시 넣는 경우만 문서 좌표 absolute 값으로 되돌린다. */ | |||
window.addEventListener('scroll', function () { | |||
window.requestAnimationFrame(function () { | |||
pvzGetActionAndTopbarPanels().forEach(pvzRestoreDocumentLockedPanel); | |||
}); | |||
}, { passive: true }); | |||
window.addEventListener('resize', function () { | |||
window.clearTimeout(pvzInitPanelDocumentLock._resizeTimer); | |||
pvzInitPanelDocumentLock._resizeTimer = window.setTimeout( | |||
pvzRecalculateDocumentLockedPanels, | |||
260 | |||
); | |||
}, { passive: true }); | |||
function pvzQueueDocumentLockedPanelRecalculation() { | |||
window.setTimeout(pvzRecalculateDocumentLockedPanels, 520); | |||
} | |||
if ( | |||
window.screen && | |||
window.screen.orientation && | |||
typeof window.screen.orientation.addEventListener === 'function' | |||
) { | |||
window.screen.orientation.addEventListener( | |||
'change', | |||
pvzQueueDocumentLockedPanelRecalculation | |||
); | |||
} else { | |||
window.addEventListener( | |||
'orientationchange', | |||
pvzQueueDocumentLockedPanelRecalculation, | |||
{ passive: true } | |||
); | |||
} | |||
} | |||
pvzInitPanelDocumentLock(); | |||
}()); | }()); | ||
2026년 7월 27일 (월) 16:32 판
/* ===== core-js.txt ===== */
/* =========================
MediaWiki:Gadget-core.js
========================= */
(function () {
'use strict';
document.documentElement.classList.add('pvz-desktop-booting');
var PVZ_SEARCH_TIMER = null;
var PVZ_SEARCH_REQUEST_ID = 0;
var PVZ_SELECTED_SUGGESTION = -1;
var PVZ_SUGGESTIONS = [];
var PVZ_LAST_SEARCH_QUERY = '';
function pvzReady(callback) {
function run() {
if (window.mw && mw.loader && mw.loader.using) {
mw.loader.using('mediawiki.util').then(callback, callback);
} else {
callback();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run, { once: true });
} else {
run();
}
}
function pvzArray(list) {
return Array.prototype.slice.call(list || []);
}
function pvzClosest(target, selector) {
if (!target) return null;
if (target.nodeType !== 1) target = target.parentElement;
if (!target || typeof target.closest !== 'function') return null;
return target.closest(selector);
}
function pvzText(node) {
return node ? (node.textContent || '').replace(/\s+/g, ' ').trim() : '';
}
function pvzRemoveById(id) {
var el = document.getElementById(id);
if (el && el.parentNode) el.parentNode.removeChild(el);
}
function pvzGetUrl(title) {
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
return mw.util.getUrl(title);
}
return '/wiki/' + encodeURIComponent(String(title || '')).replace(/%20/g, '_');
}
function pvzGetApiUrl() {
if (window.mw && mw.util && typeof mw.util.wikiScript === 'function') {
return mw.util.wikiScript('api');
}
if (window.mw && mw.config && mw.config.get('wgScriptPath')) {
return mw.config.get('wgScriptPath') + '/api.php';
}
return '/w/api.php';
}
function pvzGetPageName() {
if (window.mw && mw.config && mw.config.get('wgPageName')) {
return mw.config.get('wgPageName');
}
return (document.title || '').replace(/\s*-\s*.*$/, '').replace(/\s+/g, '_');
}
function pvzGetTitleText() {
var heading = document.getElementById('firstHeading') ||
document.querySelector('.mw-first-heading, .firstHeading');
if (heading) {
return pvzText(heading);
}
if (window.mw && mw.config && mw.config.get('wgTitle')) {
return mw.config.get('wgTitle');
}
return (document.title || '').replace(/\s*-\s*.*$/, '');
}
function pvzGetTalkPageName() {
var pageName = pvzGetPageName();
var ns;
var title;
var namespaces;
var talkNamespace;
if (window.mw && mw.config) {
ns = mw.config.get('wgNamespaceNumber');
title = mw.config.get('wgTitle');
namespaces = mw.config.get('wgFormattedNamespaces');
if (ns === -1 || ns % 2 === 1) return pageName;
talkNamespace = namespaces && namespaces[ns + 1] ? namespaces[ns + 1] : 'Talk';
if (ns === 0) {
talkNamespace = namespaces && namespaces[1] ? namespaces[1] : 'Talk';
}
return talkNamespace + ':' + title;
}
return 'Talk:' + pageName;
}
function pvzGetSearchUrl(query) {
var value = (query || '').replace(/\s+/g, ' ').trim();
if (!value) return pvzGetUrl('Special:Search');
return pvzGetUrl('Special:Search') + '?search=' + encodeURIComponent(value) + '&fulltext=1';
}
function pvzSyncClasses() {
var root = document.documentElement;
var body = document.body;
var ua = navigator.userAgent || '';
var screenW = window.screen && window.screen.width ? window.screen.width : 9999;
var screenH = window.screen && window.screen.height ? window.screen.height : 9999;
var hasTouch = !!(navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0);
var nativeMobile = !!(body && body.classList && body.classList.contains('mw-mf'));
var isVector = !!(body && body.classList && (
body.classList.contains('skin-vector') ||
body.classList.contains('skin-vector-2022')
));
var touchDevice = !!(
/Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(ua) ||
(hasTouch && Math.min(screenW, screenH) <= 1400)
);
root.classList.toggle('pvz-skin-vector', isVector);
root.classList.toggle('pvz-native-mobile', nativeMobile);
root.classList.toggle('pvz-native-desktop', !nativeMobile);
root.classList.toggle('pvz-touch-device', touchDevice);
root.classList.toggle('pvz-desktop-view-on-touch', touchDevice && !nativeMobile);
if (body) {
body.classList.toggle('pvz-vector-ready', isVector);
}
}
function pvzCreateIcon(iconClass) {
var span = document.createElement('span');
span.className = 'pvz-icon ' + iconClass;
span.setAttribute('aria-hidden', 'true');
return span;
}
function pvzSetIconOnly(el, iconClass) {
el.textContent = '';
el.appendChild(pvzCreateIcon(iconClass));
}
function pvzAppendIconText(parent, iconClass, label) {
var text = document.createElement('span');
parent.appendChild(pvzCreateIcon(iconClass));
text.textContent = label;
parent.appendChild(text);
}
function pvzSetBoardButtonContent(parent, iconClass, label) {
var text = document.createElement('span');
parent.textContent = '';
if (iconClass) {
parent.appendChild(pvzCreateIcon(iconClass));
}
text.className = 'pvz-board-btn-text';
text.textContent = label;
parent.appendChild(text);
}
function pvzPortletLink(ids) {
var i;
var link;
for (i = 0; i < ids.length; i += 1) {
link = document.querySelector('#' + ids[i] + ' a');
if (link && link.href) return link;
}
return null;
}
function pvzCloneAction(ids, label, className, iconClass) {
var source = pvzPortletLink(ids);
var link;
var action;
var ns;
var displayLabel;
if (!source) return null;
displayLabel = label || pvzText(source);
link = document.createElement('a');
link.className = className || 'pvz-board-btn';
link.href = source.href;
pvzSetBoardButtonContent(link, iconClass, displayLabel);
if (source.getAttribute('title')) {
link.title = source.getAttribute('title');
}
if (source.classList.contains('new') || source.closest('.new')) {
link.classList.add('new');
}
if (source.closest('.selected') || source.closest('.mw-list-item.selected')) {
link.classList.add('is-current');
}
if (window.mw && mw.config) {
action = mw.config.get('wgAction') || 'view';
ns = mw.config.get('wgNamespaceNumber');
if (
action === 'view' &&
ids.some(function (id) { return id === 'ca-view' || id === 'ca-nstab-main' || id === 'ca-nstab-project'; }) &&
!(typeof ns === 'number' && ns % 2 === 1)
) {
link.classList.add('is-current');
}
if (
typeof ns === 'number' &&
ns % 2 === 1 &&
ids.some(function (id) { return id === 'ca-talk'; })
) {
link.classList.add('is-current');
}
if (
(action === 'edit' || action === 'submit' || action === 'visualeditor') &&
ids.some(function (id) { return id === 'ca-edit' || id === 'ca-ve-edit' || id === 'ca-viewsource'; })
) {
link.classList.add('is-current');
}
if (
action === 'history' &&
ids.some(function (id) { return id === 'ca-history'; })
) {
link.classList.add('is-current');
}
}
return link;
}
function pvzAppendIf(parent, child) {
if (child) parent.appendChild(child);
}
function pvzCreateBoardHead() {
var head = document.createElement('div');
var left = document.createElement('div');
var title = document.createElement('div');
var tabs = document.createElement('div');
var actions = document.createElement('div');
var moreMap;
var moreLinks;
head.className = 'pvz-board-head';
left.className = 'pvz-board-head-left';
title.className = 'pvz-board-title-ribbon';
tabs.className = 'pvz-board-tabs';
actions.className = 'pvz-board-actions';
title.textContent = pvzGetTitleText();
var viewLink = pvzCloneAction(['ca-view', 'ca-nstab-main', 'ca-nstab-project'], '문서', null, 'pvz-icon-special');
var talkLink = pvzCloneAction(['ca-talk'], '토론', null, 'pvz-icon-talk');
if (viewLink) viewLink.classList.add('is-tab');
if (talkLink) talkLink.classList.add('is-tab');
pvzAppendIf(tabs, viewLink);
pvzAppendIf(tabs, talkLink);
pvzAppendIf(actions, pvzCloneAction(['ca-edit', 'ca-ve-edit', 'ca-viewsource'], '편집', null, 'pvz-icon-tools'));
pvzAppendIf(actions, pvzCloneAction(['ca-history'], '역사', null, 'pvz-icon-recent'));
pvzAppendIf(actions, pvzCloneAction(['ca-watch'], '즐겨찾기에 추가', null, 'pvz-icon-star'));
pvzAppendIf(actions, pvzCloneAction(['ca-unwatch'], '즐겨찾기 해제', null, 'pvz-icon-star'));
moreMap = [
{ ids: ['ca-move'], label: '이동', icon: 'pvz-icon-special' },
{ ids: ['ca-purge'], label: '새로고침', icon: 'pvz-icon-recent' },
{ ids: ['ca-delete'], label: '삭제', icon: 'pvz-icon-tools' },
{ ids: ['ca-protect'], label: '보호', icon: 'pvz-icon-permissions' },
{ ids: ['ca-unprotect'], label: '보호 해제', icon: 'pvz-icon-permissions' }
];
moreLinks = moreMap.map(function (item) {
return pvzCloneAction(item.ids, item.label, null, item.icon);
}).filter(Boolean);
if (moreLinks.length) {
var details = document.createElement('details');
var summary = document.createElement('summary');
var menu = document.createElement('div');
var moreHasCurrent = moreLinks.some(function (link) {
return link && link.classList && link.classList.contains('is-current');
});
details.className = 'pvz-board-more';
pvzSetBoardButtonContent(summary, 'pvz-icon-plus', '더보기');
if (moreHasCurrent) summary.classList.add('is-current');
menu.className = 'pvz-board-more-menu';
moreLinks.forEach(function (link) { menu.appendChild(link); });
details.appendChild(summary);
details.appendChild(menu);
actions.appendChild(details);
}
left.appendChild(title);
left.appendChild(tabs);
head.appendChild(left);
head.appendChild(actions);
return head;
}
function pvzGetActiveSearchInput() {
return document.getElementById('pvz-desktop-search-input') ||
document.getElementById('pvz-mobile-search-input');
}
function pvzCreateSearchPanel() {
var old = document.getElementById('pvz-search-suggestions');
var panel;
if (old) return old;
panel = document.createElement('div');
panel.id = 'pvz-search-suggestions';
panel.className = 'pvz-search-suggestions';
panel.setAttribute('aria-label', '검색어 추천');
document.body.appendChild(panel);
return panel;
}
function pvzCloseSuggestions() {
document.documentElement.classList.remove('pvz-suggestions-open');
PVZ_SELECTED_SUGGESTION = -1;
}
function pvzPositionSearchPanel() {
var panel = document.getElementById('pvz-search-suggestions');
var input = pvzGetActiveSearchInput();
var rect;
var left;
var top;
var width;
if (!panel || !input || typeof input.getBoundingClientRect !== 'function') return;
rect = input.getBoundingClientRect();
left = Math.max(8, rect.left);
top = rect.bottom + 6;
width = Math.min(rect.width, window.innerWidth - 16);
panel.style.setProperty('left', left + 'px', 'important');
panel.style.setProperty('top', top + 'px', 'important');
panel.style.setProperty('width', width + 'px', 'important');
}
function pvzOpenSuggestions() {
pvzPositionSearchPanel();
if (PVZ_SUGGESTIONS.length) {
document.documentElement.classList.add('pvz-suggestions-open');
}
}
function pvzEscapeRegExp(text) {
return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function pvzAppendHighlightedTitle(container, text, query) {
var value = text || '';
var keyword = (query || '').replace(/\s+/g, ' ').trim();
var regex;
var lastIndex = 0;
var match;
container.textContent = '';
if (!keyword) {
container.textContent = value;
return;
}
regex = new RegExp(pvzEscapeRegExp(keyword), 'ig');
while ((match = regex.exec(value)) !== null) {
if (match.index > lastIndex) {
var normal = document.createElement('span');
normal.className = 'pvz-search-suggestion-normal';
normal.textContent = value.slice(lastIndex, match.index);
container.appendChild(normal);
}
var strong = document.createElement('strong');
strong.className = 'pvz-search-suggestion-match';
strong.textContent = value.slice(match.index, match.index + match[0].length);
container.appendChild(strong);
lastIndex = match.index + match[0].length;
}
if (lastIndex === 0) {
container.textContent = value;
return;
}
if (lastIndex < value.length) {
var tail = document.createElement('span');
tail.className = 'pvz-search-suggestion-normal';
tail.textContent = value.slice(lastIndex);
container.appendChild(tail);
}
}
function pvzRenderSearchSuggestions(items) {
var panel = pvzCreateSearchPanel();
var list = document.createElement('ul');
panel.innerHTML = '';
PVZ_SELECTED_SUGGESTION = -1;
if (!items.length) {
pvzCloseSuggestions();
return;
}
list.className = 'pvz-search-suggestions-list';
items.forEach(function (item, index) {
var li = document.createElement('li');
var a = document.createElement('a');
var thumb = document.createElement('span');
var title = document.createElement('span');
li.className = 'pvz-search-suggestion';
li.setAttribute('data-pvz-suggestion-index', String(index));
a.className = 'pvz-search-suggestion-link';
a.href = item.href;
thumb.className = 'pvz-search-suggestion-thumb';
if (item.thumbnail) {
var img = document.createElement('img');
thumb.classList.add('has-image');
img.src = item.thumbnail;
img.alt = '';
img.loading = 'lazy';
thumb.appendChild(img);
} else {
thumb.classList.add('is-empty');
}
title.className = 'pvz-search-suggestion-title';
pvzAppendHighlightedTitle(title, item.title, PVZ_LAST_SEARCH_QUERY);
a.appendChild(thumb);
a.appendChild(title);
a.addEventListener('click', pvzCloseSuggestions);
li.appendChild(a);
list.appendChild(li);
});
panel.appendChild(list);
pvzOpenSuggestions();
}
function pvzFetchSearchSuggestions(query) {
var value = (query || '').replace(/\s+/g, ' ').trim();
var requestId;
var url;
if (!value || !window.fetch) {
PVZ_LAST_SEARCH_QUERY = '';
PVZ_SUGGESTIONS = [];
pvzRenderSearchSuggestions([]);
pvzCloseSuggestions();
return;
}
PVZ_LAST_SEARCH_QUERY = value;
requestId = ++PVZ_SEARCH_REQUEST_ID;
url =
pvzGetApiUrl() +
'?action=query' +
'&format=json' +
'&formatversion=2' +
'&generator=prefixsearch' +
'&gpsnamespace=0' +
'&gpslimit=8' +
'&gpssearch=' + encodeURIComponent(value) +
'&prop=pageimages' +
'&piprop=thumbnail' +
'&pithumbsize=72' +
'&pilimit=8' +
'&redirects=1';
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var pages;
if (requestId !== PVZ_SEARCH_REQUEST_ID) return;
pages = data && data.query && Array.isArray(data.query.pages)
? data.query.pages
: [];
pages.sort(function (a, b) {
return (a.index || 9999) - (b.index || 9999);
});
PVZ_SUGGESTIONS = pages.map(function (page) {
return {
title: page.title,
href: pvzGetUrl(page.title),
thumbnail: page.thumbnail && page.thumbnail.source ? page.thumbnail.source : ''
};
});
pvzRenderSearchSuggestions(PVZ_SUGGESTIONS);
})
.catch(function () {
if (requestId === PVZ_SEARCH_REQUEST_ID) {
PVZ_SUGGESTIONS = [];
pvzRenderSearchSuggestions([]);
pvzCloseSuggestions();
}
});
}
function pvzScheduleSearchSuggestions(query) {
window.clearTimeout(PVZ_SEARCH_TIMER);
PVZ_SEARCH_TIMER = window.setTimeout(function () {
pvzFetchSearchSuggestions(query);
}, 140);
}
function pvzUpdateSelectedSuggestion() {
document.querySelectorAll('.pvz-search-suggestion').forEach(function (li) {
var index = parseInt(li.getAttribute('data-pvz-suggestion-index'), 10);
li.classList.toggle('is-selected', index === PVZ_SELECTED_SUGGESTION);
});
}
function pvzHandleSearchKeydown(event) {
var input = pvzGetActiveSearchInput();
var item;
if (event.key === 'ArrowDown') {
if (!PVZ_SUGGESTIONS.length) return;
event.preventDefault();
PVZ_SELECTED_SUGGESTION += 1;
if (PVZ_SELECTED_SUGGESTION >= PVZ_SUGGESTIONS.length) PVZ_SELECTED_SUGGESTION = 0;
pvzUpdateSelectedSuggestion();
return;
}
if (event.key === 'ArrowUp') {
if (!PVZ_SUGGESTIONS.length) return;
event.preventDefault();
PVZ_SELECTED_SUGGESTION -= 1;
if (PVZ_SELECTED_SUGGESTION < 0) PVZ_SELECTED_SUGGESTION = PVZ_SUGGESTIONS.length - 1;
pvzUpdateSelectedSuggestion();
return;
}
if (event.key === 'Enter') {
if (PVZ_SELECTED_SUGGESTION >= 0 && PVZ_SUGGESTIONS[PVZ_SELECTED_SUGGESTION]) {
event.preventDefault();
item = PVZ_SUGGESTIONS[PVZ_SELECTED_SUGGESTION];
window.location.href = item.href;
return;
}
if (input && input.value.replace(/\s+/g, ' ').trim()) {
event.preventDefault();
window.location.href = pvzGetSearchUrl(input.value);
}
return;
}
if (event.key === 'Escape') {
event.preventDefault();
pvzCloseSuggestions();
}
}
function pvzGetLocal(key, fallback) {
try {
return localStorage.getItem(key) || fallback;
} catch (e) {
return fallback;
}
}
function pvzSetLocal(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {}
}
function pvzSetFontSize(value) {
if (value !== 'small' && value !== 'medium' && value !== 'large') value = 'small';
pvzSetLocal('pvz-font-size', value);
document.documentElement.classList.remove('pvz-font-small', 'pvz-font-medium', 'pvz-font-large');
document.documentElement.classList.add('pvz-font-' + value);
pvzUpdateChoices();
}
function pvzSetTheme(value) {
if (value !== 'auto' && value !== 'light' && value !== 'dark') value = 'auto';
pvzSetLocal('pvz-mobile-theme', value);
document.documentElement.classList.remove('pvz-theme-auto', 'pvz-theme-light', 'pvz-theme-dark');
document.documentElement.classList.add('pvz-theme-' + value);
pvzUpdateChoices();
}
function pvzSetWidth(value) {
if (value !== 'normal' && value !== 'wide') value = 'normal';
pvzSetLocal('pvz-mobile-width', value);
document.documentElement.classList.remove('pvz-width-normal', 'pvz-width-wide', 'pvz-width-full');
document.documentElement.classList.add('pvz-width-' + value);
pvzUpdateChoices();
}
function pvzSetAutoHide(value) {
if (value !== 'on' && value !== 'off') value = 'off';
pvzSetLocal('pvz-mobile-autohide', value);
document.documentElement.classList.remove('pvz-auto-hide-on', 'pvz-auto-hide-off');
document.documentElement.classList.add('pvz-auto-hide-' + value);
pvzUpdateChoices();
}
function pvzSetPerformance(value) {
if (value !== 'on' && value !== 'off') value = 'off';
pvzSetLocal('pvz-mobile-performance', value);
document.documentElement.classList.remove('pvz-performance-on', 'pvz-performance-off');
document.documentElement.classList.add('pvz-performance-' + value);
pvzUpdateChoices();
}
function pvzApplyPreferences() {
pvzSetFontSize(pvzGetLocal('pvz-font-size', 'small'));
pvzSetTheme(pvzGetLocal('pvz-mobile-theme', 'auto'));
pvzSetWidth(pvzGetLocal('pvz-mobile-width', 'normal'));
pvzSetAutoHide(pvzGetLocal('pvz-mobile-autohide', 'off'));
pvzSetPerformance(pvzGetLocal('pvz-mobile-performance', 'off'));
}
function pvzUpdateChoices() {
var values = {
font: pvzGetLocal('pvz-font-size', 'small'),
theme: pvzGetLocal('pvz-mobile-theme', 'auto'),
width: pvzGetLocal('pvz-mobile-width', 'normal'),
autohide: pvzGetLocal('pvz-mobile-autohide', 'off'),
performance: pvzGetLocal('pvz-mobile-performance', 'off')
};
document.querySelectorAll('.pvz-mobile-choice').forEach(function (button) {
var kind = button.getAttribute('data-pvz-setting-kind');
var value = button.getAttribute('data-pvz-setting-value');
button.classList.toggle('is-selected', values[kind] === value);
});
}
function pvzCreateMobileLink(iconClass, label, href) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = href;
pvzAppendIconText(a, iconClass, label);
li.appendChild(a);
return li;
}
function pvzCreateMobileLinkList(items) {
var ul = document.createElement('ul');
ul.className = 'pvz-mobile-link-list';
items.forEach(function (item) {
ul.appendChild(pvzCreateMobileLink(item.icon, item.label, item.href));
});
return ul;
}
function pvzCreateSheetSection(title, content) {
var section = document.createElement('section');
var h = document.createElement('div');
section.className = 'pvz-mobile-sheet-section';
h.className = 'pvz-mobile-sheet-section-title';
h.textContent = title;
section.appendChild(h);
section.appendChild(content);
return section;
}
function pvzCreateBrowsePanel() {
var panel = document.createElement('nav');
panel.id = 'pvz-mobile-panel-browse';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-left';
panel.setAttribute('aria-label', '둘러보기');
panel.appendChild(pvzCreateSheetSection('둘러보기', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-home', label: '대문', href: pvzGetUrl('대문') },
{ icon: 'pvz-icon-recent', label: '최근 바뀜', href: pvzGetUrl('Special:RecentChanges') },
{ icon: 'pvz-icon-random', label: '임의 문서로', href: pvzGetUrl('Special:Random') },
{ icon: 'pvz-icon-help', label: '미디어위키 도움말', href: pvzGetUrl('Help:Contents') },
{ icon: 'pvz-icon-special', label: '특수 문서 목록', href: pvzGetUrl('Special:SpecialPages') },
{ icon: 'pvz-icon-upload', label: '파일 올리기', href: pvzGetUrl('Special:Upload') }
])));
panel.appendChild(pvzCreateSheetSection('관리', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-settings', label: '이 위키의 핵심 설정을 관리하기', href: pvzGetUrl('Special:ManageWiki/settings') },
{ icon: 'pvz-icon-extensions', label: '이 위키의 확장 기능을 관리하기', href: pvzGetUrl('Special:ManageWiki/extensions') },
{ icon: 'pvz-icon-namespace', label: '이 위키의 이름공간 관리', href: pvzGetUrl('Special:ManageWiki/namespaces') },
{ icon: 'pvz-icon-permissions', label: '이 위키의 권한 관리', href: pvzGetUrl('Special:ManageWiki/permissions') },
{ icon: 'pvz-icon-plus', label: '이 위키의 추가 설정을 관리하기', href: pvzGetUrl('Special:ManageWiki/settings') },
{ icon: 'pvz-icon-backup', label: '이 위키의 백업을 관리', href: pvzGetUrl('Special:DataDump') }
])));
return panel;
}
function pvzCreateChoice(label, kind, value, extraClass) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'pvz-mobile-choice' + (extraClass ? ' ' + extraClass : '');
button.setAttribute('data-pvz-setting-kind', kind);
button.setAttribute('data-pvz-setting-value', value);
button.textContent = label;
button.addEventListener('click', function (event) {
event.preventDefault();
if (kind === 'font') pvzSetFontSize(value);
if (kind === 'theme') pvzSetTheme(value);
if (kind === 'width') pvzSetWidth(value);
if (kind === 'autohide') pvzSetAutoHide(value);
if (kind === 'performance') pvzSetPerformance(value);
});
return button;
}
function pvzCreateChoiceGroup(title, kind, choices, gridClass) {
var wrap = document.createElement('div');
var h = document.createElement('div');
var grid = document.createElement('div');
wrap.className = 'pvz-mobile-setting-group';
h.className = 'pvz-mobile-setting-title';
h.textContent = title;
grid.className = 'pvz-mobile-choice-grid' + (gridClass ? ' ' + gridClass : '');
choices.forEach(function (choice) {
grid.appendChild(pvzCreateChoice(choice.label, kind, choice.value, choice.extraClass));
});
wrap.appendChild(h);
wrap.appendChild(grid);
return wrap;
}
function pvzCreateSettingsPanel() {
var panel = document.createElement('section');
panel.id = 'pvz-mobile-panel-settings';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-right';
panel.setAttribute('aria-label', '화면 설정');
panel.appendChild(pvzCreateChoiceGroup('글자 크기', 'font', [
{ label: '작게', value: 'small' },
{ label: '보통', value: 'medium' },
{ label: '크게', value: 'large' }
], 'three'));
panel.appendChild(pvzCreateChoiceGroup('색상 테마', 'theme', [
{ label: '자동', value: 'auto' },
{ label: '라이트', value: 'light' },
{ label: '다크', value: 'dark', extraClass: 'dark' }
], 'three'));
panel.appendChild(pvzCreateChoiceGroup('페이지 너비', 'width', [
{ label: '기본', value: 'normal' },
{ label: '넓게', value: 'wide' }
], ''));
panel.appendChild(pvzCreateChoiceGroup('자동으로 메뉴 숨김', 'autohide', [
{ label: '끄기', value: 'off' },
{ label: '켜기', value: 'on' }
], ''));
panel.appendChild(pvzCreateChoiceGroup('Performance mode', 'performance', [
{ label: 'Off', value: 'off' },
{ label: 'On', value: 'on' }
], ''));
return panel;
}
function pvzFormatUserGroup(group) {
var map = {
bureaucrat: '사무관',
sysop: '관리자',
administrator: '관리자',
interfaceadmin: '인터페이스 관리자',
'interface-admin': '인터페이스 관리자',
bot: '봇',
autoconfirmed: '자동 인증된 사용자',
user: '사용자'
};
return map[group] || group;
}
function pvzFormatDate(value) {
var date;
var days = ['일', '월', '화', '수', '목', '금', '토'];
if (!value) return '확인 불가';
date = new Date(value);
if (isNaN(date.getTime())) return '확인 불가';
return date.getFullYear() + '년 ' + (date.getMonth() + 1) + '월 ' +
date.getDate() + '일 (' + days[date.getDay()] + ')';
}
function pvzCreateAccountPanel() {
var panel = document.createElement('section');
var username = window.mw && mw.config ? mw.config.get('wgUserName') : null;
var card = document.createElement('div');
var name = document.createElement('div');
var groups = document.createElement('div');
var stats = document.createElement('div');
panel.id = 'pvz-mobile-panel-account';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-right';
panel.setAttribute('aria-label', '계정');
card.className = 'pvz-mobile-account-card';
name.id = 'pvz-mobile-account-name';
name.className = 'pvz-mobile-account-name';
name.textContent = username || '로그인하지 않음';
groups.id = 'pvz-mobile-account-groups';
groups.className = 'pvz-mobile-account-groups';
groups.textContent = username ? '계정 정보를 불러오는 중...' : '비로그인 사용자';
stats.className = 'pvz-mobile-account-stats';
stats.innerHTML =
'<span>편집</span><span>가입일</span>' +
'<strong id="pvz-mobile-account-edits">-</strong>' +
'<strong id="pvz-mobile-account-registration">-</strong>';
card.appendChild(name);
card.appendChild(groups);
card.appendChild(stats);
panel.appendChild(card);
if (!username) {
panel.appendChild(pvzCreateSheetSection('계정', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-login', label: '로그인', href: pvzGetUrl('Special:UserLogin') },
{ icon: 'pvz-icon-plus', label: '회원 가입', href: pvzGetUrl('Special:CreateAccount') }
])));
return panel;
}
panel.appendChild(pvzCreateMobileLinkList([
{ icon: 'pvz-icon-talk', label: '토론', href: pvzGetUrl('User talk:' + username) },
{ icon: 'pvz-icon-star', label: '즐겨찾기 문서', href: pvzGetUrl('Special:Watchlist') },
{ icon: 'pvz-icon-recent', label: '기여', href: pvzGetUrl('Special:Contributions/' + username) }
]));
var logout = document.createElement('a');
logout.className = 'pvz-mobile-account-logout';
logout.href = pvzGetUrl('Special:UserLogout');
pvzAppendIconText(logout, 'pvz-icon-login', '로그아웃');
panel.appendChild(logout);
return panel;
}
function pvzLoadAccountInfo() {
var username = window.mw && mw.config ? mw.config.get('wgUserName') : null;
var url;
if (!username || !window.fetch) return;
url =
pvzGetApiUrl() +
'?action=query' +
'&format=json' +
'&list=users' +
'&usprop=groups%7Ceditcount%7Cregistration' +
'&ususers=' + encodeURIComponent(username);
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var user = data && data.query && data.query.users && data.query.users[0];
var groupBox = document.getElementById('pvz-mobile-account-groups');
var editBox = document.getElementById('pvz-mobile-account-edits');
var regBox = document.getElementById('pvz-mobile-account-registration');
if (!user) return;
if (groupBox) {
groupBox.textContent = (user.groups || [])
.filter(function (group) {
return group !== '*' && group !== 'user' && group !== 'autoconfirmed';
})
.map(pvzFormatUserGroup)
.join(' ') || '일반 사용자';
}
if (editBox) {
editBox.textContent = typeof user.editcount === 'number' ? String(user.editcount) : '-';
}
if (regBox) {
regBox.textContent = pvzFormatDate(user.registration);
}
})
.catch(function () {});
}
function pvzCreatePanels() {
var backdrop;
pvzRemoveById('pvz-mobile-panel-backdrop');
pvzRemoveById('pvz-mobile-panel-browse');
pvzRemoveById('pvz-mobile-panel-settings');
pvzRemoveById('pvz-mobile-panel-account');
backdrop = document.createElement('div');
backdrop.id = 'pvz-mobile-panel-backdrop';
backdrop.className = 'pvz-mobile-panel-backdrop';
document.body.appendChild(backdrop);
document.body.appendChild(pvzCreateBrowsePanel());
document.body.appendChild(pvzCreateSettingsPanel());
document.body.appendChild(pvzCreateAccountPanel());
pvzUpdateChoices();
}
function pvzClosePanels() {
document.documentElement.classList.remove('pvz-mobile-panel-open');
document.querySelectorAll('.pvz-mobile-sheet').forEach(function (panel) {
panel.classList.remove('is-open');
});
document.querySelectorAll('.pvz-mobile-nav-button').forEach(function (button) {
button.classList.remove('is-active');
});
}
function pvzOpenPanel(name) {
var panel = document.getElementById('pvz-mobile-panel-' + name);
if (!panel) return;
pvzCloseSuggestions();
pvzClosePanels();
document.documentElement.classList.add('pvz-mobile-panel-open');
panel.classList.add('is-open');
document.querySelectorAll('.pvz-mobile-nav-button').forEach(function (button) {
button.classList.toggle('is-active', button.getAttribute('data-pvz-mobile-panel') === name);
});
if (name === 'account') pvzLoadAccountInfo();
pvzUpdateChoices();
}
function pvzTogglePanel(name) {
var panel = document.getElementById('pvz-mobile-panel-' + name);
if (panel && panel.classList.contains('is-open')) {
pvzClosePanels();
return;
}
pvzOpenPanel(name);
}
function pvzCleanupOldBars() {
document.querySelectorAll(
'#pvz-appbar, .pvz-appbar, #pvz-pc-accountbar, .pvz-pc-accountbar, ' +
'#pvz-pc-side-settings-button, .pvz-pc-side-settings-button, .pvz-restored-topbar, ' +
'#pvz-mobile-topbar, .pvz-mobile-topbar, .pvz-timeless-custom-topbar, ' +
'.pvz-timeless-custom-search-form, .pvz-timeless-custom-search-input'
).forEach(function (el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
});
}
function pvzGroupTopbarTabs() {
var bar = document.getElementById('pvz-mobile-topbar');
var strip;
var buttons;
if (!bar) return;
strip = bar.querySelector('.pvz-topbar-tab-strip');
if (!strip) {
strip = document.createElement('div');
strip.className = 'pvz-topbar-tab-strip';
strip.setAttribute('aria-label', '상단 도구');
}
buttons = [
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="browse"]'),
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="settings"]'),
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="account"]')
].filter(Boolean);
if (!buttons.length) return;
if (strip.parentNode !== bar) {
bar.insertBefore(strip, bar.firstChild);
}
buttons.forEach(function (button) {
if (button.parentNode !== strip) {
strip.appendChild(button);
}
});
}
function pvzEnsureTopbar() {
var old = document.getElementById('pvz-mobile-topbar');
var bar;
var menuButton;
var searchForm;
var searchInput;
var alertsWrap;
var alertsButton;
var alertsPopover;
var alertsHeader;
var alertsBody;
var alertsFooter;
var allAlerts;
var preferencesLink;
var accountButton;
var settingsButton;
if (!document.body) return;
document.documentElement.classList.add('pvz-modern-ui');
pvzCleanupOldBars();
/* 데스크톱은 pvz-desktop-utilitybar만 사용한다. 예전 pvz-mobile-topbar 검색바는 만들지 않는다. */
if (window.matchMedia && window.matchMedia('(min-width: 721px)').matches) {
if (old && old.parentNode) old.parentNode.removeChild(old);
return;
}
/* 모바일 폭에서만 기존 상단바가 검색만 남거나 버튼이 빠지는 문제를 막기 위해 매번 재생성한다. */
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
bar = document.createElement('header');
bar.id = 'pvz-mobile-topbar';
bar.className = 'pvz-mobile-topbar';
bar.setAttribute('data-pvz-topbar', 'true');
bar.setAttribute('aria-label', '식좀위키 상단 바');
bar.style.setProperty('position', 'absolute', 'important');
bar.style.setProperty('top', '0', 'important');
bar.style.setProperty('right', '0', 'important');
bar.style.setProperty('left', 'auto', 'important');
bar.style.setProperty('transform', 'none', 'important');
menuButton = document.createElement('button');
menuButton.type = 'button';
menuButton.className = 'pvz-mobile-nav-button';
menuButton.title = '둘러보기';
menuButton.setAttribute('aria-label', '둘러보기');
menuButton.setAttribute('data-pvz-mobile-panel', 'browse');
pvzSetIconOnly(menuButton, 'pvz-icon-menu');
searchForm = document.createElement('form');
searchForm.className = 'pvz-mobile-search-form';
searchForm.setAttribute('role', 'search');
searchInput = document.createElement('input');
searchInput.id = 'pvz-mobile-search-input';
searchInput.className = 'pvz-mobile-search-input';
searchInput.type = 'search';
searchInput.placeholder = '문서 검색';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', function () {
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('focus', function () {
document.documentElement.classList.add('pvz-mobile-search-focused');
pvzPositionSearchPanel();
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('blur', function () {
window.setTimeout(function () {
document.documentElement.classList.remove('pvz-mobile-search-focused');
}, 200);
});
searchInput.addEventListener('keydown', pvzHandleSearchKeydown);
searchForm.addEventListener('submit', function (event) {
event.preventDefault();
if (searchInput.value.replace(/\s+/g, ' ').trim()) {
window.location.href = pvzGetSearchUrl(searchInput.value);
}
});
searchForm.appendChild(searchInput);
alertsWrap = document.createElement('div');
alertsWrap.id = 'pvz-alerts-wrap';
alertsWrap.className = 'pvz-alerts-wrap';
alertsButton = document.createElement('button');
alertsButton.type = 'button';
alertsButton.id = 'pvz-alerts-button';
alertsButton.className = 'pvz-mobile-nav-button';
alertsButton.title = '알림';
alertsButton.setAttribute('aria-label', '알림');
pvzSetIconOnly(alertsButton, 'pvz-icon-bell');
alertsPopover = document.createElement('div');
alertsPopover.id = 'pvz-alerts-popover';
alertsPopover.className = 'pvz-alerts-popover';
alertsHeader = document.createElement('div');
alertsHeader.className = 'pvz-alerts-popover-header';
alertsHeader.appendChild(pvzCreateIcon('pvz-icon-bell'));
var headerText = document.createElement('span');
headerText.textContent = '알림';
alertsHeader.appendChild(headerText);
alertsBody = document.createElement('div');
alertsBody.className = 'pvz-alerts-popover-body';
alertsBody.textContent = '새 알림이 없습니다.';
alertsFooter = document.createElement('div');
alertsFooter.className = 'pvz-alerts-popover-footer';
allAlerts = document.createElement('a');
allAlerts.href = pvzGetUrl('Special:Notifications');
allAlerts.textContent = '전체 알림';
preferencesLink = document.createElement('a');
preferencesLink.href = pvzGetUrl('Special:Preferences');
preferencesLink.textContent = '환경설정';
alertsFooter.appendChild(allAlerts);
alertsFooter.appendChild(preferencesLink);
alertsPopover.appendChild(alertsHeader);
alertsPopover.appendChild(alertsBody);
alertsPopover.appendChild(alertsFooter);
alertsWrap.appendChild(alertsButton);
alertsWrap.appendChild(alertsPopover);
accountButton = document.createElement('button');
accountButton.type = 'button';
accountButton.className = 'pvz-mobile-nav-button';
accountButton.title = '계정';
accountButton.setAttribute('aria-label', '계정');
accountButton.setAttribute('data-pvz-mobile-panel', 'account');
pvzSetIconOnly(accountButton, 'pvz-icon-account');
settingsButton = document.createElement('button');
settingsButton.type = 'button';
settingsButton.className = 'pvz-mobile-nav-button';
settingsButton.title = '화면 설정';
settingsButton.setAttribute('aria-label', '화면 설정');
settingsButton.setAttribute('data-pvz-mobile-panel', 'settings');
pvzSetIconOnly(settingsButton, 'pvz-icon-settings');
if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) {
bar.appendChild(menuButton);
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(settingsButton);
bar.appendChild(accountButton);
} else {
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(accountButton);
}
document.body.insertBefore(bar, document.body.firstChild);
pvzCreateSearchPanel();
if (!document.getElementById('pvz-mobile-panel-browse')) pvzCreatePanels();
pvzPositionSearchPanel();
}
function pvzEnsureDesktopUtilityBar() {
var old = document.getElementById('pvz-desktop-utilitybar');
var bar;
var searchForm;
var searchInput;
var alertsWrap;
var alertsButton;
var alertsPopover;
var alertsHeader;
var alertsBody;
var alertsFooter;
var allAlerts;
var preferencesLink;
var accountButton;
if (!document.body) return;
/* 모바일에서는 기존 모바일 상단바를 사용한다. */
if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) {
if (old && old.parentNode) old.parentNode.removeChild(old);
return;
}
if (old) {
if (
old.querySelector('#pvz-desktop-search-input') &&
old.querySelector('#pvz-desktop-alerts-button') &&
old.querySelector('#pvz-desktop-account-button')
) {
return;
}
if (old.parentNode) old.parentNode.removeChild(old);
}
bar = document.createElement('div');
bar.id = 'pvz-desktop-utilitybar';
bar.className = 'pvz-desktop-utilitybar';
bar.setAttribute('aria-label', '식좀위키 데스크톱 유틸리티 바');
searchForm = document.createElement('form');
searchForm.className = 'pvz-desktop-search-form';
searchForm.setAttribute('role', 'search');
searchInput = document.createElement('input');
searchInput.id = 'pvz-desktop-search-input';
searchInput.className = 'pvz-desktop-search-input';
searchInput.type = 'search';
searchInput.placeholder = '문서 검색';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', function () {
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('focus', function () {
document.documentElement.classList.add('pvz-mobile-search-focused');
pvzPositionSearchPanel();
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('blur', function () {
window.setTimeout(function () {
document.documentElement.classList.remove('pvz-mobile-search-focused');
}, 200);
});
searchInput.addEventListener('keydown', pvzHandleSearchKeydown);
searchForm.addEventListener('submit', function (event) {
event.preventDefault();
if (searchInput.value.replace(/\s+/g, ' ').trim()) {
window.location.href = pvzGetSearchUrl(searchInput.value);
}
});
searchForm.appendChild(searchInput);
alertsWrap = document.createElement('div');
alertsWrap.id = 'pvz-desktop-alerts-wrap';
alertsWrap.className = 'pvz-desktop-alerts-wrap';
alertsButton = document.createElement('button');
alertsButton.type = 'button';
alertsButton.id = 'pvz-desktop-alerts-button';
alertsButton.className = 'pvz-desktop-util-button';
alertsButton.title = '알림';
alertsButton.setAttribute('aria-label', '알림');
pvzSetIconOnly(alertsButton, 'pvz-icon-bell');
alertsPopover = document.createElement('div');
alertsPopover.id = 'pvz-desktop-alerts-popover';
alertsPopover.className = 'pvz-desktop-alerts-popover';
alertsHeader = document.createElement('div');
alertsHeader.className = 'pvz-desktop-alerts-popover-header';
alertsHeader.textContent = '알림';
alertsBody = document.createElement('div');
alertsBody.className = 'pvz-desktop-alerts-popover-body';
alertsBody.textContent = '새 알림이 없습니다.';
alertsFooter = document.createElement('div');
alertsFooter.className = 'pvz-desktop-alerts-popover-footer';
allAlerts = document.createElement('a');
allAlerts.href = pvzGetUrl('Special:Notifications');
allAlerts.textContent = '전체 알림';
preferencesLink = document.createElement('a');
preferencesLink.href = pvzGetUrl('Special:Preferences');
preferencesLink.textContent = '환경설정';
alertsFooter.appendChild(allAlerts);
alertsFooter.appendChild(preferencesLink);
alertsPopover.appendChild(alertsHeader);
alertsPopover.appendChild(alertsBody);
alertsPopover.appendChild(alertsFooter);
alertsWrap.appendChild(alertsButton);
alertsWrap.appendChild(alertsPopover);
alertsButton.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
alertsWrap.classList.toggle('is-open');
});
accountButton = document.createElement('button');
accountButton.type = 'button';
accountButton.id = 'pvz-desktop-account-button';
accountButton.className = 'pvz-desktop-util-button pvz-mobile-nav-button';
accountButton.title = '계정';
accountButton.setAttribute('aria-label', '계정');
accountButton.setAttribute('data-pvz-mobile-panel', 'account');
pvzSetIconOnly(accountButton, 'pvz-icon-account');
/* 왼쪽부터 검색바, 알림, 계정. 따라서 오른쪽 끝 기준은 계정 → 알림 → 검색바. */
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(accountButton);
document.body.insertBefore(bar, document.body.firstChild);
pvzCreateSearchPanel();
if (!document.getElementById('pvz-mobile-panel-account')) pvzCreatePanels();
pvzPositionSearchPanel();
}
function pvzEnsureLogo() {
var old = document.getElementById('pvz-page-logo-link');
var topbar = document.getElementById('pvz-mobile-topbar');
var logo;
var img;
if (old || !document.body) return;
logo = document.createElement('a');
logo.id = 'pvz-page-logo-link';
logo.className = 'pvz-page-logo-link';
logo.href = pvzGetUrl('식물 vs 좀비 위키');
logo.title = '식물 vs 좀비 위키';
logo.setAttribute('aria-label', '식물 vs 좀비 위키로 이동');
img = document.createElement('img');
img.src = '/wiki/Special:Redirect/file/PvZKR_Wiki_Logo2.png';
img.alt = 'Plants vs. Zombies 위키';
img.loading = 'eager';
logo.appendChild(img);
if (topbar && topbar.parentNode === document.body && topbar.nextSibling) document.body.insertBefore(logo, topbar.nextSibling);
else if (topbar && topbar.parentNode === document.body) document.body.appendChild(logo);
else document.body.insertBefore(logo, document.body.firstChild);
}
function pvzCreateSideLink(label, href) {
var a = document.createElement('a');
a.className = 'pvz-board-side-link';
a.href = href;
a.textContent = label;
return a;
}
function pvzCreateSideSection(title, items) {
var section = document.createElement('section');
var h = document.createElement('div');
var paper = document.createElement('div');
section.className = 'pvz-board-side-section pvz-side-section-' + String(title || '').replace(/\s+/g, '-');
h.className = 'pvz-board-side-title';
h.textContent = title;
paper.className = 'pvz-board-side-paper';
items.forEach(function (item) { paper.appendChild(pvzCreateSideLink(item.label, item.href)); });
section.appendChild(h);
section.appendChild(paper);
return section;
}
function pvzCreateSideNav() {
var nav = document.createElement('aside');
var pageName = pvzGetPageName();
var printableHref = window.location.href.split('#')[0] + (window.location.href.indexOf('?') === -1 ? '?printable=yes' : '&printable=yes');
nav.className = 'pvz-board-side-nav';
nav.setAttribute('aria-label', '위키 보조창');
nav.appendChild(pvzCreateSideSection('둘러보기', [
{ label: '최근 바뀜', href: pvzGetUrl('Special:RecentChanges') },
{ label: '임의 문서', href: pvzGetUrl('Special:Random') },
{ label: '분류', href: pvzGetUrl('Special:Categories') },
{ label: '미디어위키 도움말', href: pvzGetUrl('Help:Contents') }
]));
nav.appendChild(pvzCreateSideSection('문서 도구', [
{ label: '새 문서', href: pvzGetUrl('Special:NewPages') },
{ label: '파일 올리기', href: pvzGetUrl('Special:Upload') },
{ label: '여기로 링크하는 문서', href: pvzGetUrl('Special:WhatLinksHere/' + pageName) },
{ label: '인쇄용 판', href: printableHref },
{ label: '문서 정보', href: pvzGetUrl(pageName) + '?action=info' }
]));
nav.appendChild(pvzCreateSideSection('위키 관리', [
{ label: '특수 문서', href: pvzGetUrl('Special:SpecialPages') },
{ label: '핵심 설정 관리', href: pvzGetUrl('Special:ManageWiki/settings') },
{ label: '확장 기능 관리', href: pvzGetUrl('Special:ManageWiki/extensions') },
{ label: '이름공간 관리', href: pvzGetUrl('Special:ManageWiki/namespaces') },
{ label: '백업 관리', href: pvzGetUrl('Special:DataDump') }
]));
return nav;
}
function pvzCreateRelatedPanel() {
var panel = document.createElement('aside');
var title = document.createElement('div');
var list = document.createElement('div');
panel.id = 'pvz-board-related-panel';
panel.className = 'pvz-board-related-panel';
panel.setAttribute('aria-label', '관련 문서');
title.className = 'pvz-board-related-title';
title.textContent = '관련 문서';
list.id = 'pvz-board-related-list';
list.className = 'pvz-board-related-list';
panel.appendChild(title);
panel.appendChild(list);
return panel;
}
function pvzCreateRelatedCard(titleText, href, thumb) {
var link = document.createElement('a');
var media = document.createElement('div');
var titleBar = document.createElement('div');
var title = document.createElement('span');
link.className = 'pvz-related-card';
link.href = href;
media.className = 'pvz-related-card-media';
if (thumb) {
var img = document.createElement('img');
img.src = thumb;
img.alt = '';
img.loading = 'lazy';
media.appendChild(img);
}
titleBar.className = 'pvz-related-card-titlebar';
title.className = 'pvz-related-card-title';
title.textContent = titleText || '문서 이름';
titleBar.appendChild(title);
link.appendChild(media);
link.appendChild(titleBar);
return link;
}
function pvzCollectRelatedTitles(limit) {
var current = pvzGetPageName().replace(/_/g, ' ');
var contentText = document.getElementById('mw-content-text');
var links = contentText ? contentText.querySelectorAll('a[href]') : [];
var seen = {};
var results = [];
pvzArray(links).forEach(function (link) {
var href = link.getAttribute('href') || '';
var title = '';
if (!href || results.length >= limit) return;
if (href.indexOf('#') === 0) return;
if (href.indexOf('/wiki/') === 0) {
title = decodeURIComponent(href.replace(/^\/wiki\//, '').split('#')[0].split('?')[0]).replace(/_/g, ' ');
} else if (link.dataset && link.dataset.title) {
title = String(link.dataset.title).replace(/_/g, ' ');
}
if (!title || title === current) return;
if (/^(Special|File|파일|Category|분류|Template|틀|Help|User|사용자|Talk|토론):/i.test(title)) return;
if (seen[title]) return;
seen[title] = true;
results.push(title);
});
return results.slice(0, limit);
}
function pvzRenderRelatedCards(items) {
var list = document.getElementById('pvz-board-related-list');
if (!list) return;
list.innerHTML = '';
if (!items || !items.length) items = [{ title: '대문', href: pvzGetUrl('대문'), thumb: '' }];
items.forEach(function (item) { list.appendChild(pvzCreateRelatedCard(item.title, item.href, item.thumb)); });
}
function pvzLoadRelatedDocs() {
var titles = pvzCollectRelatedTitles(12);
var url;
pvzRenderRelatedCards(titles.map(function (title) { return { title: title, href: pvzGetUrl(title), thumb: '' }; }));
if (!titles.length || !window.fetch) return;
url = pvzGetApiUrl() + '?action=query&format=json&formatversion=2&prop=pageimages&piprop=thumbnail&pithumbsize=260&titles=' + encodeURIComponent(titles.join('|'));
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var pages = data && data.query && Array.isArray(data.query.pages) ? data.query.pages : [];
var map = {};
pages.forEach(function (page) { if (page && page.title) map[page.title] = page.thumbnail && page.thumbnail.source ? page.thumbnail.source : ''; });
pvzRenderRelatedCards(titles.map(function (title) { return { title: title, href: pvzGetUrl(title), thumb: map[title] || '' }; }));
})
.catch(function () {});
}
function pvzSetRelatedOpen(open) {
document.documentElement.classList.toggle('pvz-related-open', !!open);
if (document.body) document.body.classList.toggle('pvz-related-open', !!open);
var button = document.getElementById('pvz-side-toggle');
if (button) {
button.setAttribute('aria-expanded', open ? 'true' : 'false');
button.setAttribute('aria-label', open ? '관련 문서 닫기' : '관련 문서 열기');
button.setAttribute('title', open ? '관련 문서 닫기' : '관련 문서 열기');
button.innerHTML = '';
button.appendChild(pvzCreateIcon(open ? 'pvz-icon-chevron-left' : 'pvz-icon-chevron-right'));
}
}
function pvzEnsureSideToggle() {
var button = document.getElementById('pvz-side-toggle');
if (button || !document.body) return;
button = document.createElement('button');
button.type = 'button';
button.id = 'pvz-side-toggle';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-label', '관련 문서 열기');
button.setAttribute('title', '관련 문서 열기');
button.appendChild(pvzCreateIcon('pvz-icon-chevron-right'));
document.body.appendChild(button);
}
function pvzSetAlertsOpen(open) {
var wrap = document.getElementById('pvz-alerts-wrap');
if (wrap) wrap.classList.toggle('is-open', !!open);
}
function pvzToggleAlertsOpen() {
var wrap = document.getElementById('pvz-alerts-wrap');
if (!wrap) return;
pvzSetAlertsOpen(!wrap.classList.contains('is-open'));
}
function pvzHeadingText(heading) {
var clone;
if (!heading) return '';
clone = heading.cloneNode(true);
clone.querySelectorAll('.mw-editsection, .mw-headline-number, .toctogglecheckbox').forEach(function (el) {
el.remove();
});
return pvzText(clone);
}
function pvzSafeAnchor(text, index) {
var id = String(text || '')
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w가-힣ぁ-んァ-ン一-龥-]/g, '');
if (!id) id = 'section-' + index;
return 'pvz-section-' + id + '-' + index;
}
function pvzHeadingId(heading, text, index) {
var headline = heading.querySelector ? heading.querySelector('.mw-headline') : null;
var id;
if (headline && headline.id) return headline.id;
if (heading.id) return heading.id;
id = pvzSafeAnchor(text, index);
heading.id = id;
return id;
}
function pvzSkipHeading(heading) {
var text = pvzHeadingText(heading);
if (!text) return true;
if (
heading.closest('.pvz-custom-toc-wrap') ||
heading.closest('.toc') ||
heading.closest('#toc') ||
heading.closest('table') ||
heading.closest('.infobox') ||
heading.closest('.navbox') ||
heading.closest('.metadata') ||
heading.closest('.catlinks') ||
heading.closest('.mw-references-wrap') ||
heading.closest('.reference')
) {
return true;
}
return text === '목차' || text === 'Contents' || text === '각주' ||
text === 'References' || text === '분류';
}
function pvzIsTextParagraph(el) {
if (!el || el.tagName !== 'P') return false;
return (el.textContent || '').replace(/\s+/g, ' ').trim().length > 0;
}
function pvzCreateCustomToc() {
var contentText = document.getElementById('mw-content-text');
var contentRoot = contentText && (
contentText.querySelector('.mw-parser-output') || contentText
);
var existingWrap;
var nativeTocs;
var headings;
var leadParagraph;
var wrap;
var toc;
var head;
var title;
var toggle;
var list;
var counters = [0, 0, 0, 0, 0];
function getDepth(heading) {
var level = parseInt(heading.tagName.replace('H', ''), 10);
var depth = level - 1;
if (depth < 1) depth = 1;
if (depth > 5) depth = 5;
return depth;
}
function getNumber(depth) {
var i;
var parts = [];
counters[depth - 1] += 1;
for (i = depth; i < counters.length; i += 1) {
counters[i] = 0;
}
for (i = 0; i < depth; i += 1) {
if (!counters[i]) counters[i] = 1;
parts.push(counters[i]);
}
return parts.join('.');
}
function isExcludedLeadContainer(element) {
if (!element || !element.matches) return false;
return element.matches(
'table, figure, .thumb, .tright, .tleft, .floatright, .floatleft, .floatnone, ' +
'.infobox, .pvz-cp-infobox, .pvz-character-infobox, .character-infobox, ' +
'.navbox, .metadata, .catlinks, .mw-references-wrap, ' +
'.pvz-message, .pvz-custom-toc-wrap, #toc, .toc'
);
}
function isLeadParagraph(element) {
var clone;
var text;
/*
첫 문단은 .mw-parser-output의 직접 자식인 <p>만 인정한다.
틀이나 정보상자 내부의 <p>를 첫 문장으로 오인하지 않는다.
*/
if (!element || element.parentNode !== contentRoot || element.tagName !== 'P') {
return false;
}
if (isExcludedLeadContainer(element)) return false;
/*
TemplateStyles/ResourceLoader가 넣은 style 요소나
CSS 전용 문단은 실제 본문 문단으로 취급하지 않는다.
*/
if (
element.querySelector(
'style, script, template, noscript, ' +
'link[rel="mw-deduplicated-inline-style"], ' +
'[data-mw-deduplicate]'
)
) {
return false;
}
clone = element.cloneNode(true);
clone.querySelectorAll(
'figure, .thumb, .mw-file-description, .mw-default-size, ' +
'.mw-image-border, .reference, sup, .mw-editsection, ' +
'style, script, template, noscript, link, [data-mw-deduplicate]'
).forEach(function (node) {
node.remove();
});
text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
/*
일부 파서 구조에서는 CSS가 순수 텍스트 노드로 남을 수 있으므로
대표적인 CSS 시작 패턴도 추가로 제외한다.
*/
if (
/^(?:\.mw-parser-output\b|@media\b|@supports\b|@layer\b|:root\b)/.test(text) &&
/[{};]/.test(text)
) {
return false;
}
return text.length > 0;
}
function findLeadParagraph() {
var children;
var i;
var child;
if (!contentRoot) return null;
children = pvzArray(contentRoot.children);
for (i = 0; i < children.length; i += 1) {
child = children[i];
if (isExcludedLeadContainer(child)) continue;
if (isLeadParagraph(child)) {
return child;
}
}
return null;
}
function placeTocAfterLead(tocWrap, paragraph) {
if (
!tocWrap ||
!paragraph ||
paragraph.parentNode !== contentRoot
) {
return false;
}
if (
tocWrap.parentNode === contentRoot &&
paragraph.nextSibling === tocWrap
) {
return true;
}
contentRoot.insertBefore(tocWrap, paragraph.nextSibling);
return true;
}
if (!contentRoot || contentRoot.querySelector('.pvz-no-custom-toc')) return;
leadParagraph = findLeadParagraph();
/*
실제 첫 본문 문단이 확인되기 전에는 목차를 임의 위치에 생성하지 않는다.
틀:작성중이나 캐릭터 정보상자 내부 문단은 첫 문단 후보에서 제외한다.
*/
if (!leadParagraph) return;
existingWrap = contentRoot.querySelector('.pvz-custom-toc-wrap');
if (existingWrap) {
placeTocAfterLead(existingWrap, leadParagraph);
return;
}
headings = pvzArray(
contentRoot.querySelectorAll('h2, h3, h4, h5, h6')
).filter(function (heading) {
return !pvzSkipHeading(heading);
});
if (headings.length < 2) return;
nativeTocs = pvzArray(
contentRoot.querySelectorAll('#toc, .toc')
).filter(function (node) {
return node && !(
node.classList &&
node.classList.contains('pvz-custom-toc')
);
});
nativeTocs.forEach(function (old) {
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
});
wrap = document.createElement('div');
wrap.className = 'pvz-custom-toc-wrap';
toc = document.createElement('nav');
toc.className = 'pvz-custom-toc';
toc.setAttribute('aria-label', '목차');
head = document.createElement('div');
head.className = 'pvz-custom-toc-head';
title = document.createElement('div');
title.className = 'pvz-custom-toc-title';
title.textContent = '목차';
toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'pvz-custom-toc-toggle';
toggle.setAttribute('aria-expanded', 'true');
toggle.textContent = '접기';
toggle.addEventListener('click', function (event) {
var collapsed;
event.preventDefault();
collapsed = !toc.classList.contains('is-collapsed');
toc.classList.toggle('is-collapsed', collapsed);
toggle.setAttribute(
'aria-expanded',
collapsed ? 'false' : 'true'
);
toggle.textContent = collapsed ? '펼치기' : '접기';
});
head.appendChild(title);
head.appendChild(toggle);
list = document.createElement('ol');
list.className = 'pvz-custom-toc-list';
headings.forEach(function (heading, index) {
var depth = getDepth(heading);
var level = depth + 1;
var text = pvzHeadingText(heading);
var anchor = pvzHeadingId(heading, text, index + 1);
var number = getNumber(depth);
var item = document.createElement('li');
var link = document.createElement('a');
var numberSpan = document.createElement('span');
var textSpan = document.createElement('span');
item.className =
'pvz-custom-toc-item pvz-custom-toc-level-' +
level +
' pvz-custom-toc-depth-' +
depth;
link.href = '#' + anchor;
numberSpan.className = 'pvz-custom-toc-number';
numberSpan.textContent = number;
textSpan.className = 'pvz-custom-toc-text';
textSpan.textContent = text;
link.appendChild(numberSpan);
link.appendChild(textSpan);
item.appendChild(link);
list.appendChild(item);
});
toc.appendChild(head);
toc.appendChild(list);
wrap.appendChild(toc);
placeTocAfterLead(wrap, leadParagraph);
}
function pvzEnsureRelatedPanel() {
var panel = document.getElementById('pvz-board-related-panel') ||
document.querySelector('.pvz-board-related-panel');
if (!document.body) return panel;
if (!panel) {
panel = pvzCreateRelatedPanel();
}
/* 관련 문서 창은 왼쪽 레일 안에 두지 않고 body 직속으로 둔다.
이렇게 해야 blur 막이나 본문창보다 항상 위에 올라올 수 있다. */
if (panel.parentNode !== document.body) {
document.body.appendChild(panel);
}
return panel;
}
function pvzBuildBoard() {
var contentText = document.getElementById('mw-content-text');
var boardRoot;
var frameOuter;
var frameThird;
var frameSecond;
var frameInner;
var boardBody;
var paper;
var paperMain;
var paperMeta;
var sideNav;
var relatedPanel;
var leftRail;
var logo;
var catlinks;
var firstChild;
if (!contentText || !document.body) return;
pvzEnsureSideToggle();
if (document.body.classList.contains('pvz-board-ready')) {
logo = document.getElementById('pvz-page-logo-link');
boardRoot = document.getElementById('bodyContent') || document.querySelector('#content') || contentText.parentElement;
if (boardRoot && boardRoot.classList.contains('pvz-board-root')) {
leftRail = boardRoot.querySelector('.pvz-board-left-rail');
sideNav = boardRoot.querySelector('.pvz-board-side-nav');
relatedPanel = pvzEnsureRelatedPanel();
if (!leftRail) { leftRail = document.createElement('div'); leftRail.className = 'pvz-board-left-rail'; boardRoot.insertBefore(leftRail, boardRoot.firstChild); }
if (logo && logo.parentNode !== leftRail) leftRail.appendChild(logo);
if (!sideNav) leftRail.appendChild(pvzCreateSideNav());
relatedPanel = pvzEnsureRelatedPanel();
}
pvzLoadRelatedDocs();
pvzCreateCustomToc();
return;
}
boardRoot = document.getElementById('bodyContent') || document.querySelector('#content') || contentText.parentElement;
if (!boardRoot) return;
boardRoot.classList.add('pvz-board-root');
logo = document.getElementById('pvz-page-logo-link');
if (logo && logo.parentNode !== boardRoot) boardRoot.insertBefore(logo, boardRoot.firstChild);
frameOuter = document.createElement('div'); frameOuter.className = 'pvz-board-frame-outer-real';
frameThird = document.createElement('div'); frameThird.className = 'pvz-board-frame-third-real';
frameSecond = document.createElement('div'); frameSecond.className = 'pvz-board-frame-second-real';
frameInner = document.createElement('div'); frameInner.className = 'pvz-board-frame-inner-real';
boardBody = document.createElement('div'); boardBody.className = 'pvz-board-body';
paper = document.createElement('div'); paper.className = 'pvz-board-paper';
paperMain = document.createElement('div'); paperMain.className = 'pvz-board-paper-main';
paperMeta = document.createElement('div'); paperMeta.className = 'pvz-board-paper-meta';
sideNav = pvzCreateSideNav(); relatedPanel = pvzEnsureRelatedPanel();
leftRail = document.createElement('div'); leftRail.className = 'pvz-board-left-rail';
if (logo) leftRail.appendChild(logo);
leftRail.appendChild(sideNav);
firstChild = boardRoot.firstChild;
boardRoot.insertBefore(leftRail, firstChild || null);
boardRoot.insertBefore(frameOuter, leftRail.nextSibling || null);
paperMain.appendChild(contentText);
catlinks = document.getElementById('catlinks') || document.querySelector('.catlinks');
if (catlinks && !paperMeta.contains(catlinks)) paperMeta.appendChild(catlinks);
paper.appendChild(paperMain); paper.appendChild(paperMeta);
boardBody.appendChild(pvzCreateBoardHead()); boardBody.appendChild(paper);
frameInner.appendChild(boardBody); frameSecond.appendChild(frameInner); frameThird.appendChild(frameSecond); frameOuter.appendChild(frameThird);
document.body.classList.add('pvz-board-ready');
document.documentElement.classList.add('pvz-desktop-booted');
pvzLoadRelatedDocs();
pvzCreateCustomToc();
}
function pvzMarkBoardActionActive(target) {
var button = pvzClosest(target, '.pvz-board-btn, .pvz-board-more > summary');
if (!button) return;
document.querySelectorAll('.pvz-board-btn.is-clicked, .pvz-board-more > summary.is-clicked').forEach(function (el) {
el.classList.remove('is-clicked');
});
button.classList.add('is-clicked');
}
function pvzBindEvents() {
if (window.PVZ_VECTOR_EVENTS_BOUND) return;
window.PVZ_VECTOR_EVENTS_BOUND = true;
document.addEventListener('click', function (event) {
var panelButton = pvzClosest(event.target, '.pvz-mobile-nav-button[data-pvz-mobile-panel], .pvz-desktop-util-button[data-pvz-mobile-panel]');
var panel = pvzClosest(event.target, '.pvz-mobile-sheet');
var panelBackdrop = pvzClosest(event.target, '#pvz-mobile-panel-backdrop');
var suggestionsPanel = pvzClosest(event.target, '#pvz-search-suggestions');
var searchInput = pvzClosest(event.target, '#pvz-mobile-search-input');
var alertsButton = pvzClosest(event.target, '#pvz-alerts-button');
var alertsWrap = pvzClosest(event.target, '#pvz-alerts-wrap');
var desktopAlertsWrap = pvzClosest(event.target, '#pvz-desktop-alerts-wrap');
var sideToggle = pvzClosest(event.target, '#pvz-side-toggle');
var boardAction = pvzClosest(event.target, '.pvz-board-btn, .pvz-board-more > summary');
var name;
if (sideToggle) {
event.preventDefault();
event.stopPropagation();
pvzSetRelatedOpen(!document.body.classList.contains('pvz-related-open'));
return;
}
if (boardAction) {
pvzMarkBoardActionActive(boardAction);
}
if (alertsButton) {
event.preventDefault();
event.stopPropagation();
pvzToggleAlertsOpen();
return;
}
if (panelButton) {
event.preventDefault();
event.stopPropagation();
name = panelButton.getAttribute('data-pvz-mobile-panel');
pvzTogglePanel(name);
return;
}
if (panelBackdrop) {
event.preventDefault();
pvzClosePanels();
return;
}
if (!suggestionsPanel && !searchInput) pvzCloseSuggestions();
if (!alertsWrap) pvzSetAlertsOpen(false);
if (!desktopAlertsWrap) {
var desktopAlerts = document.getElementById('pvz-desktop-alerts-wrap');
if (desktopAlerts) desktopAlerts.classList.remove('is-open');
}
if (document.body.classList.contains('pvz-related-open') && !pvzClosest(event.target, '.pvz-board-related-panel') && !sideToggle) {
pvzSetRelatedOpen(false);
}
if (!panel && !panelButton && !panelBackdrop) pvzClosePanels();
}, true);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
pvzCloseSuggestions();
pvzClosePanels();
pvzSetAlertsOpen(false);
var desktopAlerts = document.getElementById('pvz-desktop-alerts-wrap');
if (desktopAlerts) desktopAlerts.classList.remove('is-open');
pvzSetRelatedOpen(false);
}
});
window.addEventListener('resize', function () {
pvzSyncClasses();
pvzPositionSearchPanel();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzLoadRelatedDocs();
});
window.addEventListener('scroll', function () {
pvzPositionSearchPanel();
}, true);
}
function pvzInit() {
pvzSyncClasses();
pvzApplyPreferences();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzBuildBoard();
pvzBindEvents();
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 120);
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 600);
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 1400);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function () {
pvzInit();
});
}
pvzReady(pvzInit);
}());
/* ===== mobile-js.txt ===== */
/* =========================
MediaWiki:Gadget-mobile.js
Timeless 모바일 알림·리본·본문 프레임 재정리 v125
- v124 기준
- 검색바 1280px 이상 판정을 visualViewport가 아닌 레이아웃 viewport 기준으로 변경
- 아이패드 가로 화면 핀치 확대 시 검색바가 100% 폭으로 풀리는 문제 수정
- 검색바 왼쪽 돋보기 아이콘 제거
- 검색 문구를 '위키에서 검색하세요'로 변경
- 검색 제안에서 입력 문자열과 일치하는 부분만 굵게 강조
- MediaWiki API 기반 검색 제안과 문서 대표 이미지 표시 유지
- 한글 IME 입력·키보드 탐색·바깥 클릭 닫기 대응 유지
- 화면 회전·리사이즈·본문 폭 변화 시 검색바 폭 자동 재계산
- 사용자 환경 기반 초기 테마 적용 유지
- Timeless 상단 패널 absolute 문서 좌표 방식 유지
- 모바일 목차 .mf-section-0 대응 유지
- footer 아래 실제 여백 감지 기능 유지
========================= */
(function () {
'use strict';
document.documentElement.classList.add('pvz-timeless-mobile-booting');
function pvzArray(list) {
return Array.prototype.slice.call(list || []);
}
function pvzIsTimeless() {
var skinName = '';
if (window.mw && mw.config && typeof mw.config.get === 'function') {
skinName = mw.config.get('skin') || mw.config.get('wgDefaultSkin') || '';
}
return skinName === 'timeless' ||
(document.body && document.body.classList && document.body.classList.contains('skin-timeless'));
}
function pvzMarkReady() {
document.documentElement.classList.add('pvz-timeless-mobile-ready');
document.documentElement.classList.add('pvz-timeless-mobile-booted');
if (document.body) {
document.body.classList.add('pvz-timeless-mobile-ready');
}
}
function pvzSkipHeading(heading) {
if (!heading) return true;
if (heading.closest && heading.closest('.pvz-custom-toc, .toc, #toc, .navbox, .metadata, .catlinks, .mw-references-wrap')) return true;
if (heading.classList && heading.classList.contains('pvz-custom-toc-title')) return true;
return !(heading.textContent || '').replace(/\s+/g, ' ').trim();
}
function pvzHeadingText(heading) {
var clone = heading.cloneNode(true);
var text;
clone.querySelectorAll('.mw-editsection, .mw-headline-anchor, .reference, sup').forEach(function (node) {
node.remove();
});
text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
return text || '문단';
}
function pvzHeadingId(heading, text, index) {
var headline = heading.querySelector && heading.querySelector('.mw-headline');
var anchor = headline || heading;
var id = anchor.id;
if (!id) {
id = 'pvz-heading-' + index + '-' + encodeURIComponent(text).replace(/%/g, '');
anchor.id = id;
}
return id;
}
function pvzNormalizeText(text) {
return (text || '').replace(/\s+/g, ' ').trim();
}
function pvzLinkKey(link) {
var href = link && link.getAttribute ? link.getAttribute('href') || '' : '';
var text = pvzNormalizeText(link ? link.textContent : '');
return href + '|' + text;
}
function pvzFindExistingLink(config) {
var links = pvzArray(document.querySelectorAll('a[href]'));
var idPatterns = config.ids || [];
var textPatterns = config.text || [];
var hrefPatterns = config.href || [];
var i;
var link;
var href;
var text;
var id;
for (i = 0; i < links.length; i += 1) {
link = links[i];
id = link.id || (link.parentNode && link.parentNode.id) || '';
if (id && idPatterns.some(function (pattern) {
return pattern.test(id);
})) {
return link;
}
}
for (i = 0; i < links.length; i += 1) {
link = links[i];
href = link.getAttribute('href') || '';
if (href && hrefPatterns.some(function (pattern) {
return pattern.test(href);
})) {
return link;
}
}
for (i = 0; i < links.length; i += 1) {
link = links[i];
text = pvzNormalizeText(link.textContent);
if (text && textPatterns.some(function (pattern) {
return pattern.test(text);
})) {
return link;
}
}
return null;
}
function pvzMakeFallbackUrl(type) {
var pageName = '';
var title = '';
if (window.mw && mw.config && typeof mw.config.get === 'function') {
pageName = mw.config.get('wgPageName') || '';
title = mw.config.get('wgTitle') || pageName || '';
}
if (!window.mw || !mw.util || typeof mw.util.getUrl !== 'function') {
return '';
}
if (type === 'upload') return mw.util.getUrl('Special:Upload');
if (type === 'shorturl') return mw.util.getUrl('Special:UrlShortener');
if (type === 'whatlinkshere') return mw.util.getUrl('Special:WhatLinksHere/' + pageName);
if (type === 'recentchangeslinked') return mw.util.getUrl('Special:RecentChangesLinked/' + pageName);
if (type === 'printable') return mw.util.getUrl(pageName, { printable: 'yes' });
if (type === 'permalink') {
if (mw.config.get('wgRevisionId')) {
return mw.util.getUrl(pageName, { oldid: mw.config.get('wgRevisionId') });
}
return '';
}
if (type === 'info') return mw.util.getUrl(pageName, { action: 'info' });
if (type === 'history') return mw.util.getUrl(pageName, { action: 'history' });
/* 권한성 문서 도구는 기본 링크가 실제로 존재할 때만 노출한다. */
if (type === 'move') return '';
if (type === 'delete') return '';
if (type === 'protect') return '';
return title ? mw.util.getUrl(title) : '';
}
function pvzToolEntry(label, type, matchConfig) {
var found = pvzFindExistingLink(matchConfig || {});
var href = found ? found.getAttribute('href') : pvzMakeFallbackUrl(type);
if (!href) return null;
return {
label: label,
href: href,
source: found || null
};
}
function pvzMakeSpecialUrl(specialName) {
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
return mw.util.getUrl('Special:' + specialName);
}
return '/wiki/Special:' + specialName;
}
function pvzMakePageUrl(pageName) {
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
return mw.util.getUrl(pageName);
}
return '/wiki/' + String(pageName || '').replace(/\s/g, '_');
}
function pvzPageName() {
if (window.mw && mw.config && typeof mw.config.get === 'function') {
return mw.config.get('wgPageName') || '';
}
return '';
}
function pvzMainPageName() {
if (window.mw && mw.config && typeof mw.config.get === 'function') {
return mw.config.get('wgMainPageTitle') || '식물 vs 좀비 위키';
}
return '식물 vs 좀비 위키';
}
function pvzToolEntryWithFallback(label, type, matchConfig, fallbackHref) {
var found = pvzFindExistingLink(matchConfig || {});
var href = found ? found.getAttribute('href') : fallbackHref;
if (!href) return null;
return {
label: label,
href: href,
source: found || null
};
}
function pvzToolEntryFromNativeOnly(label, matchConfig) {
var found = pvzFindExistingLink(matchConfig || {});
if (!found) return null;
return {
label: label,
href: found.getAttribute('href'),
source: found
};
}
function pvzCreateToolsSvgIcon() {
var span = document.createElement('span');
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
var path = document.createElementNS(svgNS, 'path');
span.className = 'pvz-custom-mobile-tools-icon';
span.setAttribute('aria-hidden', 'true');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('focusable', 'false');
path.setAttribute('d', 'M21.2 18.6 13.9 11.3c.7-1.9.3-4.1-1.2-5.6-1.6-1.6-4-2-6-1.1l3.5 3.5-2.1 2.1-3.6-3.5c-.8 2-.4 4.4 1.2 6 1.5 1.5 3.7 1.9 5.6 1.2l7.3 7.3c.3.3.8.3 1.1 0l1.5-1.5c.3-.3.3-.8 0-1.1Z');
svg.appendChild(path);
span.appendChild(svg);
return span;
}
function pvzCreateToolsToggle(shell) {
var button = document.createElement('button');
var text = document.createElement('span');
button.type = 'button';
button.className = 'pvz-custom-mobile-tools-toggle';
button.setAttribute('aria-controls', 'pvz-custom-mobile-tools-list');
button.setAttribute('aria-expanded', 'false');
button.title = '도구';
text.className = 'pvz-custom-mobile-tools-toggle-text';
text.textContent = '도구';
button.appendChild(pvzCreateToolsSvgIcon());
button.appendChild(text);
button.addEventListener('click', function (event) {
var collapsed;
event.preventDefault();
collapsed = !shell.classList.contains('is-collapsed');
shell.classList.toggle('is-collapsed', collapsed);
shell.dataset.pvzManualToggle = '1';
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
});
return button;
}
function pvzApplyToolsCompactState(shell) {
var button;
var isCompact;
if (!shell) return;
button = shell.querySelector('.pvz-custom-mobile-tools-toggle');
isCompact = !!(window.matchMedia && window.matchMedia('(max-width: 760px)').matches);
/* 모바일에서도 보조창은 기본적으로 보이게 둔다.
사용자가 직접 도구 버튼을 눌렀을 때만 접힌 상태를 유지한다. */
if (!isCompact || shell.dataset.pvzManualToggle !== '1') {
shell.classList.remove('is-collapsed');
}
if (button) {
button.setAttribute('aria-expanded', shell.classList.contains('is-collapsed') ? 'false' : 'true');
}
}
function pvzFindLogoBlockInside(anchor) {
var candidates;
var i;
var node;
if (!anchor || !anchor.querySelectorAll) return null;
candidates = pvzArray(anchor.querySelectorAll(
'#p-logo, .mw-logo, .mw-wiki-logo, #mw-wiki-logo, ' +
'a.mw-wiki-logo, a[title*="식물 vs 좀비 위키"], a[aria-label*="식물 vs 좀비 위키"], ' +
'a[href*="식물_vs_좀비_위키"], a[href*="Main_Page"]'
));
for (i = 0; i < candidates.length; i += 1) {
node = candidates[i];
while (node && node.parentNode && node.parentNode !== anchor) {
if (
node.matches &&
node.matches('#p-logo, .mw-logo, .mw-wiki-logo, #mw-wiki-logo, .mw-portlet, nav, header, div, a')
) {
if (node.parentNode === anchor) return node;
}
node = node.parentNode;
}
if (node && node.parentNode === anchor) return node;
}
return null;
}
function pvzInsertToolsBelowLogo(anchor, shell) {
var logoBlock;
if (!anchor || !shell) return;
logoBlock = pvzFindLogoBlockInside(anchor);
if (logoBlock && logoBlock.parentNode === anchor) {
if (logoBlock.nextSibling) {
anchor.insertBefore(shell, logoBlock.nextSibling);
} else {
anchor.appendChild(shell);
}
return;
}
anchor.appendChild(shell);
}
function pvzCreateToolsSection(title, entries, sectionKey) {
var section;
var heading;
var list;
entries = (entries || []).filter(Boolean);
if (!entries.length) return null;
section = document.createElement('section');
section.className = 'pvz-custom-mobile-tools-section';
if (sectionKey) section.setAttribute('data-pvz-tools-section', sectionKey);
heading = document.createElement('h2');
heading.className = 'pvz-custom-mobile-tools-heading';
heading.textContent = title;
list = document.createElement('ul');
list.className = 'pvz-custom-mobile-tools-list';
entries.forEach(function (entry) {
var item = document.createElement('li');
var link = document.createElement('a');
var text;
item.className = 'pvz-custom-mobile-tools-item';
link.href = entry.href || '#';
if (entry.className) link.className = entry.className;
if (entry.themeMode) {
link.setAttribute('data-pvz-timeless-theme-mode', entry.themeMode);
link.setAttribute('role', 'button');
}
if (entry.icon) {
link.appendChild(pvzCreateInlineSvgIcon(entry.icon));
text = document.createElement('span');
text.textContent = entry.label;
link.appendChild(text);
} else {
link.textContent = entry.label;
}
if (entry.source) {
if (entry.source.title) link.title = entry.source.title;
if (entry.source.target) link.target = entry.source.target;
}
item.appendChild(link);
list.appendChild(item);
});
section.appendChild(heading);
section.appendChild(list);
return section;
}
function pvzFindTimelessToolsAnchor() {
var selectors = [
'#mw-site-navigation',
'#mw-related-navigation',
'#mw-content',
'#content',
'#bodyContent',
'.mw-body'
];
var i;
var node;
for (i = 0; i < selectors.length; i += 1) {
node = document.querySelector('body.skin-timeless ' + selectors[i]) ||
document.querySelector(selectors[i]);
if (node) return node;
}
return document.body;
}
function pvzHideNativeTimelessToolPortlets() {
var candidates = pvzArray(document.querySelectorAll(
'body.skin-timeless #mw-site-navigation .mw-portlet, ' +
'body.skin-timeless #mw-related-navigation .mw-portlet, ' +
'body.skin-timeless #mw-site-navigation [id^="p-"], ' +
'body.skin-timeless #mw-related-navigation [id^="p-"], ' +
'body.skin-timeless nav[id^="p-"], ' +
'body.skin-timeless div[id^="p-"], ' +
'body.skin-timeless #catlinks, ' +
'body.skin-timeless .catlinks'
));
candidates.forEach(function (node) {
var text;
var id;
if (!node || !node.classList) return;
if (node.classList.contains('pvz-custom-mobile-tools') || node.classList.contains('pvz-custom-mobile-tools-shell') || node.classList.contains('pvz-timeless-wide-rail')) return;
if (node.id === 'p-logo') return;
if (node.closest && node.closest('.pvz-custom-mobile-tools-shell')) return;
text = pvzNormalizeText(node.textContent);
id = node.id || '';
if (
/p-(navigation|namespaces|views|variants|personal|tb|wiki-tools|wikitools|page-tools|actions|cactions|more|tb-page|tb-more|managewiki|management|admin)/i.test(id) ||
/둘러보기|Navigation|관리|Manage|위키 도구|Wiki tools|Site tools|문서 도구|Page tools|Document tools|더 보기|More/.test(text) ||
/대문|최근 바뀜|임의 문서|미디어위키 도움말|특수 문서 목록|Main page|Recent changes|Random page|Help|Special pages/.test(text) ||
/이 위키의 핵심 설정|이 위키의 확장 기능|이 위키의 이름공간|이 위키의 권한|이 위키의 추가 설정|이 위키의 백업/.test(text) ||
(/파일 올리기|Upload file/.test(text) && /축약된 URL|Short URL|Get shortened URL/.test(text)) ||
(/삭제|Delete/.test(text) && /이동|Move/.test(text) && /보호|Protect/.test(text)) ||
(/여기를 가리키는 문서|What links here/.test(text) && /문서 정보|Page information/.test(text))
) {
node.classList.add('pvz-native-tools-hidden');
}
});
pvzArray(document.querySelectorAll(
'body.skin-timeless #mw-site-navigation, ' +
'body.skin-timeless #mw-related-navigation'
)).forEach(function (container) {
var hasVisibleNonCustom = false;
pvzArray(container.children).forEach(function (child) {
if (!child || !child.classList) return;
if (child.classList.contains('pvz-custom-mobile-tools-shell') || child.classList.contains('pvz-timeless-wide-rail')) return;
if (child.id === 'p-logo' || child.matches('.mw-logo, .mw-wiki-logo, #mw-wiki-logo, header')) return;
if (!child.classList.contains('pvz-native-tools-hidden')) {
if ((child.textContent || '').replace(/\s+/g, '').length > 0) {
hasVisibleNonCustom = true;
}
}
});
if (!hasVisibleNonCustom) {
container.classList.add('pvz-native-tools-container-cleaned');
}
});
}
function pvzRemoveTimelessColorStripes() {
pvzArray(document.querySelectorAll(
'body.skin-timeless #mw-header-hack, ' +
'body.skin-timeless .color-bar, ' +
'body.skin-timeless .header-color-bar, ' +
'body.skin-timeless .mw-header-color-bar, ' +
'body.skin-timeless #mw-header-hack .color-left, ' +
'body.skin-timeless #mw-header-hack .color-middle, ' +
'body.skin-timeless #mw-header-hack .color-right, ' +
'body.skin-timeless #mw-header-container .color-left, ' +
'body.skin-timeless #mw-header-container .color-middle, ' +
'body.skin-timeless #mw-header-container .color-right, ' +
'body.skin-timeless #mw-content-container .color-left, ' +
'body.skin-timeless #mw-content-container .color-middle, ' +
'body.skin-timeless #mw-content-container .color-right, ' +
'body.skin-timeless [class~="color-left"], ' +
'body.skin-timeless [class~="color-middle"], ' +
'body.skin-timeless [class~="color-right"], ' +
'body.skin-timeless [class*="color-left"], ' +
'body.skin-timeless [class*="color-middle"], ' +
'body.skin-timeless [class*="color-right"]'
)).forEach(function (node) {
if (node && node.parentNode) node.parentNode.removeChild(node);
});
}
function pvzCategoryEntriesForCustomBox() {
var entries = [];
var seen = {};
pvzArray(document.querySelectorAll('#catlinks a[href], .catlinks a[href]')).forEach(function (link) {
var label = pvzNormalizeText(link.textContent);
var href = link.getAttribute('href');
var key;
if (!label || !href) return;
if (/^(분류|Categories?|Category)$/i.test(label)) return;
key = href + '|' + label;
if (seen[key]) return;
seen[key] = true;
entries.push({
label: label,
href: href,
source: link
});
});
return entries;
}
/* =========================
커스텀 검색바 시작
- 기존 Timeless/MediaWiki 헤더 검색창 대신 독립 검색 UI를 사용한다.
- 실제 검색은 Special:Search로 제출한다.
- 검색 제안은 MediaWiki API opensearch를 사용한다.
- 1280px 이상에서는 실제 #mw-content의 위치와 폭을 그대로 따라간다.
========================= */
var pvzCustomSearchInputTimer = null;
var pvzCustomSearchRequestId = 0;
var pvzCustomSearchActiveIndex = -1;
var pvzCustomSearchComposing = false;
var pvzCustomSearchEventsBound = false;
var pvzCustomSearchViewportBound = false;
var pvzCustomSearchResizeObserver = null;
var pvzCustomSearchWidthFrame = 0;
var pvzSearchThumbnailCache = Object.create(null);
function pvzCreateSearchSvgIcon() {
var span = document.createElement('span');
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
var circle = document.createElementNS(svgNS, 'circle');
var line = document.createElementNS(svgNS, 'path');
span.className = 'pvz-custom-search-icon';
span.setAttribute('aria-hidden', 'true');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('focusable', 'false');
circle.setAttribute('cx', '10.8');
circle.setAttribute('cy', '10.8');
circle.setAttribute('r', '6.6');
circle.setAttribute('fill', 'none');
circle.setAttribute('stroke', 'currentColor');
circle.setAttribute('stroke-width', '2.2');
line.setAttribute('d', 'M15.7 15.7 21 21');
line.setAttribute('fill', 'none');
line.setAttribute('stroke', 'currentColor');
line.setAttribute('stroke-width', '2.2');
line.setAttribute('stroke-linecap', 'round');
svg.appendChild(circle);
svg.appendChild(line);
span.appendChild(svg);
return span;
}
function pvzCustomSearchElements() {
return {
root: document.getElementById('pvz-custom-searchbox'),
form: document.getElementById('pvz-custom-searchbox-form'),
input: document.getElementById('pvz-custom-searchbox-input'),
suggestions: document.getElementById('pvz-custom-searchbox-suggestions')
};
}
function pvzClearCustomSearchSuggestions() {
var elements = pvzCustomSearchElements();
pvzCustomSearchActiveIndex = -1;
if (elements.suggestions) {
elements.suggestions.textContent = '';
elements.suggestions.hidden = true;
elements.suggestions.classList.remove('is-open');
}
if (elements.input) {
elements.input.setAttribute('aria-expanded', 'false');
elements.input.removeAttribute('aria-activedescendant');
}
if (document.body) {
document.body.classList.remove('pvz-custom-search-suggestions-open');
}
}
function pvzCustomSearchSuggestionLinks() {
var elements = pvzCustomSearchElements();
if (!elements.suggestions) return [];
return pvzArray(
elements.suggestions.querySelectorAll('.pvz-custom-search-suggestion')
);
}
function pvzSetCustomSearchActiveIndex(index) {
var elements = pvzCustomSearchElements();
var links = pvzCustomSearchSuggestionLinks();
if (!links.length || !elements.input) {
pvzCustomSearchActiveIndex = -1;
return;
}
if (index < 0) index = links.length - 1;
if (index >= links.length) index = 0;
pvzCustomSearchActiveIndex = index;
links.forEach(function (link, currentIndex) {
var active = currentIndex === index;
link.classList.toggle('is-active', active);
link.setAttribute('aria-selected', active ? 'true' : 'false');
});
elements.input.setAttribute(
'aria-activedescendant',
links[index].id
);
if (typeof links[index].scrollIntoView === 'function') {
links[index].scrollIntoView({
block: 'nearest'
});
}
}
function pvzLoadSearchThumbnail(title) {
if (!title) return Promise.resolve('');
if (Object.prototype.hasOwnProperty.call(pvzSearchThumbnailCache, title)) {
return pvzSearchThumbnailCache[title];
}
if (!window.mw || !mw.loader) {
return Promise.resolve('');
}
pvzSearchThumbnailCache[title] = mw.loader.using('mediawiki.api').then(function () {
var api = new mw.Api();
return api.get({
action: 'query',
prop: 'pageimages',
piprop: 'thumbnail',
pithumbsize: 80,
titles: title,
formatversion: 2
});
}).then(function (data) {
var pages = data && data.query && data.query.pages;
var page = pages && pages[0];
return page && page.thumbnail && page.thumbnail.source ?
page.thumbnail.source :
'';
}).catch(function () {
return '';
});
return pvzSearchThumbnailCache[title];
}
function pvzAppendHighlightedSearchText(container, text, query) {
var source = String(text || '');
var needle = String(query || '');
var sourceLower;
var needleLower;
var cursor = 0;
var index;
var strong;
container.textContent = '';
if (!needle) {
container.textContent = source;
return;
}
sourceLower = source.toLocaleLowerCase();
needleLower = needle.toLocaleLowerCase();
while (cursor < source.length) {
index = sourceLower.indexOf(needleLower, cursor);
if (index === -1) {
container.appendChild(
document.createTextNode(source.slice(cursor))
);
break;
}
if (index > cursor) {
container.appendChild(
document.createTextNode(source.slice(cursor, index))
);
}
strong = document.createElement('strong');
strong.className = 'pvz-custom-search-suggestion-match';
strong.textContent = source.slice(index, index + needle.length);
container.appendChild(strong);
cursor = index + needle.length;
}
if (!source.length) {
container.textContent = '';
}
}
function pvzRenderCustomSearchSuggestions(query, data, requestId) {
var elements = pvzCustomSearchElements();
var titles;
var urls;
if (
requestId !== pvzCustomSearchRequestId ||
!elements.suggestions ||
!elements.input
) {
return;
}
if (pvzNormalizeText(elements.input.value) !== query) return;
titles = Array.isArray(data && data[1]) ? data[1] : [];
urls = Array.isArray(data && data[3]) ? data[3] : [];
elements.suggestions.textContent = '';
pvzCustomSearchActiveIndex = -1;
if (!titles.length) {
pvzClearCustomSearchSuggestions();
return;
}
titles.slice(0, 7).forEach(function (title, index) {
var link = document.createElement('a');
var thumb = document.createElement('span');
var label = document.createElement('span');
var href = urls[index] || pvzMakePageUrl(title);
link.id = 'pvz-custom-search-option-' + index;
link.className = 'pvz-custom-search-suggestion';
link.href = href;
link.setAttribute('role', 'option');
link.setAttribute('aria-selected', 'false');
thumb.className = 'pvz-custom-search-suggestion-thumb';
thumb.setAttribute('aria-hidden', 'true');
label.className = 'pvz-custom-search-suggestion-label';
pvzAppendHighlightedSearchText(label, title, query);
link.appendChild(thumb);
link.appendChild(label);
elements.suggestions.appendChild(link);
pvzLoadSearchThumbnail(title).then(function (source) {
var image;
if (!source || !thumb.isConnected) return;
image = document.createElement('img');
image.src = source;
image.alt = '';
image.loading = 'lazy';
image.decoding = 'async';
thumb.appendChild(image);
thumb.classList.add('has-image');
});
});
elements.suggestions.hidden = false;
elements.suggestions.classList.add('is-open');
elements.input.setAttribute('aria-expanded', 'true');
if (document.body) {
document.body.classList.add('pvz-custom-search-suggestions-open');
}
}
function pvzRequestCustomSearchSuggestions(query) {
var requestId;
query = pvzNormalizeText(query);
if (query.length < 1) {
pvzClearCustomSearchSuggestions();
return;
}
if (!window.mw || !mw.loader) {
pvzClearCustomSearchSuggestions();
return;
}
requestId = ++pvzCustomSearchRequestId;
mw.loader.using('mediawiki.api').then(function () {
var api = new mw.Api();
return api.get({
action: 'opensearch',
search: query,
limit: 7,
namespace: 0,
redirects: 'resolve',
format: 'json'
});
}).then(function (data) {
pvzRenderCustomSearchSuggestions(query, data, requestId);
}).catch(function () {
if (requestId === pvzCustomSearchRequestId) {
pvzClearCustomSearchSuggestions();
}
});
}
function pvzQueueCustomSearchSuggestions() {
var elements = pvzCustomSearchElements();
window.clearTimeout(pvzCustomSearchInputTimer);
if (!elements.input || pvzCustomSearchComposing) return;
pvzCustomSearchInputTimer = window.setTimeout(function () {
pvzRequestCustomSearchSuggestions(elements.input.value);
}, 170);
}
function pvzFindCustomSearchMainContent() {
return (
document.querySelector('body.skin-timeless #mw-content') ||
document.querySelector('body.skin-timeless #content') ||
document.querySelector('body.skin-timeless .mw-body')
);
}
function pvzSyncCustomSearchWidth() {
var elements = pvzCustomSearchElements();
var header = document.getElementById('mw-header-container');
var content;
var headerRect;
var contentRect;
var layoutViewportWidth = Math.round(
document.documentElement.clientWidth ||
window.innerWidth ||
0
);
if (!elements.root || !header) return;
elements.root.classList.remove('is-main-width');
elements.root.style.removeProperty('--pvz-custom-search-left');
elements.root.style.removeProperty('--pvz-custom-search-width');
if (layoutViewportWidth < 1280) return;
content = pvzFindCustomSearchMainContent();
if (!content || typeof content.getBoundingClientRect !== 'function') return;
headerRect = header.getBoundingClientRect();
contentRect = content.getBoundingClientRect();
if (contentRect.width <= 0) return;
elements.root.style.setProperty(
'--pvz-custom-search-left',
Math.round(contentRect.left - headerRect.left) + 'px'
);
elements.root.style.setProperty(
'--pvz-custom-search-width',
Math.round(contentRect.width) + 'px'
);
elements.root.classList.add('is-main-width');
}
function pvzQueueCustomSearchWidthSync() {
window.cancelAnimationFrame(pvzCustomSearchWidthFrame);
pvzCustomSearchWidthFrame = window.requestAnimationFrame(function () {
pvzSyncCustomSearchWidth();
});
}
function pvzBindCustomSearchViewportSync() {
var content;
if (pvzCustomSearchViewportBound) {
pvzQueueCustomSearchWidthSync();
return;
}
pvzCustomSearchViewportBound = true;
window.addEventListener('resize', pvzQueueCustomSearchWidthSync, {
passive: true
});
if (
window.screen &&
window.screen.orientation &&
typeof window.screen.orientation.addEventListener === 'function'
) {
window.screen.orientation.addEventListener(
'change',
pvzQueueCustomSearchWidthSync
);
}
if (
window.visualViewport &&
typeof window.visualViewport.addEventListener === 'function'
) {
window.visualViewport.addEventListener(
'resize',
pvzQueueCustomSearchWidthSync,
{ passive: true }
);
}
content = pvzFindCustomSearchMainContent();
if (window.ResizeObserver && content) {
pvzCustomSearchResizeObserver = new ResizeObserver(function () {
pvzQueueCustomSearchWidthSync();
});
pvzCustomSearchResizeObserver.observe(content);
}
}
function pvzBindCustomSearchEvents() {
var elements = pvzCustomSearchElements();
if (
pvzCustomSearchEventsBound ||
!elements.form ||
!elements.input ||
!elements.suggestions
) {
return;
}
pvzCustomSearchEventsBound = true;
elements.input.addEventListener('compositionstart', function () {
pvzCustomSearchComposing = true;
});
elements.input.addEventListener('compositionend', function () {
pvzCustomSearchComposing = false;
pvzQueueCustomSearchSuggestions();
});
elements.input.addEventListener('input', function () {
if (!pvzCustomSearchComposing) {
pvzQueueCustomSearchSuggestions();
}
});
elements.input.addEventListener('focus', function () {
if (pvzNormalizeText(elements.input.value)) {
pvzQueueCustomSearchSuggestions();
}
});
elements.input.addEventListener('keydown', function (event) {
var links = pvzCustomSearchSuggestionLinks();
if (event.key === 'ArrowDown' && links.length) {
event.preventDefault();
pvzSetCustomSearchActiveIndex(pvzCustomSearchActiveIndex + 1);
return;
}
if (event.key === 'ArrowUp' && links.length) {
event.preventDefault();
pvzSetCustomSearchActiveIndex(pvzCustomSearchActiveIndex - 1);
return;
}
if (
event.key === 'Enter' &&
pvzCustomSearchActiveIndex >= 0 &&
links[pvzCustomSearchActiveIndex]
) {
event.preventDefault();
window.location.href = links[pvzCustomSearchActiveIndex].href;
return;
}
if (event.key === 'Escape') {
pvzClearCustomSearchSuggestions();
elements.input.blur();
}
});
elements.form.addEventListener('submit', function (event) {
if (!pvzNormalizeText(elements.input.value)) {
event.preventDefault();
elements.input.focus();
}
});
elements.suggestions.addEventListener('pointerdown', function (event) {
var link = event.target && event.target.closest ?
event.target.closest('.pvz-custom-search-suggestion') :
null;
if (!link) return;
link.classList.add('is-pressed');
window.setTimeout(function () {
if (link.classList) link.classList.remove('is-pressed');
}, 140);
});
document.addEventListener('pointerdown', function (event) {
var root = document.getElementById('pvz-custom-searchbox');
if (!root || root.contains(event.target)) return;
pvzClearCustomSearchSuggestions();
}, { passive: true });
}
function pvzBuildCustomSearch() {
var header = document.getElementById('mw-header-container');
var existing = document.getElementById('pvz-custom-searchbox');
var root;
var form;
var field;
var input;
var button;
var suggestions;
if (!header) return null;
if (existing) {
pvzSyncCustomSearchWidth();
return existing;
}
root = document.createElement('div');
root.id = 'pvz-custom-searchbox';
root.className = 'pvz-custom-searchbox';
root.setAttribute('role', 'search');
form = document.createElement('form');
form.id = 'pvz-custom-searchbox-form';
form.className = 'pvz-custom-searchbox-form';
form.method = 'get';
form.action = pvzMakeSpecialUrl('Search');
field = document.createElement('div');
field.className = 'pvz-custom-searchbox-field';
input = document.createElement('input');
input.id = 'pvz-custom-searchbox-input';
input.className = 'pvz-custom-searchbox-input';
input.type = 'search';
input.name = 'search';
input.placeholder = '위키에서 검색하세요';
input.autocomplete = 'off';
input.spellcheck = false;
input.setAttribute('autocapitalize', 'off');
input.setAttribute('enterkeyhint', 'search');
input.setAttribute('aria-label', '위키에서 검색하세요');
input.setAttribute('aria-autocomplete', 'list');
input.setAttribute('aria-controls', 'pvz-custom-searchbox-suggestions');
input.setAttribute('aria-expanded', 'false');
button = document.createElement('button');
button.className = 'pvz-custom-searchbox-submit';
button.type = 'submit';
button.setAttribute('aria-label', '검색');
button.appendChild(pvzCreateSearchSvgIcon());
suggestions = document.createElement('div');
suggestions.id = 'pvz-custom-searchbox-suggestions';
suggestions.className = 'pvz-custom-searchbox-suggestions';
suggestions.setAttribute('role', 'listbox');
suggestions.setAttribute('aria-label', '검색 제안');
suggestions.hidden = true;
field.appendChild(input);
field.appendChild(button);
form.appendChild(field);
root.appendChild(form);
root.appendChild(suggestions);
header.appendChild(root);
if (document.body) {
document.body.classList.add('pvz-custom-search-ready');
}
pvzBindCustomSearchEvents();
pvzBindCustomSearchViewportSync();
pvzQueueCustomSearchWidthSync();
window.setTimeout(pvzQueueCustomSearchWidthSync, 120);
window.setTimeout(pvzQueueCustomSearchWidthSync, 420);
return root;
}
/* 커스텀 검색바 끝 */
function pvzCreateInlineSvgIcon(kind) {
var span = document.createElement('span');
var svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg');
var path = document.createElementNS(svgNS, 'path');
var paths = {
menu: 'M3 6h18v2.4H3V6Zm0 5h18v2.4H3V11Zm0 5h18v2.4H3V16Z',
tools: 'M21.2 18.6 13.9 11.3c.7-1.9.3-4.1-1.2-5.6-1.6-1.6-4-2-6-1.1l3.5 3.5-2.1 2.1-3.6-3.5c-.8 2-.4 4.4 1.2 6 1.5 1.5 3.7 1.9 5.6 1.2l7.3 7.3c.3.3.8.3 1.1 0l1.5-1.5c.3-.3.3-.8 0-1.1Z',
document: 'M6 2h9l5 5v15H6V2Zm8 1.8V8h4.2L14 3.8ZM8.5 12h7v1.8h-7V12Zm0 4h7v1.8h-7V16Z',
view: 'M6 2h9l5 5v15H6V2Zm8 1.8V8h4.2L14 3.8ZM8.5 12h7v1.8h-7V12Zm0 4h7v1.8h-7V16Z',
talk: 'M4 4h16v12H9.2L4 20V4Zm3 3v2h10V7H7Zm0 4v2h7v-2H7Z',
edit: 'M4 17.2V21h3.8L18.9 9.9l-3.8-3.8L4 17.2Zm16.2-9.6-1.8 1.8-3.8-3.8 1.8-1.8a1.2 1.2 0 0 1 1.7 0l2.1 2.1a1.2 1.2 0 0 1 0 1.7Z',
history: 'M12 4a8 8 0 1 1-7.4 5H2l4-4 4 4H6.8A5.9 5.9 0 1 0 12 6V4Zm1 4v4l3 2-1.1 1.7-3.4-2.2A1 1 0 0 1 11 12V8h2Z',
watch: 'm12 2 2.9 6.2 6.6 1-4.8 4.7 1.2 6.7-5.9-3.2-5.9 3.2 1.2-6.7-4.8-4.8 6.6-.9L12 2Z',
more: 'M6 10a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Zm6 0a2 2 0 1 1 0 4 2 2 0 0 1 0-4Z',
alerts: 'M12 2a5 5 0 0 0-5 5v2.1c0 1.1-.4 2.2-1.1 3L4.3 14a1 1 0 0 0 .7 1.7h14a1 1 0 0 0 .7-1.7l-1.6-1.9A4.8 4.8 0 0 1 17 9.1V7a5 5 0 0 0-5-5Zm0 20a3 3 0 0 0 2.8-2H9.2A3 3 0 0 0 12 22Z',
account: 'M12 12a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0 2c-5.4 0-9 2.7-9 6v1h18v-1c0-3.3-3.6-6-9-6Z',
sun: 'M12 4V1h2v3h-2Zm0 19v-3h2v3h-2ZM4.2 5.6 2.1 3.5 3.5 2.1l2.1 2.1-1.4 1.4Zm16.3 16.3-2.1-2.1 1.4-1.4 2.1 2.1-1.4 1.4ZM1 13v-2h3v2H1Zm19 0v-2h3v2h-3ZM3.5 21.9l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1ZM19.8 5.6l-1.4-1.4 2.1-2.1 1.4 1.4-2.1 2.1ZM13 7a5 5 0 1 1 0 10 5 5 0 0 1 0-10Z',
moon: 'M21 14.8A8.5 8.5 0 0 1 9.2 3a7 7 0 1 0 8.8 8.8A8.4 8.4 0 0 1 21 14.8Z'
};
span.className = 'pvz-timeless-action-icon pvz-timeless-action-icon-' + kind;
span.setAttribute('aria-hidden', 'true');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('focusable', 'false');
path.setAttribute('d', paths[kind] || paths.menu);
svg.appendChild(path);
span.appendChild(svg);
return span;
}
function pvzClampNumber(value, min, max) {
if (max < min) return min;
return Math.max(min, Math.min(value, max));
}
function pvzFindTimelessPanelButton(name) {
var selector = '.pvz-timeless-action-button[data-pvz-timeless-panel="' + name + '"], ' +
'.pvz-timeless-page-action[data-pvz-timeless-panel="' + name + '"]';
var buttons = pvzArray(document.querySelectorAll(selector));
var i;
var rect;
for (i = 0; i < buttons.length; i += 1) {
if (!buttons[i] || typeof buttons[i].getBoundingClientRect !== 'function') continue;
rect = buttons[i].getBoundingClientRect();
if (rect.width > 0 && rect.height > 0) return buttons[i];
}
return buttons[0] || null;
}
function pvzGetTimelessViewport() {
var vv = window.visualViewport;
var width = window.innerWidth || document.documentElement.clientWidth || 0;
var height = window.innerHeight || document.documentElement.clientHeight || 0;
if (!vv || typeof vv.width !== 'number' || typeof vv.height !== 'number') {
return {
left: 0,
top: 0,
width: width,
height: height,
centerX: width / 2
};
}
return {
left: typeof vv.offsetLeft === 'number' ? vv.offsetLeft : 0,
top: typeof vv.offsetTop === 'number' ? vv.offsetTop : 0,
width: vv.width || width,
height: vv.height || height,
centerX: (typeof vv.offsetLeft === 'number' ? vv.offsetLeft : 0) + ((vv.width || width) / 2)
};
}
var pvzTimelessViewportWidth = 0;
var pvzTimelessViewportHeight = 0;
var pvzTimelessPanelRefreshTimers = [];
var pvzTimelessPanelCloseTimers = new WeakMap();
function pvzResetTimelessPanelGeometry(panel) {
if (!panel || panel.nodeType !== 1) return;
panel.removeAttribute('data-pvz-panel-fixed-at-open');
panel.removeAttribute('data-pvz-panel-document-locked');
panel.removeAttribute('data-pvz-panel-document-top');
panel.removeAttribute('data-pvz-panel-document-left');
panel.removeAttribute('data-pvz-panel-document-width');
[
'position',
'top',
'left',
'right',
'width',
'max-width',
'max-height',
'overflow',
'transform',
'transform-origin',
'z-index'
].forEach(function (property) {
panel.style.removeProperty(property);
});
}
function pvzClearTimelessPanelCloseTimer(panel) {
var timer;
if (!panel) return;
timer = pvzTimelessPanelCloseTimers.get(panel);
if (!timer) return;
window.clearTimeout(timer);
pvzTimelessPanelCloseTimers.delete(panel);
}
function pvzScheduleTimelessPanelGeometryCleanup(panel) {
var timer;
if (!panel) return;
pvzClearTimelessPanelCloseTimer(panel);
timer = window.setTimeout(function () {
pvzTimelessPanelCloseTimers.delete(panel);
if (!panel.classList.contains('is-open')) {
pvzResetTimelessPanelGeometry(panel);
}
}, 240);
pvzTimelessPanelCloseTimers.set(panel, timer);
}
function pvzPositionTimelessPanel(name) {
var panel = document.getElementById('pvz-timeless-panel-' + name);
var button = pvzFindTimelessPanelButton(name);
var viewport;
var buttonRect;
var panelRect;
var panelWidth;
var center;
var minLeft;
var maxLeft;
var left;
var top;
var scrollX;
var scrollY;
if (!panel || !button || typeof button.getBoundingClientRect !== 'function') return;
viewport = pvzGetTimelessViewport();
buttonRect = button.getBoundingClientRect();
panelRect = panel.getBoundingClientRect();
panelWidth = panelRect.width || 236;
panelWidth = Math.min(panelWidth, Math.max(0, viewport.width - 16));
center = buttonRect.left + (buttonRect.width / 2);
minLeft = viewport.left + 8;
maxLeft = viewport.left + viewport.width - panelWidth - 8;
left = pvzClampNumber(center - (panelWidth / 2), minLeft, maxLeft);
top = viewport.top + buttonRect.bottom + 6;
scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0;
scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
panel.style.setProperty('position', 'absolute', 'important');
panel.style.setProperty('top', Math.round(top + scrollY) + 'px', 'important');
panel.style.setProperty('left', Math.round(left + scrollX) + 'px', 'important');
panel.style.setProperty('right', 'auto', 'important');
panel.style.setProperty('max-width', 'calc(100vw - 12px)', 'important');
panel.style.setProperty('max-height', 'none', 'important');
panel.style.setProperty('overflow', 'visible', 'important');
panel.style.setProperty('transform', 'none', 'important');
}
function pvzRepositionOpenTimelessPanel() {
var open = document.querySelector('.pvz-timeless-sheet.is-open');
var name;
if (!open || !open.id) return;
name = open.id.replace(/^pvz-timeless-panel-/, '');
if (name) pvzPositionTimelessPanel(name);
}
function pvzQueueTimelessPanelViewportRefresh() {
pvzTimelessPanelRefreshTimers.forEach(function (timer) {
window.clearTimeout(timer);
});
pvzTimelessPanelRefreshTimers = [];
window.requestAnimationFrame(function () {
pvzRepositionOpenTimelessPanel();
});
[140, 420].forEach(function (delay) {
pvzTimelessPanelRefreshTimers.push(
window.setTimeout(function () {
pvzRepositionOpenTimelessPanel();
}, delay)
);
});
}
function pvzHandleTimelessViewportResize() {
var viewport = pvzGetTimelessViewport();
var width = Math.round(viewport.width || window.innerWidth || 0);
var height = Math.round(viewport.height || window.innerHeight || 0);
var widthChanged;
var heightChanged;
var orientationChanged;
if (!pvzTimelessViewportWidth || !pvzTimelessViewportHeight) {
pvzTimelessViewportWidth = width;
pvzTimelessViewportHeight = height;
return;
}
widthChanged = Math.abs(width - pvzTimelessViewportWidth);
heightChanged = Math.abs(height - pvzTimelessViewportHeight);
orientationChanged =
(width > height) !== (pvzTimelessViewportWidth > pvzTimelessViewportHeight);
pvzTimelessViewportWidth = width;
pvzTimelessViewportHeight = height;
if (orientationChanged || widthChanged >= 80 || heightChanged >= 120) {
pvzQueueTimelessPanelViewportRefresh();
}
}
function pvzCloseTimelessPanels() {
document.documentElement.classList.remove('pvz-timeless-panel-open');
if (document.body) document.body.classList.remove('pvz-timeless-panel-open');
pvzArray(document.querySelectorAll('.pvz-timeless-sheet')).forEach(function (panel) {
panel.classList.remove('is-open');
pvzScheduleTimelessPanelGeometryCleanup(panel);
});
pvzArray(document.querySelectorAll('.pvz-timeless-action-button, .pvz-timeless-page-action[data-pvz-timeless-panel]')).forEach(function (button) {
button.classList.remove('is-active');
button.setAttribute('aria-expanded', 'false');
});
}
function pvzOpenTimelessPanel(name) {
var panel = document.getElementById('pvz-timeless-panel-' + name);
if (!panel) return;
if (name === 'alerts') pvzRefreshTimelessAlertsPanel();
pvzCloseTimelessPanels();
pvzClearTimelessPanelCloseTimer(panel);
pvzResetTimelessPanelGeometry(panel);
pvzPositionTimelessPanel(name);
document.documentElement.classList.add('pvz-timeless-panel-open');
if (document.body) document.body.classList.add('pvz-timeless-panel-open');
panel.classList.add('is-open');
pvzArray(document.querySelectorAll('.pvz-timeless-action-button, .pvz-timeless-page-action[data-pvz-timeless-panel]')).forEach(function (button) {
var active = button.getAttribute('data-pvz-timeless-panel') === name;
button.classList.toggle('is-active', active);
button.setAttribute('aria-expanded', active ? 'true' : 'false');
});
}
function pvzToggleTimelessPanel(name) {
var panel = document.getElementById('pvz-timeless-panel-' + name);
if (panel && panel.classList.contains('is-open')) {
pvzCloseTimelessPanels();
return;
}
pvzOpenTimelessPanel(name);
}
function pvzCreateTimelessActionButton(name, label, iconKind) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'pvz-timeless-action-button pvz-timeless-action-' + name;
button.setAttribute('data-pvz-timeless-panel', name);
button.setAttribute('aria-label', label);
button.setAttribute('aria-expanded', 'false');
button.title = label;
button.appendChild(pvzCreateInlineSvgIcon(iconKind));
return button;
}
function pvzGetTalkPageUrl() {
var pageName = pvzPageName();
var title = '';
if (window.mw && mw.config && typeof mw.config.get === 'function') {
title = mw.config.get('wgTitle') || pageName;
}
if (!pageName) return pvzMakePageUrl('Talk:' + title);
if (/^토론:|^Talk:/i.test(pageName)) return pvzMakePageUrl(pageName);
if (pageName.indexOf(':') === -1) return pvzMakePageUrl('토론:' + title);
return pvzMakePageUrl('토론:' + title);
}
function pvzIsCurrentPageAction(kind) {
var action = 'view';
var ns = null;
if (window.mw && mw.config && typeof mw.config.get === 'function') {
action = mw.config.get('wgAction') || 'view';
ns = mw.config.get('wgNamespaceNumber');
}
if (kind === 'view') {
return action === 'view' && !(typeof ns === 'number' && ns % 2 === 1);
}
if (kind === 'talk') {
return typeof ns === 'number' && ns % 2 === 1;
}
if (kind === 'edit') {
return action === 'edit' || action === 'submit' || action === 'visualeditor';
}
if (kind === 'history') {
return action === 'history';
}
return false;
}
function pvzCreatePageActionElement(kind, label, href, extraClass) {
var el;
if (href) {
el = document.createElement('a');
el.href = href;
} else {
el = document.createElement('button');
el.type = 'button';
}
el.className = 'pvz-timeless-page-action pvz-timeless-page-action-' + (extraClass || kind);
if (pvzIsCurrentPageAction(kind)) {
el.classList.add('is-current');
el.setAttribute('aria-current', 'page');
}
el.setAttribute('aria-label', label);
el.title = label;
el.appendChild(pvzCreateInlineSvgIcon(kind));
el.querySelector('.pvz-timeless-action-icon').classList.add('pvz-timeless-page-action-icon');
return el;
}
function pvzCreatePagePanelButton(kind, label, panelName, extraClass) {
var button = pvzCreatePageActionElement(kind, label, '', extraClass);
button.setAttribute('data-pvz-timeless-panel', panelName);
button.setAttribute('aria-expanded', 'false');
return button;
}
function pvzPageActionUrl(type) {
var found;
var pageName = pvzPageName();
if (type === 'view') {
found = pvzFindExistingLink({
ids: [/ca-view/i, /ca-nstab-main/i, /ca-nstab-project/i],
text: [/^문서$/, /^Read$/i, /^Page$/i]
});
return found ? found.getAttribute('href') : pvzMakePageUrl(pageName);
}
if (type === 'talk') {
found = pvzFindExistingLink({
ids: [/ca-talk/i, /ca-talkpage/i],
text: [/^토론$/, /^Discussion$/i, /^Talk$/i]
});
return found ? found.getAttribute('href') : pvzGetTalkPageUrl();
}
if (type === 'edit') {
found = pvzFindExistingLink({
ids: [/ca-edit/i, /ca-ve-edit/i, /ca-viewsource/i],
href: [/action=(edit|submit)/i, /veaction=edit/i],
text: [/^편집$/, /^원본 편집$/, /^Edit$/i, /^View source$/i]
});
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=edit' : '');
}
if (type === 'history') {
found = pvzFindExistingLink({
ids: [/ca-history/i],
href: [/action=history/i],
text: [/^역사$/, /^문서 역사$/, /^View history$/i, /^History$/i]
});
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=history' : '');
}
if (type === 'watch') {
found = pvzFindExistingLink({
ids: [/ca-watch/i, /ca-unwatch/i, /ca-watchlist/i],
href: [/action=(watch|unwatch)/i, /Special:(Watchlist|주시문서)/i, /특수:(주시문서|Watchlist)/i],
text: [/주시문서/, /즐겨찾기/, /^Watch$/i, /^Unwatch$/i]
});
return found ? found.getAttribute('href') : (pageName ? pvzMakePageUrl(pageName) + '?action=watch' : pvzMakeSpecialUrl('Watchlist'));
}
return '';
}
function pvzEnsureTimelessPageActions() {
var old = document.getElementById('pvz-timeless-page-actions');
var heading = document.getElementById('firstHeading') ||
document.querySelector('.mw-first-heading, .firstHeading');
var wrap = document.createElement('nav');
var left = document.createElement('div');
var right = document.createElement('div');
var watchUrl = pvzPageActionUrl('watch');
if (!heading || !heading.parentNode) return;
if (old && old.parentNode) old.parentNode.removeChild(old);
wrap.id = 'pvz-timeless-page-actions';
wrap.className = 'pvz-timeless-page-actions';
wrap.setAttribute('aria-label', '문서 작업');
left.className = 'pvz-timeless-page-actions-left';
right.className = 'pvz-timeless-page-actions-right';
left.appendChild(pvzCreatePageActionElement('view', '문서', pvzPageActionUrl('view'), 'view'));
left.appendChild(pvzCreatePageActionElement('talk', '토론', pvzPageActionUrl('talk'), 'talk'));
right.appendChild(pvzCreatePageActionElement('edit', '편집', pvzPageActionUrl('edit'), 'edit'));
right.appendChild(pvzCreatePageActionElement('history', '역사', pvzPageActionUrl('history'), 'history'));
if (watchUrl) {
right.appendChild(pvzCreatePageActionElement('watch', '즐겨찾기', watchUrl, 'watch'));
}
right.appendChild(pvzCreatePagePanelButton('more', '더보기', 'more', 'more'));
wrap.appendChild(left);
wrap.appendChild(right);
if (heading.nextSibling) {
heading.parentNode.insertBefore(wrap, heading.nextSibling);
} else {
heading.parentNode.appendChild(wrap);
}
}
function pvzEnsureTimelessActionbar() {
var bar = document.getElementById('pvz-timeless-mobile-actions');
var host = document.querySelector('body.skin-timeless #mw-header-container') ||
document.getElementById('mw-header-container') ||
document.querySelector('body.skin-timeless #mw-header') ||
document.body;
if (!host) return bar;
if (!bar) {
bar = document.createElement('div');
bar.id = 'pvz-timeless-mobile-actions';
bar.className = 'pvz-timeless-mobile-actions';
bar.setAttribute('aria-label', '식좀위키 모바일 빠른 메뉴');
}
bar.innerHTML = '';
bar.appendChild(pvzCreateTimelessActionButton('browse', '둘러보기', 'menu'));
bar.appendChild(pvzCreateTimelessActionButton('settings', '위키 도구', 'tools'));
bar.appendChild(pvzCreateTimelessActionButton('document', '문서 도구', 'document'));
bar.appendChild(pvzCreateTimelessActionButton('alerts', '알림', 'alerts'));
bar.appendChild(pvzCreateTimelessActionButton('account', '계정', 'account'));
if (bar.parentNode !== host) {
host.appendChild(bar);
}
var title = document.getElementById('pvz-timeless-mobile-title');
if (!title) {
title = document.createElement('a');
title.id = 'pvz-timeless-mobile-title';
title.href = pvzMakePageUrl(pvzMainPageName());
title.textContent = '식물 vs 좀비 위키';
title.setAttribute('aria-label', '식물 vs 좀비 위키 대문으로 이동');
}
if (title.parentNode !== host) {
host.appendChild(title);
}
return bar;
}
function pvzCreateTimelessPanel(name, label, sections) {
var old = document.getElementById('pvz-timeless-panel-' + name);
var panel = document.createElement('section');
var body = document.createElement('div');
if (old && old.parentNode) old.parentNode.removeChild(old);
panel.id = 'pvz-timeless-panel-' + name;
panel.className = 'pvz-timeless-sheet pvz-timeless-sheet-' + name;
panel.setAttribute('aria-label', label);
body.className = 'pvz-timeless-sheet-body';
(sections || []).filter(Boolean).forEach(function (section) {
body.appendChild(section);
});
panel.appendChild(body);
document.body.appendChild(panel);
}
function pvzEnsureTimelessPanelBackdrop() {
var old = document.getElementById('pvz-timeless-panel-backdrop');
if (old) return old;
old = document.createElement('div');
old.id = 'pvz-timeless-panel-backdrop';
old.className = 'pvz-timeless-panel-backdrop';
document.body.appendChild(old);
return old;
}
function pvzStoredColorMode() {
try {
return localStorage.getItem('pvz-timeless-color-mode') || '';
} catch (e) {
return '';
}
}
function pvzCurrentColorMode() {
var stored = pvzStoredColorMode();
var root = document.documentElement;
if (stored === 'light' || stored === 'dark') return stored;
if (root && root.classList && (
root.classList.contains('theme-dark') ||
root.classList.contains('dark') ||
root.classList.contains('client-dark-mode') ||
root.classList.contains('mw-theme-night') ||
root.classList.contains('skin-theme-clientpref-night')
)) {
return 'dark';
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
function pvzApplyTimelessColorMode(mode, persist) {
var root = document.documentElement;
var body = document.body;
var add;
var remove;
if (mode !== 'light' && mode !== 'dark') mode = 'light';
if (typeof persist === 'undefined') persist = true;
if (persist) {
try {
localStorage.setItem('pvz-timeless-color-mode', mode);
} catch (e) {}
}
add = mode === 'dark'
? ['theme-dark', 'dark', 'client-dark-mode', 'mw-theme-night', 'skin-theme-clientpref-night']
: ['theme-light', 'light', 'client-light-mode', 'skin-theme-clientpref-day'];
remove = mode === 'dark'
? ['theme-light', 'light', 'client-light-mode', 'skin-theme-clientpref-day']
: ['theme-dark', 'dark', 'client-dark-mode', 'mw-theme-night', 'skin-theme-clientpref-night'];
[root, body].forEach(function (node) {
if (!node || !node.classList) return;
remove.forEach(function (name) { node.classList.remove(name); });
add.forEach(function (name) { node.classList.add(name); });
});
}
var pvzSystemThemeMedia = null;
var pvzSystemThemeListenerBound = false;
function pvzHasStoredColorMode() {
var stored = pvzStoredColorMode();
return stored === 'light' || stored === 'dark';
}
function pvzApplyInitialTimelessColorMode() {
var stored = pvzStoredColorMode();
var mode = pvzCurrentColorMode();
pvzApplyTimelessColorMode(mode, stored === 'light' || stored === 'dark');
}
function pvzBindSystemThemeListener() {
if (
pvzSystemThemeListenerBound ||
!window.matchMedia
) {
return;
}
pvzSystemThemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
pvzSystemThemeListenerBound = true;
function handleSystemThemeChange(event) {
if (pvzHasStoredColorMode()) return;
pvzApplyTimelessColorMode(event.matches ? 'dark' : 'light', false);
window.setTimeout(function () {
if (typeof pvzBuildCustomMobileTools === 'function') {
pvzBuildCustomMobileTools();
}
}, 0);
}
if (typeof pvzSystemThemeMedia.addEventListener === 'function') {
pvzSystemThemeMedia.addEventListener('change', handleSystemThemeChange);
} else if (typeof pvzSystemThemeMedia.addListener === 'function') {
pvzSystemThemeMedia.addListener(handleSystemThemeChange);
}
}
function pvzThemeToggleEntry() {
var current = pvzCurrentColorMode();
if (current === 'dark') {
return {
label: '라이트 모드',
href: '#',
icon: 'sun',
className: 'pvz-timeless-theme-toggle',
themeMode: 'light'
};
}
return {
label: '다크 모드',
href: '#',
icon: 'moon',
className: 'pvz-timeless-theme-toggle',
themeMode: 'dark'
};
}
/* =========================
계정 패널 사용자 정보 시작
- 현재 로그인 사용자의 명시적 직책, 편집 횟수, 가입일을 Action API에서 불러온다.
========================= */
var pvzAccountInfoPromise = null;
function pvzAccountRoleLabel(group) {
var labels = {
'bureaucrat': '사무관',
'interface-admin': '인터페이스 관리자',
'sysop': '관리자',
'patroller': '순찰자',
'rollbacker': '되돌리기 사용자',
'autopatrolled': '자동 순찰 면제 사용자',
'bot': '봇',
'confirmed': '인증된 사용자',
'checkuser': '검사관',
'suppress': '기록보호자',
'oversight': '기록보호자',
'steward': '스튜어드',
'translator': '번역자',
'translationadmin': '번역 관리자'
};
return labels[group] || group;
}
function pvzAccountExplicitRoles(userinfo) {
var memberships = userinfo && userinfo.groupmemberships;
var groups = userinfo && userinfo.groups;
var names = [];
if (Array.isArray(memberships) && memberships.length) {
memberships.forEach(function (membership) {
if (membership && membership.group) names.push(membership.group);
});
} else if (Array.isArray(groups)) {
groups.forEach(function (group) {
if (group && group !== '*' && group !== 'user' && group !== 'autoconfirmed') {
names.push(group);
}
});
}
names = names.filter(function (group, index, list) {
return group && list.indexOf(group) === index;
});
if (!names.length) return '일반 사용자';
return names.map(pvzAccountRoleLabel).join(' · ');
}
function pvzFormatAccountRegistrationDate(value) {
var date;
var formatter;
var parts;
var data = {};
if (!value) return '확인할 수 없음';
date = new Date(value);
if (isNaN(date.getTime())) return '확인할 수 없음';
try {
formatter = new Intl.DateTimeFormat('ko-KR', {
timeZone: 'Asia/Seoul',
year: 'numeric',
month: 'numeric',
day: 'numeric',
weekday: 'short'
});
parts = formatter.formatToParts(date);
parts.forEach(function (part) {
if (part.type !== 'literal') data[part.type] = part.value;
});
return (
data.year + '년 ' +
data.month + '월 ' +
data.day + '일 (' +
data.weekday + ')'
);
} catch (e) {
return date.getFullYear() + '년 ' +
(date.getMonth() + 1) + '월 ' +
date.getDate() + '일';
}
}
function pvzLoadAccountInfo() {
if (pvzAccountInfoPromise) return pvzAccountInfoPromise;
if (!window.mw || !mw.loader || !mw.config || !mw.config.get('wgUserName')) {
return Promise.resolve(null);
}
pvzAccountInfoPromise = mw.loader.using('mediawiki.api').then(function () {
var api = new mw.Api();
return api.get({
action: 'query',
meta: 'userinfo',
uiprop: 'groups|groupmemberships|editcount|registrationdate',
formatversion: 2
});
}).then(function (data) {
return data && data.query ? data.query.userinfo : null;
}).catch(function () {
return null;
});
return pvzAccountInfoPromise;
}
function pvzCreateAccountInfoRow(label, value) {
var row = document.createElement('div');
var key = document.createElement('span');
var text = document.createElement('span');
row.className = 'pvz-timeless-account-info-row';
key.className = 'pvz-timeless-account-info-label';
key.textContent = label;
text.className = 'pvz-timeless-account-info-value';
text.textContent = value;
row.appendChild(key);
row.appendChild(text);
return row;
}
function pvzCreateAccountInfoBlock(username) {
var box = document.createElement('div');
box.className = 'pvz-timeless-account-info';
box.setAttribute('aria-label', username + ' 계정 정보');
box.appendChild(pvzCreateAccountInfoRow('직책', '불러오는 중…'));
box.appendChild(pvzCreateAccountInfoRow('편집', '불러오는 중…'));
box.appendChild(pvzCreateAccountInfoRow('가입일', '불러오는 중…'));
pvzLoadAccountInfo().then(function (userinfo) {
var rows;
var editcount;
if (!box.isConnected || !userinfo) return;
rows = box.querySelectorAll('.pvz-timeless-account-info-value');
editcount = Number(userinfo.editcount);
if (rows[0]) rows[0].textContent = pvzAccountExplicitRoles(userinfo);
if (rows[1]) {
rows[1].textContent = isNaN(editcount) ?
'확인할 수 없음' :
editcount.toLocaleString('ko-KR') + '회';
}
if (rows[2]) rows[2].textContent = pvzFormatAccountRegistrationDate(userinfo.registrationdate);
});
return box;
}
/* 계정 패널 사용자 정보 끝 */
function pvzAccountSection() {
var username = window.mw && mw.config && typeof mw.config.get === 'function' ? mw.config.get('wgUserName') : null;
var entries;
var section;
var list;
if (username) {
entries = [
{ label: username, href: pvzMakePageUrl('User:' + username) },
{ label: '사용자 토론', href: pvzMakePageUrl('User talk:' + username) },
{ label: '주시문서 목록', href: pvzMakeSpecialUrl('Watchlist') },
{ label: '기여', href: pvzMakeSpecialUrl('Contributions/' + username) },
{ label: '환경설정', href: pvzMakeSpecialUrl('Preferences') },
{ label: '로그아웃', href: pvzMakeSpecialUrl('UserLogout') },
pvzThemeToggleEntry()
];
} else {
entries = [
{ label: '로그인', href: pvzMakeSpecialUrl('UserLogin') },
{ label: '회원 가입', href: pvzMakeSpecialUrl('CreateAccount') },
pvzThemeToggleEntry()
];
}
section = pvzCreateToolsSection('계정', entries, 'account');
if (section && username) {
list = section.querySelector('.pvz-custom-mobile-tools-list');
section.insertBefore(pvzCreateAccountInfoBlock(username), list || null);
}
return section;
}
function pvzNativeNotificationCount() {
var nodes = pvzArray(document.querySelectorAll(
'.mw-echo-notifications-badge, .mw-echo-notification-badge, ' +
'#pt-notifications-alert .mw-echo-notifications-badge, ' +
'#pt-notifications-notice .mw-echo-notifications-badge, ' +
'#pt-notifications-alert [data-counter-num], ' +
'#pt-notifications-notice [data-counter-num]'
));
var count = 0;
nodes.forEach(function (node) {
var raw = '';
var value;
if (!node) return;
raw = node.getAttribute('data-counter-num') || node.getAttribute('data-counter-text') || node.getAttribute('aria-label') || node.textContent || '';
value = parseInt(String(raw).replace(/[^0-9]/g, ''), 10);
if (!isNaN(value)) count += value;
});
return count;
}
function pvzCollectNotificationEntries() {
var entries = [];
var seen = {};
var selectors = [
'.mw-echo-ui-notificationItemWidget a[href]',
'.mw-echo-notification a[href]',
'.mw-echo-notifications a[href]',
'.mw-notification a[href]',
'.mw-notifications a[href]',
'#pt-notifications-alert a[href]',
'#pt-notifications-notice a[href]'
];
pvzArray(document.querySelectorAll(selectors.join(', '))).forEach(function (link) {
var label = pvzNormalizeText(link.textContent || link.getAttribute('title') || link.getAttribute('aria-label') || '');
var href = link.getAttribute('href');
var key;
if (!href) return;
if (!label || /^(알림|공지|Notifications?|Alerts?|Notices?)$/i.test(label)) return;
key = href + '|' + label;
if (seen[key]) return;
seen[key] = true;
entries.push({
label: label,
href: href,
source: link
});
});
return entries.slice(0, 8);
}
function pvzCreateAlertEmptySection() {
var section = document.createElement('section');
var heading = document.createElement('h2');
var list = document.createElement('ul');
var item = document.createElement('li');
var empty = document.createElement('span');
section.className = 'pvz-custom-mobile-tools-section pvz-timeless-alert-section';
section.setAttribute('data-pvz-tools-section', 'alerts');
heading.className = 'pvz-custom-mobile-tools-heading';
heading.textContent = '알림';
list.className = 'pvz-custom-mobile-tools-list';
item.className = 'pvz-custom-mobile-tools-item';
empty.className = 'pvz-timeless-alert-empty';
empty.textContent = '새 알림이 없습니다.';
item.appendChild(empty);
list.appendChild(item);
section.appendChild(heading);
section.appendChild(list);
return section;
}
function pvzAlertsSection() {
var entries = pvzCollectNotificationEntries();
var count = pvzNativeNotificationCount();
if (!entries.length && count < 1) {
return pvzCreateAlertEmptySection();
}
if (!entries.length && count > 0) {
entries.push({ label: '알림 확인하기', href: pvzMakeSpecialUrl('Notifications') });
}
entries.push({ label: '전체 알림', href: pvzMakeSpecialUrl('Notifications') });
entries.push({ label: '환경설정', href: pvzMakeSpecialUrl('Preferences') });
return pvzCreateToolsSection('알림', entries, 'alerts');
}
function pvzRefreshTimelessAlertsPanel() {
var panel = document.getElementById('pvz-timeless-panel-alerts');
var body = panel && panel.querySelector('.pvz-timeless-sheet-body');
if (!body) return;
body.innerHTML = '';
body.appendChild(pvzAlertsSection());
}
function pvzBuildTimelessWideRail(side, sections, includeLogo) {
var rail = document.createElement('aside');
var logo;
var img;
rail.className = 'pvz-timeless-wide-rail pvz-timeless-wide-rail-' + side;
rail.setAttribute('aria-label', side === 'left' ? '왼쪽 보조창' : '오른쪽 보조창');
if (includeLogo) {
logo = document.createElement('a');
logo.className = 'pvz-timeless-wide-logo';
logo.href = pvzMakePageUrl(pvzMainPageName());
logo.title = '식물 vs 좀비 위키';
logo.setAttribute('aria-label', '식물 vs 좀비 위키로 이동');
img = document.createElement('img');
img.src = '/wiki/Special:Redirect/file/PvZKR_Wiki_Logo2.png';
img.alt = 'Plants vs. Zombies 위키';
img.loading = 'eager';
logo.appendChild(img);
rail.appendChild(logo);
}
(sections || []).filter(Boolean).forEach(function (section) {
rail.appendChild(section);
});
return rail;
}
function pvzBuildTimelessWideTools(navEntries, wikiEntries, moreEntries, manageEntries, categoryEntries) {
var leftHost = document.querySelector('body.skin-timeless #mw-site-navigation') || document.getElementById('mw-site-navigation');
var rightHost = document.querySelector('body.skin-timeless #mw-related-navigation') || document.getElementById('mw-related-navigation');
var leftRail;
var rightRail;
pvzArray(document.querySelectorAll('.pvz-timeless-wide-rail')).forEach(function (node) {
if (node && node.parentNode) node.parentNode.removeChild(node);
});
if (!leftHost && !rightHost) return;
leftRail = pvzBuildTimelessWideRail('left', [
pvzCreateToolsSection('둘러보기', navEntries, 'navigation'),
pvzCreateToolsSection('위키 도구', wikiEntries, 'wiki')
], true);
rightRail = pvzBuildTimelessWideRail('right', [
pvzCreateToolsSection('문서 도구', moreEntries, 'document'),
pvzCreateToolsSection('분류', categoryEntries, 'category'),
pvzCreateToolsSection('위키 관리', manageEntries, 'management')
], false);
if (leftHost) leftHost.appendChild(leftRail);
else document.body.insertBefore(leftRail, document.body.firstChild);
if (rightHost) rightHost.appendChild(rightRail);
else document.body.appendChild(rightRail);
}
function pvzBuildMobileCategoryBox(categorySection) {
var old = document.querySelector('.pvz-mobile-category-box');
var box;
var content;
if (old && old.parentNode) old.parentNode.removeChild(old);
if (!categorySection) return;
box = document.createElement('div');
box.className = 'pvz-mobile-category-box';
box.appendChild(categorySection);
content = document.getElementById('mw-content-text') || document.querySelector('#bodyContent, .mw-body-content, #mw-content, #content');
if (content && content.parentNode) {
if (content.nextSibling) content.parentNode.insertBefore(box, content.nextSibling);
else content.parentNode.appendChild(box);
} else {
document.body.appendChild(box);
}
}
function pvzBindTimelessPanelEvents() {
if (window.PVZ_TIMELESS_PANEL_EVENTS_BOUND) return;
window.PVZ_TIMELESS_PANEL_EVENTS_BOUND = true;
document.addEventListener('click', function (event) {
var themeToggle = event.target && event.target.closest ? event.target.closest('[data-pvz-timeless-theme-mode]') : null;
var button = event.target && event.target.closest ? event.target.closest('.pvz-timeless-action-button[data-pvz-timeless-panel], .pvz-timeless-page-action[data-pvz-timeless-panel]') : null;
var panel = event.target && event.target.closest ? event.target.closest('.pvz-timeless-sheet') : null;
var backdrop = event.target && event.target.closest ? event.target.closest('#pvz-timeless-panel-backdrop') : null;
if (themeToggle) {
event.preventDefault();
event.stopPropagation();
pvzApplyTimelessColorMode(themeToggle.getAttribute('data-pvz-timeless-theme-mode'));
pvzCloseTimelessPanels();
window.setTimeout(pvzBuildCustomMobileTools, 0);
return;
}
if (button) {
event.preventDefault();
event.stopPropagation();
pvzToggleTimelessPanel(button.getAttribute('data-pvz-timeless-panel'));
return;
}
if (backdrop) {
event.preventDefault();
pvzCloseTimelessPanels();
return;
}
if (!panel) pvzCloseTimelessPanels();
}, true);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') pvzCloseTimelessPanels();
});
pvzHandleTimelessViewportResize();
window.addEventListener('resize', function () {
pvzHandleTimelessViewportResize();
}, { passive: true });
if (
window.screen &&
window.screen.orientation &&
typeof window.screen.orientation.addEventListener === 'function'
) {
window.screen.orientation.addEventListener('change', function () {
pvzQueueTimelessPanelViewportRefresh();
});
} else {
window.addEventListener('orientationchange', function () {
pvzQueueTimelessPanelViewportRefresh();
}, { passive: true });
}
if (window.visualViewport && typeof window.visualViewport.addEventListener === 'function') {
window.visualViewport.addEventListener('resize', function () {
pvzHandleTimelessViewportResize();
}, { passive: true });
/* visualViewport.scroll은 등록하지 않는다.
주소창 접힘/스크롤만으로 패널 좌표를 계속 재계산하지 않는다. */
}
}
function pvzBuildCustomMobileTools() {
var oldShell = document.querySelector('.pvz-custom-mobile-tools-shell, .pvz-custom-mobile-tools');
var oldCategory = document.querySelector('.pvz-mobile-category-box');
var navSection;
var manageSection;
var wikiSection;
var moreSection;
var documentSection;
var alertsSection;
var categorySection;
var createdKeys = {};
var navEntries;
var manageEntries;
var wikiEntries;
var moreEntries;
var categoryEntries;
function unique(entry) {
var key;
if (!entry) return null;
key = entry.href + '|' + entry.label;
if (createdKeys[key]) return null;
createdKeys[key] = true;
return entry;
}
if (!pvzIsTimeless() || !document.body) return;
document.body.classList.remove('pvz-custom-mobile-tools-ready');
if (oldShell && oldShell.parentNode) oldShell.parentNode.removeChild(oldShell);
if (oldCategory && oldCategory.parentNode) oldCategory.parentNode.removeChild(oldCategory);
pvzArray(document.querySelectorAll('.pvz-timeless-wide-rail')).forEach(function (node) {
if (node && node.parentNode) node.parentNode.removeChild(node);
});
navEntries = [
unique(pvzToolEntryWithFallback('대문', 'mainpage', {
ids: [/n-mainpage/i, /n-main-page/i],
href: [/\/wiki\/(%EC%8B%9D%EB%AC%BC_vs_%EC%A2%80%EB%B9%84_%EC%9C%84%ED%82%A4|식물_vs_좀비_위키|Main_Page)/i],
text: [/^대문$/, /^Main page$/i]
}, pvzMakePageUrl(pvzMainPageName()))),
unique(pvzToolEntryWithFallback('최근 바뀜', 'recentchanges', {
ids: [/n-recentchanges/i],
href: [/Special:(RecentChanges|최근바뀜)/i, /특수:(최근바뀜|RecentChanges)/i],
text: [/^최근 바뀜$/, /^Recent changes$/i]
}, pvzMakeSpecialUrl('RecentChanges'))),
unique(pvzToolEntryWithFallback('미디어위키 도움말', 'help', {
ids: [/n-help/i],
href: [/mediawiki\.org.*Help/i, /Help:Contents/i],
text: [/미디어위키 도움말/, /^도움말$/, /MediaWiki help/i, /^Help$/i]
}, 'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents')),
unique(pvzToolEntryWithFallback('특수 문서 목록', 'specialpages', {
ids: [/n-specialpages/i, /t-specialpages/i],
href: [/Special:(SpecialPages|특수문서)/i, /특수:(특수문서|SpecialPages)/i],
text: [/특수 문서 목록/, /Special pages/i]
}, pvzMakeSpecialUrl('SpecialPages'))),
unique(pvzToolEntryWithFallback('무작위 문서', 'random', {
ids: [/n-randompage/i, /n-random/i],
href: [/Special:(Random|RandomPage|임의문서)/i, /특수:(임의문서|Random|RandomPage)/i],
text: [/임의 문서/, /무작위 문서/, /Random page/i]
}, pvzMakeSpecialUrl('Random')))
].filter(Boolean);
moreEntries = [
unique(pvzToolEntry('여기를 가리키는 문서', 'whatlinkshere', {
ids: [/t-whatlinkshere/i],
href: [/Special:(WhatLinksHere|가리키는문서)/i, /특수:(가리키는문서|WhatLinksHere)/i],
text: [/여기를 가리키는 문서/, /What links here/i]
})),
unique(pvzToolEntry('가리키는 글의 최근 바뀜', 'recentchangeslinked', {
ids: [/t-recentchangeslinked/i],
href: [/Special:(RecentChangesLinked|가리키는글의최근바뀜)/i, /특수:(가리키는글의최근바뀜|RecentChangesLinked)/i],
text: [/가리키는 글의 최근 바뀜/, /Related changes/i]
})),
unique(pvzToolEntry('고유 링크', 'permalink', {
ids: [/t-permalink/i],
href: [/oldid=/i],
text: [/고유 링크/, /Permanent link/i]
})),
unique(pvzToolEntry('문서 정보', 'info', {
ids: [/t-info/i, /ca-info/i],
href: [/action=info/i],
text: [/문서 정보/, /Page information/i]
})),
unique(pvzToolEntry('문서 기록', 'history', {
ids: [/ca-history/i],
href: [/action=history/i],
text: [/문서 역사/, /문서 기록/, /^역사$/, /^History$/i, /^View history$/i]
}))
].filter(Boolean);
wikiEntries = [
unique(pvzToolEntry('파일 올리기', 'upload', {
ids: [/t-upload/i, /pt-upload/i],
href: [/Special:(Upload|올리기)/i, /특수:(올리기|Upload)/i],
text: [/^파일 올리기$/, /^Upload file$/i]
})),
unique(pvzToolEntry('축약된 URL 얻기', 'shorturl', {
ids: [/t-urlshortener/i, /t-shorturl/i],
href: [/Special:(UrlShortener|ShortUrl|URLShortener)/i, /특수:(UrlShortener|ShortUrl|축약)/i],
text: [/축약된 URL/, /Short URL/i, /Get shortened URL/i]
}))
].filter(Boolean);
manageEntries = [
unique(pvzToolEntryWithFallback('특수 문서', 'specialpages', {
href: [/Special:(SpecialPages|특수문서)/i, /특수:(SpecialPages|특수문서)/i],
text: [/^특수 문서$/, /^Special pages$/i]
}, pvzMakeSpecialUrl('SpecialPages'))),
unique(pvzToolEntryWithFallback('핵심 설정 관리', 'managewiki-core', {
href: [/Special:ManageWiki(?:\/core)?(?:[/?#]|$)/i, /특수:ManageWiki(?:\/core)?(?:[/?#]|$)/i],
text: [/이 위키의 핵심 설정을 관리하기/, /Manage this wiki's core settings/i]
}, pvzMakeSpecialUrl('ManageWiki/core'))),
unique(pvzToolEntryWithFallback('확장 기능 관리', 'managewiki-extensions', {
href: [/Special:ManageWiki\/extensions/i, /특수:ManageWiki\/extensions/i],
text: [/이 위키의 확장 기능을 관리하기/, /Manage this wiki's extensions/i]
}, pvzMakeSpecialUrl('ManageWiki/extensions'))),
unique(pvzToolEntryWithFallback('권한 관리', 'managewiki-permissions', {
href: [/Special:ManageWiki\/permissions/i, /특수:ManageWiki\/permissions/i],
text: [/이 위키의 권한 관리/, /Manage this wiki's permissions/i]
}, pvzMakeSpecialUrl('ManageWiki/permissions'))),
unique(pvzToolEntryWithFallback('이름공간 관리', 'managewiki-namespaces', {
href: [/Special:ManageWiki\/namespaces/i, /특수:ManageWiki\/namespaces/i],
text: [/이 위키의 이름공간 관리/, /Manage this wiki's namespaces/i]
}, pvzMakeSpecialUrl('ManageWiki/namespaces'))),
unique(pvzToolEntryWithFallback('백업 관리', 'datadump', {
href: [/Special:(DataDump|ManageWiki\/backup|ManageWiki\/backups)/i, /특수:(DataDump|ManageWiki\/backup|ManageWiki\/backups)/i],
text: [/이 위키의 백업을 관리/, /Manage this wiki's backups/i, /^DataDump$/i]
}, pvzMakeSpecialUrl('DataDump')))
].filter(Boolean);
categoryEntries = pvzCategoryEntriesForCustomBox().map(unique).filter(Boolean);
navSection = pvzCreateToolsSection('둘러보기', navEntries, 'navigation');
moreSection = pvzCreateToolsSection('더 보기', moreEntries, 'more');
documentSection = pvzCreateToolsSection('문서 도구', moreEntries, 'document');
wikiSection = pvzCreateToolsSection('위키 도구', wikiEntries, 'wiki');
manageSection = pvzCreateToolsSection('위키 관리', manageEntries, 'management');
alertsSection = pvzAlertsSection();
categorySection = pvzCreateToolsSection('분류', categoryEntries, 'category');
pvzEnsureTimelessPageActions();
pvzEnsureTimelessActionbar();
pvzEnsureTimelessPanelBackdrop();
pvzCreateTimelessPanel('browse', '둘러보기', [navSection]);
pvzCreateTimelessPanel('settings', '위키 도구', [wikiSection, manageSection]);
pvzCreateTimelessPanel('document', '문서 도구', [documentSection]);
pvzCreateTimelessPanel('more', '더 보기', [moreSection]);
pvzCreateTimelessPanel('alerts', '알림', [alertsSection]);
pvzCreateTimelessPanel('account', '계정', [pvzAccountSection()]);
pvzBuildMobileCategoryBox(categorySection);
pvzBuildTimelessWideTools(navEntries, wikiEntries, moreEntries, manageEntries, categoryEntries);
pvzBindTimelessPanelEvents();
document.body.classList.add('pvz-custom-mobile-tools-ready');
document.body.classList.add('pvz-timeless-actions-ready');
pvzHideNativeTimelessToolPortlets();
pvzRemoveTimelessColorStripes();
}
function pvzStabilizeCustomToc() {
var toc = document.querySelector('.pvz-custom-toc');
var list = toc && toc.querySelector('.pvz-custom-toc-list');
if (!toc || !list || toc.classList.contains('is-collapsed')) return;
toc.classList.add('pvz-toc-reflowing');
window.requestAnimationFrame(function () {
var width;
toc.classList.remove('pvz-toc-reflowing');
/*
첫 렌더링 때 Timeless/폰트 로딩/float 계산이 겹치면
목차 항목의 max-content 폭이 너무 좁게 잡히는 경우가 있다.
offsetWidth를 한 번 읽고 클래스를 붙여 강제로 레이아웃을 재계산한다.
*/
width = toc.offsetWidth;
toc.style.setProperty('--pvz-toc-measured-width', width + 'px');
toc.classList.add('pvz-toc-layout-ready');
});
}
function pvzQueueTocStabilize() {
pvzStabilizeCustomToc();
window.setTimeout(pvzStabilizeCustomToc, 80);
window.setTimeout(pvzStabilizeCustomToc, 250);
window.setTimeout(pvzStabilizeCustomToc, 700);
if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') {
document.fonts.ready.then(function () {
pvzStabilizeCustomToc();
});
}
}
function pvzCreateCustomToc() {
var contentText = document.getElementById('mw-content-text');
var parserOutput = contentText && (
contentText.querySelector('.mw-parser-output') || contentText
);
var leadContainer = parserOutput && (
parserOutput.querySelector(':scope > .mf-section-0') ||
parserOutput.querySelector(':scope > section.mf-section-0') ||
parserOutput
);
var existingWrap;
var nativeTocs;
var headings;
var leadParagraph;
var wrap;
var toc;
var head;
var title;
var toggle;
var list;
var counters = [0, 0, 0, 0, 0];
function getDepth(heading) {
var level = parseInt(heading.tagName.replace('H', ''), 10);
var depth = level - 1;
if (depth < 1) depth = 1;
if (depth > 5) depth = 5;
return depth;
}
function getNumber(depth) {
var i;
var parts = [];
counters[depth - 1] += 1;
for (i = depth; i < counters.length; i += 1) {
counters[i] = 0;
}
for (i = 0; i < depth; i += 1) {
if (!counters[i]) counters[i] = 1;
parts.push(counters[i]);
}
return parts.join('.');
}
function isExcludedLeadContainer(element) {
if (!element || !element.matches) return false;
return element.matches(
'table, figure, .thumb, .tright, .tleft, .floatright, .floatleft, .floatnone, ' +
'.infobox, .pvz-cp-infobox, .pvz-character-infobox, .character-infobox, ' +
'.navbox, .metadata, .catlinks, .mw-references-wrap, ' +
'.pvz-message, .pvz-custom-toc-wrap, #toc, .toc'
);
}
function isLeadParagraph(element) {
var clone;
var text;
if (
!element ||
element.parentNode !== leadContainer ||
element.tagName !== 'P'
) {
return false;
}
if (isExcludedLeadContainer(element)) return false;
if (
element.querySelector(
'style, script, template, noscript, ' +
'link[rel="mw-deduplicated-inline-style"], ' +
'[data-mw-deduplicate]'
)
) {
return false;
}
clone = element.cloneNode(true);
clone.querySelectorAll(
'figure, .thumb, .mw-file-description, .mw-default-size, ' +
'.mw-image-border, .reference, sup, .mw-editsection, ' +
'style, script, template, noscript, link, [data-mw-deduplicate]'
).forEach(function (node) {
node.remove();
});
text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
if (!text) return false;
if (
/^(?:\.mw-parser-output\b|@media\b|@supports\b|@layer\b|:root\b)/.test(text) &&
/[{};]/.test(text)
) {
return false;
}
return true;
}
function findLeadParagraph() {
var children;
var i;
var child;
if (!leadContainer) return null;
children = pvzArray(leadContainer.children);
for (i = 0; i < children.length; i += 1) {
child = children[i];
if (isExcludedLeadContainer(child)) continue;
if (isLeadParagraph(child)) {
return child;
}
}
return null;
}
function placeTocAfterLead(tocWrap, paragraph) {
if (
!tocWrap ||
!paragraph ||
paragraph.parentNode !== leadContainer
) {
return false;
}
if (
tocWrap.parentNode === leadContainer &&
paragraph.nextSibling === tocWrap
) {
return true;
}
leadContainer.insertBefore(tocWrap, paragraph.nextSibling);
return true;
}
if (
!parserOutput ||
!leadContainer ||
parserOutput.querySelector('.pvz-no-custom-toc')
) {
return;
}
leadParagraph = findLeadParagraph();
if (!leadParagraph) return;
existingWrap = parserOutput.querySelector('.pvz-custom-toc-wrap');
if (existingWrap) {
placeTocAfterLead(existingWrap, leadParagraph);
pvzQueueTocStabilize();
return;
}
headings = pvzArray(
parserOutput.querySelectorAll('h2, h3, h4, h5, h6')
).filter(function (heading) {
return !pvzSkipHeading(heading);
});
if (headings.length < 2) return;
nativeTocs = pvzArray(
parserOutput.querySelectorAll('#toc, .toc')
).filter(function (node) {
return node && !(
node.classList &&
node.classList.contains('pvz-custom-toc')
);
});
nativeTocs.forEach(function (old) {
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
});
wrap = document.createElement('div');
wrap.className = 'pvz-custom-toc-wrap';
toc = document.createElement('nav');
toc.className = 'pvz-custom-toc';
toc.setAttribute('aria-label', '목차');
head = document.createElement('div');
head.className = 'pvz-custom-toc-head';
title = document.createElement('div');
title.className = 'pvz-custom-toc-title';
title.textContent = '목차';
toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'pvz-custom-toc-toggle';
toggle.setAttribute('aria-expanded', 'true');
toggle.textContent = '접기';
toggle.addEventListener('click', function (event) {
var collapsed;
event.preventDefault();
collapsed = !toc.classList.contains('is-collapsed');
toc.classList.toggle('is-collapsed', collapsed);
toggle.setAttribute(
'aria-expanded',
collapsed ? 'false' : 'true'
);
toggle.textContent = collapsed ? '펼치기' : '접기';
});
head.appendChild(title);
head.appendChild(toggle);
list = document.createElement('ol');
list.className = 'pvz-custom-toc-list';
headings.forEach(function (heading, index) {
var depth = getDepth(heading);
var level = depth + 1;
var text = pvzHeadingText(heading);
var anchor = pvzHeadingId(heading, text, index + 1);
var number = getNumber(depth);
var item = document.createElement('li');
var link = document.createElement('a');
var numberSpan = document.createElement('span');
var textSpan = document.createElement('span');
item.className =
'pvz-custom-toc-item pvz-custom-toc-level-' +
level +
' pvz-custom-toc-depth-' +
depth;
link.href = '#' + anchor;
numberSpan.className = 'pvz-custom-toc-number';
numberSpan.textContent = number;
textSpan.className = 'pvz-custom-toc-text';
textSpan.textContent = text;
link.appendChild(numberSpan);
link.appendChild(textSpan);
item.appendChild(link);
list.appendChild(item);
});
toc.appendChild(head);
toc.appendChild(list);
wrap.appendChild(toc);
placeTocAfterLead(wrap, leadParagraph);
pvzQueueTocStabilize();
}
var pvzFooterGapTimer = null;
function pvzGetFooterElement() {
return document.getElementById('mw-footer-container') ||
document.getElementById('footer') ||
document.querySelector('.mw-footer') ||
document.querySelector('footer');
}
function pvzUpdateFooterBackdropGapClass() {
var footer = pvzGetFooterElement();
var html = document.documentElement;
var body = document.body;
var rect;
var viewportHeight;
var docHeight;
var hasGap;
if (!html || !body || !footer || !footer.getBoundingClientRect) return;
html.classList.remove('pvz-footer-backdrop-gap-needed');
body.classList.remove('pvz-footer-backdrop-gap-needed');
rect = footer.getBoundingClientRect();
viewportHeight = window.visualViewport && window.visualViewport.height ?
window.visualViewport.height :
window.innerHeight;
docHeight = Math.max(
body.scrollHeight || 0,
body.offsetHeight || 0,
html.clientHeight || 0,
html.scrollHeight || 0,
html.offsetHeight || 0
);
/* footer container의 실제 바닥이 viewport 아래까지 닿지 못할 때만 켠다.
3px 여유값은 확대/축소 반올림 오차 방지용이다. */
hasGap = rect.bottom < viewportHeight - 3 && docHeight <= viewportHeight + 6;
if (hasGap) {
html.classList.add('pvz-footer-backdrop-gap-needed');
body.classList.add('pvz-footer-backdrop-gap-needed');
}
}
function pvzQueueFooterBackdropGapCheck() {
window.clearTimeout(pvzFooterGapTimer);
pvzFooterGapTimer = window.setTimeout(pvzUpdateFooterBackdropGapClass, 80);
}
function pvzStartFooterBackdropGapWatcher() {
pvzUpdateFooterBackdropGapClass();
window.setTimeout(pvzUpdateFooterBackdropGapClass, 120);
window.setTimeout(pvzUpdateFooterBackdropGapClass, 400);
window.setTimeout(pvzUpdateFooterBackdropGapClass, 1000);
window.addEventListener('resize', pvzQueueFooterBackdropGapCheck, { passive: true });
window.addEventListener('orientationchange', pvzQueueFooterBackdropGapCheck, { passive: true });
window.addEventListener('load', pvzQueueFooterBackdropGapCheck, { passive: true });
if (window.visualViewport) {
window.visualViewport.addEventListener('resize', pvzQueueFooterBackdropGapCheck, { passive: true });
window.visualViewport.addEventListener('scroll', pvzQueueFooterBackdropGapCheck, { passive: true });
}
if (window.MutationObserver && document.body) {
var observer = new MutationObserver(pvzQueueFooterBackdropGapCheck);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class', 'style']
});
}
}
function pvzInit() {
if (!pvzIsTimeless()) return;
pvzApplyInitialTimelessColorMode();
pvzBindSystemThemeListener();
pvzBuildCustomSearch();
pvzMarkReady();
pvzRemoveTimelessColorStripes();
pvzStartFooterBackdropGapWatcher();
pvzCreateCustomToc();
pvzBuildCustomMobileTools();
window.setTimeout(pvzBuildCustomSearch, 120);
window.setTimeout(pvzBuildCustomMobileTools, 300);
window.setTimeout(pvzBuildCustomSearch, 600);
window.setTimeout(pvzBuildCustomMobileTools, 1000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', pvzInit);
} else {
pvzInit();
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function () {
pvzInit();
});
}
if (window.addEventListener) {
window.addEventListener('resize', function () {
var shell = document.querySelector('.pvz-custom-mobile-tools-shell');
pvzApplyToolsCompactState(shell);
});
}
/* =========================
v94 쿠키 안내문 portal
- CSS z-index만으로 footer 뒤에서 빠져나오지 못하는 경우를 해결한다.
- 쿠키 안내문 DOM을 footer 밖, body 직속으로 옮겨 최상위 fixed 레이어로 표시한다.
========================= */
function pvzLooksLikeCookieWarning(el) {
var text;
var idClass;
if (!el || el.nodeType !== 1) return false;
text = (el.textContent || '').replace(/\s+/g, ' ').trim();
idClass = ((el.id || '') + ' ' + (el.className || '')).toLowerCase();
if (text.indexOf('쿠키는 저희의 서비스 전달에 도움을 줍니다') !== -1) return true;
if (text.indexOf('저희의 쿠키 사용에 동의') !== -1) return true;
if (text.indexOf('쿠키') !== -1 && text.indexOf('서비스') !== -1 && text.length > 20) return true;
if (idClass.indexOf('cookiewarning') !== -1) return true;
if (idClass.indexOf('cookie-warning') !== -1) return true;
if (idClass.indexOf('cookiewarning') !== -1) return true;
if (idClass.indexOf('mw-cookie') !== -1) return true;
if (idClass.indexOf('ext-cookie') !== -1) return true;
return false;
}
function pvzFindCookieWarningElement() {
var selectors = [
'#mw-cookiewarning-container',
'.mw-cookiewarning-container',
'#mw-cookiewarning',
'.mw-cookiewarning',
'.mw-cookie-warning',
'#cookieWarning',
'.cookieWarning',
'.cookie-warning',
'.ext-cookie-warning',
'.ext-cookie-warning-container',
'[id*="cookiewarning" i]',
'[class*="cookiewarning" i]',
'[id*="cookie-warning" i]',
'[class*="cookie-warning" i]',
'[id*="cookieWarning" i]',
'[class*="cookieWarning" i]',
'[id*="mw-cookie" i]',
'[class*="mw-cookie" i]',
'[id*="ext-cookie" i]',
'[class*="ext-cookie" i]'
];
var candidates = [];
var found;
var all;
var i;
var el;
var parent;
selectors.forEach(function (selector) {
try {
pvzArray(document.querySelectorAll(selector)).forEach(function (node) {
if (candidates.indexOf(node) === -1) candidates.push(node);
});
} catch (e) {}
});
for (i = 0; i < candidates.length; i += 1) {
if (pvzLooksLikeCookieWarning(candidates[i])) return candidates[i];
}
all = document.body ? document.body.querySelectorAll('div, aside, section, p, span') : [];
for (i = 0; i < all.length; i += 1) {
el = all[i];
if (pvzLooksLikeCookieWarning(el)) {
parent = el.closest('#mw-cookiewarning-container, .mw-cookiewarning-container, #mw-cookiewarning, .mw-cookiewarning, .mw-cookie-warning, #cookieWarning, .cookieWarning, .cookie-warning, .ext-cookie-warning, .ext-cookie-warning-container, div, aside, section') || el;
return parent;
}
}
return null;
}
function pvzPortalCookieWarning() {
var el = pvzFindCookieWarningElement();
if (!el || !document.body) return;
el.classList.add('pvz-cookie-warning-portal');
if (el.parentNode !== document.body) {
document.body.appendChild(el);
}
}
function pvzInitCookieWarningPortal() {
var observer;
pvzPortalCookieWarning();
window.setTimeout(pvzPortalCookieWarning, 250);
window.setTimeout(pvzPortalCookieWarning, 1000);
window.setTimeout(pvzPortalCookieWarning, 2500);
if (window.MutationObserver && document.body) {
observer = new MutationObserver(function () {
pvzPortalCookieWarning();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
pvzInitCookieWarningPortal();
/* =========================
액션바 계열 패널 문서 좌표 고정
- Timeless 상단 sheet는 v122의 전용 위치 처리에서 직접 absolute 좌표를 계산한다.
- 아래 document-lock은 그 외 액션바/드롭다운 패널에만 사용한다.
========================= */
function pvzGetActionAndTopbarPanels() {
var selectors = [
'.pvz-timeless-panel.is-open',
'.pvz-mobile-panel.is-open',
'.pvz-timeless-menu.is-open',
'.pvz-timeless-dropdown.is-open',
'.pvz-actionbar-panel.is-open',
'.pvz-action-panel.is-open',
'.pvz-topbar-panel.is-open',
'.pvz-topbar-menu.is-open',
'.pvz-timeless-panel[aria-hidden="false"]',
'.pvz-mobile-panel[aria-hidden="false"]',
'.pvz-timeless-menu[aria-hidden="false"]',
'.pvz-timeless-dropdown[aria-hidden="false"]',
'[data-pvz-panel-open="true"]',
'[data-pvz-sheet-open="true"]',
'[data-pvz-menu-open="true"]'
];
var panels = [];
selectors.forEach(function (selector) {
try {
pvzArray(document.querySelectorAll(selector)).forEach(function (panel) {
if (panels.indexOf(panel) === -1) panels.push(panel);
});
} catch (e) {}
});
return panels;
}
function pvzDocumentLockPanel(panel) {
var rect;
var scrollX;
var scrollY;
var docTop;
var docLeft;
var width;
if (!panel || panel.nodeType !== 1) return;
rect = panel.getBoundingClientRect();
scrollX = window.pageXOffset || document.documentElement.scrollLeft || 0;
scrollY = window.pageYOffset || document.documentElement.scrollTop || 0;
docTop = rect.top + scrollY;
docLeft = rect.left + scrollX;
width = rect.width || panel.offsetWidth || 0;
panel.dataset.pvzPanelDocumentLocked = 'true';
panel.dataset.pvzPanelDocumentTop = String(docTop);
panel.dataset.pvzPanelDocumentLeft = String(docLeft);
panel.dataset.pvzPanelDocumentWidth = String(width);
panel.style.setProperty('position', 'absolute', 'important');
panel.style.setProperty('top', docTop + 'px', 'important');
panel.style.setProperty('left', docLeft + 'px', 'important');
panel.style.setProperty('right', 'auto', 'important');
panel.style.setProperty('width', width + 'px', 'important');
panel.style.setProperty('max-width', 'calc(100vw - 12px)', 'important');
panel.style.setProperty('transform', 'none', 'important');
panel.style.setProperty('z-index', '2147483000', 'important');
}
function pvzRestoreDocumentLockedPanel(panel) {
var top;
var left;
var width;
if (!panel || panel.nodeType !== 1) return;
if (panel.dataset.pvzPanelDocumentLocked !== 'true') return;
top = panel.dataset.pvzPanelDocumentTop;
left = panel.dataset.pvzPanelDocumentLeft;
width = panel.dataset.pvzPanelDocumentWidth;
panel.style.setProperty('position', 'absolute', 'important');
if (top) panel.style.setProperty('top', top + 'px', 'important');
if (left) panel.style.setProperty('left', left + 'px', 'important');
if (width) panel.style.setProperty('width', width + 'px', 'important');
panel.style.setProperty('right', 'auto', 'important');
panel.style.setProperty('transform', 'none', 'important');
}
function pvzDocumentLockOpenPanels() {
pvzGetActionAndTopbarPanels().forEach(function (panel) {
if (panel.dataset.pvzPanelDocumentLocked === 'true') {
pvzRestoreDocumentLockedPanel(panel);
return;
}
pvzDocumentLockPanel(panel);
});
}
function pvzRecalculateDocumentLockedPanels() {
pvzGetActionAndTopbarPanels().forEach(function (panel) {
panel.removeAttribute('data-pvz-panel-document-locked');
panel.removeAttribute('data-pvz-panel-document-top');
panel.removeAttribute('data-pvz-panel-document-left');
panel.removeAttribute('data-pvz-panel-document-width');
panel.style.removeProperty('position');
panel.style.removeProperty('top');
panel.style.removeProperty('left');
panel.style.removeProperty('right');
panel.style.removeProperty('width');
panel.style.removeProperty('max-width');
panel.style.removeProperty('transform');
window.setTimeout(function () {
pvzDocumentLockPanel(panel);
}, 0);
});
}
function pvzInitPanelDocumentLock() {
var observer;
pvzDocumentLockOpenPanels();
if (window.MutationObserver && document.body) {
observer = new MutationObserver(function () {
pvzDocumentLockOpenPanels();
});
observer.observe(document.body, {
attributes: true,
childList: true,
subtree: true,
attributeFilter: ['class', 'style', 'aria-hidden', 'data-pvz-panel-open', 'data-pvz-sheet-open', 'data-pvz-menu-open']
});
}
/* 스크롤 중에는 좌표를 새로 계산하지 않는다.
기존 코드가 fixed/top 값을 다시 넣는 경우만 문서 좌표 absolute 값으로 되돌린다. */
window.addEventListener('scroll', function () {
window.requestAnimationFrame(function () {
pvzGetActionAndTopbarPanels().forEach(pvzRestoreDocumentLockedPanel);
});
}, { passive: true });
window.addEventListener('resize', function () {
window.clearTimeout(pvzInitPanelDocumentLock._resizeTimer);
pvzInitPanelDocumentLock._resizeTimer = window.setTimeout(
pvzRecalculateDocumentLockedPanels,
260
);
}, { passive: true });
function pvzQueueDocumentLockedPanelRecalculation() {
window.setTimeout(pvzRecalculateDocumentLockedPanels, 520);
}
if (
window.screen &&
window.screen.orientation &&
typeof window.screen.orientation.addEventListener === 'function'
) {
window.screen.orientation.addEventListener(
'change',
pvzQueueDocumentLockedPanelRecalculation
);
} else {
window.addEventListener(
'orientationchange',
pvzQueueDocumentLockedPanelRecalculation,
{ passive: true }
);
}
}
pvzInitPanelDocumentLock();
}());