미디어위키:Gadget-core.js: 두 판 사이의 차이
식물 vs 좀비 위키
새 문서: →========================= 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(); | |||
}()); | }()); | ||
