미디어위키:Gadget-Vector.js
참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
/* =========================
MediaWiki:Gadget-core.js
========================= */
(function () {
'use strict';
document.documentElement.classList.add('pvz-desktop-booting');
var PVZ_SEARCH_TIMER = null;
var PVZ_SEARCH_REQUEST_ID = 0;
var PVZ_SELECTED_SUGGESTION = -1;
var PVZ_SUGGESTIONS = [];
var PVZ_LAST_SEARCH_QUERY = '';
function pvzReady(callback) {
function run() {
if (window.mw && mw.loader && mw.loader.using) {
mw.loader.using('mediawiki.util').then(callback, callback);
} else {
callback();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run, { once: true });
} else {
run();
}
}
function pvzArray(list) {
return Array.prototype.slice.call(list || []);
}
function pvzClosest(target, selector) {
if (!target) return null;
if (target.nodeType !== 1) target = target.parentElement;
if (!target || typeof target.closest !== 'function') return null;
return target.closest(selector);
}
function pvzText(node) {
return node ? (node.textContent || '').replace(/\s+/g, ' ').trim() : '';
}
function pvzRemoveById(id) {
var el = document.getElementById(id);
if (el && el.parentNode) el.parentNode.removeChild(el);
}
function pvzGetUrl(title) {
if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
return mw.util.getUrl(title);
}
return '/wiki/' + encodeURIComponent(String(title || '')).replace(/%20/g, '_');
}
function pvzGetApiUrl() {
if (window.mw && mw.util && typeof mw.util.wikiScript === 'function') {
return mw.util.wikiScript('api');
}
if (window.mw && mw.config && mw.config.get('wgScriptPath')) {
return mw.config.get('wgScriptPath') + '/api.php';
}
return '/w/api.php';
}
function pvzGetPageName() {
if (window.mw && mw.config && mw.config.get('wgPageName')) {
return mw.config.get('wgPageName');
}
return (document.title || '').replace(/\s*-\s*.*$/, '').replace(/\s+/g, '_');
}
function pvzGetTitleText() {
var heading = document.getElementById('firstHeading') ||
document.querySelector('.mw-first-heading, .firstHeading');
if (heading) {
return pvzText(heading);
}
if (window.mw && mw.config && mw.config.get('wgTitle')) {
return mw.config.get('wgTitle');
}
return (document.title || '').replace(/\s*-\s*.*$/, '');
}
function pvzGetTalkPageName() {
var pageName = pvzGetPageName();
var ns;
var title;
var namespaces;
var talkNamespace;
if (window.mw && mw.config) {
ns = mw.config.get('wgNamespaceNumber');
title = mw.config.get('wgTitle');
namespaces = mw.config.get('wgFormattedNamespaces');
if (ns === -1 || ns % 2 === 1) return pageName;
talkNamespace = namespaces && namespaces[ns + 1] ? namespaces[ns + 1] : 'Talk';
if (ns === 0) {
talkNamespace = namespaces && namespaces[1] ? namespaces[1] : 'Talk';
}
return talkNamespace + ':' + title;
}
return 'Talk:' + pageName;
}
function pvzGetSearchUrl(query) {
var value = (query || '').replace(/\s+/g, ' ').trim();
if (!value) return pvzGetUrl('Special:Search');
return pvzGetUrl('Special:Search') + '?search=' + encodeURIComponent(value) + '&fulltext=1';
}
function pvzSyncClasses() {
var root = document.documentElement;
var body = document.body;
var ua = navigator.userAgent || '';
var screenW = window.screen && window.screen.width ? window.screen.width : 9999;
var screenH = window.screen && window.screen.height ? window.screen.height : 9999;
var hasTouch = !!(navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0);
var nativeMobile = !!(body && body.classList && body.classList.contains('mw-mf'));
var isVector = !!(body && body.classList && (
body.classList.contains('skin-vector') ||
body.classList.contains('skin-vector-2022')
));
var touchDevice = !!(
/Android|iPhone|iPad|iPod|Mobile|Windows Phone/i.test(ua) ||
(hasTouch && Math.min(screenW, screenH) <= 1400)
);
root.classList.toggle('pvz-skin-vector', isVector);
root.classList.toggle('pvz-native-mobile', nativeMobile);
root.classList.toggle('pvz-native-desktop', !nativeMobile);
root.classList.toggle('pvz-touch-device', touchDevice);
root.classList.toggle('pvz-desktop-view-on-touch', touchDevice && !nativeMobile);
if (body) {
body.classList.toggle('pvz-vector-ready', isVector);
}
}
function pvzCreateIcon(iconClass) {
var span = document.createElement('span');
span.className = 'pvz-icon ' + iconClass;
span.setAttribute('aria-hidden', 'true');
return span;
}
function pvzSetIconOnly(el, iconClass) {
el.textContent = '';
el.appendChild(pvzCreateIcon(iconClass));
}
function pvzAppendIconText(parent, iconClass, label) {
var text = document.createElement('span');
parent.appendChild(pvzCreateIcon(iconClass));
text.textContent = label;
parent.appendChild(text);
}
function pvzSetBoardButtonContent(parent, iconClass, label) {
var text = document.createElement('span');
parent.textContent = '';
if (iconClass) {
parent.appendChild(pvzCreateIcon(iconClass));
}
text.className = 'pvz-board-btn-text';
text.textContent = label;
parent.appendChild(text);
}
function pvzPortletLink(ids) {
var i;
var link;
for (i = 0; i < ids.length; i += 1) {
link = document.querySelector('#' + ids[i] + ' a');
if (link && link.href) return link;
}
return null;
}
function pvzCloneAction(ids, label, className, iconClass) {
var source = pvzPortletLink(ids);
var link;
var action;
var ns;
var displayLabel;
if (!source) return null;
displayLabel = label || pvzText(source);
link = document.createElement('a');
link.className = className || 'pvz-board-btn';
link.href = source.href;
pvzSetBoardButtonContent(link, iconClass, displayLabel);
if (source.getAttribute('title')) {
link.title = source.getAttribute('title');
}
if (source.classList.contains('new') || source.closest('.new')) {
link.classList.add('new');
}
if (source.closest('.selected') || source.closest('.mw-list-item.selected')) {
link.classList.add('is-current');
}
if (window.mw && mw.config) {
action = mw.config.get('wgAction') || 'view';
ns = mw.config.get('wgNamespaceNumber');
if (
action === 'view' &&
ids.some(function (id) { return id === 'ca-view' || id === 'ca-nstab-main' || id === 'ca-nstab-project'; }) &&
!(typeof ns === 'number' && ns % 2 === 1)
) {
link.classList.add('is-current');
}
if (
typeof ns === 'number' &&
ns % 2 === 1 &&
ids.some(function (id) { return id === 'ca-talk'; })
) {
link.classList.add('is-current');
}
if (
(action === 'edit' || action === 'submit' || action === 'visualeditor') &&
ids.some(function (id) { return id === 'ca-edit' || id === 'ca-ve-edit' || id === 'ca-viewsource'; })
) {
link.classList.add('is-current');
}
if (
action === 'history' &&
ids.some(function (id) { return id === 'ca-history'; })
) {
link.classList.add('is-current');
}
}
return link;
}
function pvzAppendIf(parent, child) {
if (child) parent.appendChild(child);
}
function pvzCreateBoardHead() {
var head = document.createElement('div');
var left = document.createElement('div');
var title = document.createElement('div');
var tabs = document.createElement('div');
var actions = document.createElement('div');
var moreMap;
var moreLinks;
head.className = 'pvz-board-head';
left.className = 'pvz-board-head-left';
title.className = 'pvz-board-title-ribbon';
tabs.className = 'pvz-board-tabs';
actions.className = 'pvz-board-actions';
title.textContent = pvzGetTitleText();
var viewLink = pvzCloneAction(['ca-view', 'ca-nstab-main', 'ca-nstab-project'], '문서', null, 'pvz-icon-special');
var talkLink = pvzCloneAction(['ca-talk'], '토론', null, 'pvz-icon-talk');
if (viewLink) viewLink.classList.add('is-tab');
if (talkLink) talkLink.classList.add('is-tab');
pvzAppendIf(tabs, viewLink);
pvzAppendIf(tabs, talkLink);
pvzAppendIf(actions, pvzCloneAction(['ca-edit', 'ca-ve-edit', 'ca-viewsource'], '편집', null, 'pvz-icon-tools'));
pvzAppendIf(actions, pvzCloneAction(['ca-history'], '역사', null, 'pvz-icon-recent'));
pvzAppendIf(actions, pvzCloneAction(['ca-watch'], '즐겨찾기에 추가', null, 'pvz-icon-star'));
pvzAppendIf(actions, pvzCloneAction(['ca-unwatch'], '즐겨찾기 해제', null, 'pvz-icon-star'));
moreMap = [
{ ids: ['ca-move'], label: '이동', icon: 'pvz-icon-special' },
{ ids: ['ca-purge'], label: '새로고침', icon: 'pvz-icon-recent' },
{ ids: ['ca-delete'], label: '삭제', icon: 'pvz-icon-tools' },
{ ids: ['ca-protect'], label: '보호', icon: 'pvz-icon-permissions' },
{ ids: ['ca-unprotect'], label: '보호 해제', icon: 'pvz-icon-permissions' }
];
moreLinks = moreMap.map(function (item) {
return pvzCloneAction(item.ids, item.label, null, item.icon);
}).filter(Boolean);
if (moreLinks.length) {
var details = document.createElement('details');
var summary = document.createElement('summary');
var menu = document.createElement('div');
var moreHasCurrent = moreLinks.some(function (link) {
return link && link.classList && link.classList.contains('is-current');
});
details.className = 'pvz-board-more';
pvzSetBoardButtonContent(summary, 'pvz-icon-plus', '더보기');
if (moreHasCurrent) summary.classList.add('is-current');
menu.className = 'pvz-board-more-menu';
moreLinks.forEach(function (link) { menu.appendChild(link); });
details.appendChild(summary);
details.appendChild(menu);
actions.appendChild(details);
}
left.appendChild(title);
left.appendChild(tabs);
head.appendChild(left);
head.appendChild(actions);
return head;
}
function pvzGetActiveSearchInput() {
return document.getElementById('pvz-desktop-search-input') ||
document.getElementById('pvz-mobile-search-input');
}
function pvzCreateSearchPanel() {
var old = document.getElementById('pvz-search-suggestions');
var panel;
if (old) return old;
panel = document.createElement('div');
panel.id = 'pvz-search-suggestions';
panel.className = 'pvz-search-suggestions';
panel.setAttribute('aria-label', '검색어 추천');
document.body.appendChild(panel);
return panel;
}
function pvzCloseSuggestions() {
document.documentElement.classList.remove('pvz-suggestions-open');
PVZ_SELECTED_SUGGESTION = -1;
}
function pvzPositionSearchPanel() {
var panel = document.getElementById('pvz-search-suggestions');
var input = pvzGetActiveSearchInput();
var rect;
var left;
var top;
var width;
if (!panel || !input || typeof input.getBoundingClientRect !== 'function') return;
rect = input.getBoundingClientRect();
left = Math.max(8, rect.left);
top = rect.bottom + 6;
width = Math.min(rect.width, window.innerWidth - 16);
panel.style.setProperty('left', left + 'px', 'important');
panel.style.setProperty('top', top + 'px', 'important');
panel.style.setProperty('width', width + 'px', 'important');
}
function pvzOpenSuggestions() {
pvzPositionSearchPanel();
if (PVZ_SUGGESTIONS.length) {
document.documentElement.classList.add('pvz-suggestions-open');
}
}
function pvzEscapeRegExp(text) {
return String(text || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function pvzAppendHighlightedTitle(container, text, query) {
var value = text || '';
var keyword = (query || '').replace(/\s+/g, ' ').trim();
var regex;
var lastIndex = 0;
var match;
container.textContent = '';
if (!keyword) {
container.textContent = value;
return;
}
regex = new RegExp(pvzEscapeRegExp(keyword), 'ig');
while ((match = regex.exec(value)) !== null) {
if (match.index > lastIndex) {
var normal = document.createElement('span');
normal.className = 'pvz-search-suggestion-normal';
normal.textContent = value.slice(lastIndex, match.index);
container.appendChild(normal);
}
var strong = document.createElement('strong');
strong.className = 'pvz-search-suggestion-match';
strong.textContent = value.slice(match.index, match.index + match[0].length);
container.appendChild(strong);
lastIndex = match.index + match[0].length;
}
if (lastIndex === 0) {
container.textContent = value;
return;
}
if (lastIndex < value.length) {
var tail = document.createElement('span');
tail.className = 'pvz-search-suggestion-normal';
tail.textContent = value.slice(lastIndex);
container.appendChild(tail);
}
}
function pvzRenderSearchSuggestions(items) {
var panel = pvzCreateSearchPanel();
var list = document.createElement('ul');
panel.innerHTML = '';
PVZ_SELECTED_SUGGESTION = -1;
if (!items.length) {
pvzCloseSuggestions();
return;
}
list.className = 'pvz-search-suggestions-list';
items.forEach(function (item, index) {
var li = document.createElement('li');
var a = document.createElement('a');
var thumb = document.createElement('span');
var title = document.createElement('span');
li.className = 'pvz-search-suggestion';
li.setAttribute('data-pvz-suggestion-index', String(index));
a.className = 'pvz-search-suggestion-link';
a.href = item.href;
thumb.className = 'pvz-search-suggestion-thumb';
if (item.thumbnail) {
var img = document.createElement('img');
thumb.classList.add('has-image');
img.src = item.thumbnail;
img.alt = '';
img.loading = 'lazy';
thumb.appendChild(img);
} else {
thumb.classList.add('is-empty');
}
title.className = 'pvz-search-suggestion-title';
pvzAppendHighlightedTitle(title, item.title, PVZ_LAST_SEARCH_QUERY);
a.appendChild(thumb);
a.appendChild(title);
a.addEventListener('click', pvzCloseSuggestions);
li.appendChild(a);
list.appendChild(li);
});
panel.appendChild(list);
pvzOpenSuggestions();
}
function pvzFetchSearchSuggestions(query) {
var value = (query || '').replace(/\s+/g, ' ').trim();
var requestId;
var url;
if (!value || !window.fetch) {
PVZ_LAST_SEARCH_QUERY = '';
PVZ_SUGGESTIONS = [];
pvzRenderSearchSuggestions([]);
pvzCloseSuggestions();
return;
}
PVZ_LAST_SEARCH_QUERY = value;
requestId = ++PVZ_SEARCH_REQUEST_ID;
url =
pvzGetApiUrl() +
'?action=query' +
'&format=json' +
'&formatversion=2' +
'&generator=prefixsearch' +
'&gpsnamespace=0' +
'&gpslimit=8' +
'&gpssearch=' + encodeURIComponent(value) +
'&prop=pageimages' +
'&piprop=thumbnail' +
'&pithumbsize=72' +
'&pilimit=8' +
'&redirects=1';
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var pages;
if (requestId !== PVZ_SEARCH_REQUEST_ID) return;
pages = data && data.query && Array.isArray(data.query.pages)
? data.query.pages
: [];
pages.sort(function (a, b) {
return (a.index || 9999) - (b.index || 9999);
});
PVZ_SUGGESTIONS = pages.map(function (page) {
return {
title: page.title,
href: pvzGetUrl(page.title),
thumbnail: page.thumbnail && page.thumbnail.source ? page.thumbnail.source : ''
};
});
pvzRenderSearchSuggestions(PVZ_SUGGESTIONS);
})
.catch(function () {
if (requestId === PVZ_SEARCH_REQUEST_ID) {
PVZ_SUGGESTIONS = [];
pvzRenderSearchSuggestions([]);
pvzCloseSuggestions();
}
});
}
function pvzScheduleSearchSuggestions(query) {
window.clearTimeout(PVZ_SEARCH_TIMER);
PVZ_SEARCH_TIMER = window.setTimeout(function () {
pvzFetchSearchSuggestions(query);
}, 140);
}
function pvzUpdateSelectedSuggestion() {
document.querySelectorAll('.pvz-search-suggestion').forEach(function (li) {
var index = parseInt(li.getAttribute('data-pvz-suggestion-index'), 10);
li.classList.toggle('is-selected', index === PVZ_SELECTED_SUGGESTION);
});
}
function pvzHandleSearchKeydown(event) {
var input = pvzGetActiveSearchInput();
var item;
if (event.key === 'ArrowDown') {
if (!PVZ_SUGGESTIONS.length) return;
event.preventDefault();
PVZ_SELECTED_SUGGESTION += 1;
if (PVZ_SELECTED_SUGGESTION >= PVZ_SUGGESTIONS.length) PVZ_SELECTED_SUGGESTION = 0;
pvzUpdateSelectedSuggestion();
return;
}
if (event.key === 'ArrowUp') {
if (!PVZ_SUGGESTIONS.length) return;
event.preventDefault();
PVZ_SELECTED_SUGGESTION -= 1;
if (PVZ_SELECTED_SUGGESTION < 0) PVZ_SELECTED_SUGGESTION = PVZ_SUGGESTIONS.length - 1;
pvzUpdateSelectedSuggestion();
return;
}
if (event.key === 'Enter') {
if (PVZ_SELECTED_SUGGESTION >= 0 && PVZ_SUGGESTIONS[PVZ_SELECTED_SUGGESTION]) {
event.preventDefault();
item = PVZ_SUGGESTIONS[PVZ_SELECTED_SUGGESTION];
window.location.href = item.href;
return;
}
if (input && input.value.replace(/\s+/g, ' ').trim()) {
event.preventDefault();
window.location.href = pvzGetSearchUrl(input.value);
}
return;
}
if (event.key === 'Escape') {
event.preventDefault();
pvzCloseSuggestions();
}
}
function pvzGetLocal(key, fallback) {
try {
return localStorage.getItem(key) || fallback;
} catch (e) {
return fallback;
}
}
function pvzSetLocal(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {}
}
function pvzSetFontSize(value) {
if (value !== 'small' && value !== 'medium' && value !== 'large') value = 'small';
pvzSetLocal('pvz-font-size', value);
document.documentElement.classList.remove('pvz-font-small', 'pvz-font-medium', 'pvz-font-large');
document.documentElement.classList.add('pvz-font-' + value);
pvzUpdateChoices();
}
function pvzSetTheme(value) {
if (value !== 'auto' && value !== 'light' && value !== 'dark') value = 'auto';
pvzSetLocal('pvz-mobile-theme', value);
document.documentElement.classList.remove('pvz-theme-auto', 'pvz-theme-light', 'pvz-theme-dark');
document.documentElement.classList.add('pvz-theme-' + value);
pvzUpdateChoices();
}
function pvzSetWidth(value) {
if (value !== 'normal' && value !== 'wide') value = 'normal';
pvzSetLocal('pvz-mobile-width', value);
document.documentElement.classList.remove('pvz-width-normal', 'pvz-width-wide', 'pvz-width-full');
document.documentElement.classList.add('pvz-width-' + value);
pvzUpdateChoices();
}
function pvzSetAutoHide(value) {
if (value !== 'on' && value !== 'off') value = 'off';
pvzSetLocal('pvz-mobile-autohide', value);
document.documentElement.classList.remove('pvz-auto-hide-on', 'pvz-auto-hide-off');
document.documentElement.classList.add('pvz-auto-hide-' + value);
pvzUpdateChoices();
}
function pvzSetPerformance(value) {
if (value !== 'on' && value !== 'off') value = 'off';
pvzSetLocal('pvz-mobile-performance', value);
document.documentElement.classList.remove('pvz-performance-on', 'pvz-performance-off');
document.documentElement.classList.add('pvz-performance-' + value);
pvzUpdateChoices();
}
function pvzApplyPreferences() {
pvzSetFontSize(pvzGetLocal('pvz-font-size', 'small'));
pvzSetTheme(pvzGetLocal('pvz-mobile-theme', 'auto'));
pvzSetWidth(pvzGetLocal('pvz-mobile-width', 'normal'));
pvzSetAutoHide(pvzGetLocal('pvz-mobile-autohide', 'off'));
pvzSetPerformance(pvzGetLocal('pvz-mobile-performance', 'off'));
}
function pvzUpdateChoices() {
var values = {
font: pvzGetLocal('pvz-font-size', 'small'),
theme: pvzGetLocal('pvz-mobile-theme', 'auto'),
width: pvzGetLocal('pvz-mobile-width', 'normal'),
autohide: pvzGetLocal('pvz-mobile-autohide', 'off'),
performance: pvzGetLocal('pvz-mobile-performance', 'off')
};
document.querySelectorAll('.pvz-mobile-choice').forEach(function (button) {
var kind = button.getAttribute('data-pvz-setting-kind');
var value = button.getAttribute('data-pvz-setting-value');
button.classList.toggle('is-selected', values[kind] === value);
});
}
function pvzCreateMobileLink(iconClass, label, href) {
var li = document.createElement('li');
var a = document.createElement('a');
a.href = href;
pvzAppendIconText(a, iconClass, label);
li.appendChild(a);
return li;
}
function pvzCreateMobileLinkList(items) {
var ul = document.createElement('ul');
ul.className = 'pvz-mobile-link-list';
items.forEach(function (item) {
ul.appendChild(pvzCreateMobileLink(item.icon, item.label, item.href));
});
return ul;
}
function pvzCreateSheetSection(title, content) {
var section = document.createElement('section');
var h = document.createElement('div');
section.className = 'pvz-mobile-sheet-section';
h.className = 'pvz-mobile-sheet-section-title';
h.textContent = title;
section.appendChild(h);
section.appendChild(content);
return section;
}
function pvzCreateBrowsePanel() {
var panel = document.createElement('nav');
panel.id = 'pvz-mobile-panel-browse';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-left';
panel.setAttribute('aria-label', '둘러보기');
panel.appendChild(pvzCreateSheetSection('둘러보기', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-home', label: '대문', href: pvzGetUrl('대문') },
{ icon: 'pvz-icon-recent', label: '최근 바뀜', href: pvzGetUrl('Special:RecentChanges') },
{ icon: 'pvz-icon-random', label: '임의 문서로', href: pvzGetUrl('Special:Random') },
{ icon: 'pvz-icon-help', label: '미디어위키 도움말', href: pvzGetUrl('Help:Contents') },
{ icon: 'pvz-icon-special', label: '특수 문서 목록', href: pvzGetUrl('Special:SpecialPages') },
{ icon: 'pvz-icon-upload', label: '파일 올리기', href: pvzGetUrl('Special:Upload') }
])));
panel.appendChild(pvzCreateSheetSection('관리', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-settings', label: '이 위키의 핵심 설정을 관리하기', href: pvzGetUrl('Special:ManageWiki/settings') },
{ icon: 'pvz-icon-extensions', label: '이 위키의 확장 기능을 관리하기', href: pvzGetUrl('Special:ManageWiki/extensions') },
{ icon: 'pvz-icon-namespace', label: '이 위키의 이름공간 관리', href: pvzGetUrl('Special:ManageWiki/namespaces') },
{ icon: 'pvz-icon-permissions', label: '이 위키의 권한 관리', href: pvzGetUrl('Special:ManageWiki/permissions') },
{ icon: 'pvz-icon-plus', label: '이 위키의 추가 설정을 관리하기', href: pvzGetUrl('Special:ManageWiki/settings') },
{ icon: 'pvz-icon-backup', label: '이 위키의 백업을 관리', href: pvzGetUrl('Special:DataDump') }
])));
return panel;
}
function pvzCreateChoice(label, kind, value, extraClass) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'pvz-mobile-choice' + (extraClass ? ' ' + extraClass : '');
button.setAttribute('data-pvz-setting-kind', kind);
button.setAttribute('data-pvz-setting-value', value);
button.textContent = label;
button.addEventListener('click', function (event) {
event.preventDefault();
if (kind === 'font') pvzSetFontSize(value);
if (kind === 'theme') pvzSetTheme(value);
if (kind === 'width') pvzSetWidth(value);
if (kind === 'autohide') pvzSetAutoHide(value);
if (kind === 'performance') pvzSetPerformance(value);
});
return button;
}
function pvzCreateChoiceGroup(title, kind, choices, gridClass) {
var wrap = document.createElement('div');
var h = document.createElement('div');
var grid = document.createElement('div');
wrap.className = 'pvz-mobile-setting-group';
h.className = 'pvz-mobile-setting-title';
h.textContent = title;
grid.className = 'pvz-mobile-choice-grid' + (gridClass ? ' ' + gridClass : '');
choices.forEach(function (choice) {
grid.appendChild(pvzCreateChoice(choice.label, kind, choice.value, choice.extraClass));
});
wrap.appendChild(h);
wrap.appendChild(grid);
return wrap;
}
function pvzCreateSettingsPanel() {
var panel = document.createElement('section');
panel.id = 'pvz-mobile-panel-settings';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-right';
panel.setAttribute('aria-label', '화면 설정');
panel.appendChild(pvzCreateChoiceGroup('글자 크기', 'font', [
{ label: '작게', value: 'small' },
{ label: '보통', value: 'medium' },
{ label: '크게', value: 'large' }
], 'three'));
panel.appendChild(pvzCreateChoiceGroup('색상 테마', 'theme', [
{ label: '자동', value: 'auto' },
{ label: '라이트', value: 'light' },
{ label: '다크', value: 'dark', extraClass: 'dark' }
], 'three'));
panel.appendChild(pvzCreateChoiceGroup('페이지 너비', 'width', [
{ label: '기본', value: 'normal' },
{ label: '넓게', value: 'wide' }
], ''));
panel.appendChild(pvzCreateChoiceGroup('자동으로 메뉴 숨김', 'autohide', [
{ label: '끄기', value: 'off' },
{ label: '켜기', value: 'on' }
], ''));
panel.appendChild(pvzCreateChoiceGroup('Performance mode', 'performance', [
{ label: 'Off', value: 'off' },
{ label: 'On', value: 'on' }
], ''));
return panel;
}
function pvzFormatUserGroup(group) {
var map = {
bureaucrat: '사무관',
sysop: '관리자',
administrator: '관리자',
interfaceadmin: '인터페이스 관리자',
'interface-admin': '인터페이스 관리자',
bot: '봇',
autoconfirmed: '자동 인증된 사용자',
user: '사용자'
};
return map[group] || group;
}
function pvzFormatDate(value) {
var date;
var days = ['일', '월', '화', '수', '목', '금', '토'];
if (!value) return '확인 불가';
date = new Date(value);
if (isNaN(date.getTime())) return '확인 불가';
return date.getFullYear() + '년 ' + (date.getMonth() + 1) + '월 ' +
date.getDate() + '일 (' + days[date.getDay()] + ')';
}
function pvzCreateAccountPanel() {
var panel = document.createElement('section');
var username = window.mw && mw.config ? mw.config.get('wgUserName') : null;
var card = document.createElement('div');
var name = document.createElement('div');
var groups = document.createElement('div');
var stats = document.createElement('div');
panel.id = 'pvz-mobile-panel-account';
panel.className = 'pvz-mobile-sheet pvz-mobile-sheet-right';
panel.setAttribute('aria-label', '계정');
card.className = 'pvz-mobile-account-card';
name.id = 'pvz-mobile-account-name';
name.className = 'pvz-mobile-account-name';
name.textContent = username || '로그인하지 않음';
groups.id = 'pvz-mobile-account-groups';
groups.className = 'pvz-mobile-account-groups';
groups.textContent = username ? '계정 정보를 불러오는 중...' : '비로그인 사용자';
stats.className = 'pvz-mobile-account-stats';
stats.innerHTML =
'<span>편집</span><span>가입일</span>' +
'<strong id="pvz-mobile-account-edits">-</strong>' +
'<strong id="pvz-mobile-account-registration">-</strong>';
card.appendChild(name);
card.appendChild(groups);
card.appendChild(stats);
panel.appendChild(card);
if (!username) {
panel.appendChild(pvzCreateSheetSection('계정', pvzCreateMobileLinkList([
{ icon: 'pvz-icon-login', label: '로그인', href: pvzGetUrl('Special:UserLogin') },
{ icon: 'pvz-icon-plus', label: '회원 가입', href: pvzGetUrl('Special:CreateAccount') }
])));
return panel;
}
panel.appendChild(pvzCreateMobileLinkList([
{ icon: 'pvz-icon-talk', label: '토론', href: pvzGetUrl('User talk:' + username) },
{ icon: 'pvz-icon-star', label: '즐겨찾기 문서', href: pvzGetUrl('Special:Watchlist') },
{ icon: 'pvz-icon-recent', label: '기여', href: pvzGetUrl('Special:Contributions/' + username) }
]));
var logout = document.createElement('a');
logout.className = 'pvz-mobile-account-logout';
logout.href = pvzGetUrl('Special:UserLogout');
pvzAppendIconText(logout, 'pvz-icon-login', '로그아웃');
panel.appendChild(logout);
return panel;
}
function pvzLoadAccountInfo() {
var username = window.mw && mw.config ? mw.config.get('wgUserName') : null;
var url;
if (!username || !window.fetch) return;
url =
pvzGetApiUrl() +
'?action=query' +
'&format=json' +
'&list=users' +
'&usprop=groups%7Ceditcount%7Cregistration' +
'&ususers=' + encodeURIComponent(username);
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var user = data && data.query && data.query.users && data.query.users[0];
var groupBox = document.getElementById('pvz-mobile-account-groups');
var editBox = document.getElementById('pvz-mobile-account-edits');
var regBox = document.getElementById('pvz-mobile-account-registration');
if (!user) return;
if (groupBox) {
groupBox.textContent = (user.groups || [])
.filter(function (group) {
return group !== '*' && group !== 'user' && group !== 'autoconfirmed';
})
.map(pvzFormatUserGroup)
.join(' ') || '일반 사용자';
}
if (editBox) {
editBox.textContent = typeof user.editcount === 'number' ? String(user.editcount) : '-';
}
if (regBox) {
regBox.textContent = pvzFormatDate(user.registration);
}
})
.catch(function () {});
}
function pvzCreatePanels() {
var backdrop;
pvzRemoveById('pvz-mobile-panel-backdrop');
pvzRemoveById('pvz-mobile-panel-browse');
pvzRemoveById('pvz-mobile-panel-settings');
pvzRemoveById('pvz-mobile-panel-account');
backdrop = document.createElement('div');
backdrop.id = 'pvz-mobile-panel-backdrop';
backdrop.className = 'pvz-mobile-panel-backdrop';
document.body.appendChild(backdrop);
document.body.appendChild(pvzCreateBrowsePanel());
document.body.appendChild(pvzCreateSettingsPanel());
document.body.appendChild(pvzCreateAccountPanel());
pvzUpdateChoices();
}
function pvzClosePanels() {
document.documentElement.classList.remove('pvz-mobile-panel-open');
document.querySelectorAll('.pvz-mobile-sheet').forEach(function (panel) {
panel.classList.remove('is-open');
});
document.querySelectorAll('.pvz-mobile-nav-button').forEach(function (button) {
button.classList.remove('is-active');
});
}
function pvzOpenPanel(name) {
var panel = document.getElementById('pvz-mobile-panel-' + name);
if (!panel) return;
pvzCloseSuggestions();
pvzClosePanels();
document.documentElement.classList.add('pvz-mobile-panel-open');
panel.classList.add('is-open');
document.querySelectorAll('.pvz-mobile-nav-button').forEach(function (button) {
button.classList.toggle('is-active', button.getAttribute('data-pvz-mobile-panel') === name);
});
if (name === 'account') pvzLoadAccountInfo();
pvzUpdateChoices();
}
function pvzTogglePanel(name) {
var panel = document.getElementById('pvz-mobile-panel-' + name);
if (panel && panel.classList.contains('is-open')) {
pvzClosePanels();
return;
}
pvzOpenPanel(name);
}
function pvzCleanupOldBars() {
document.querySelectorAll(
'#pvz-appbar, .pvz-appbar, #pvz-pc-accountbar, .pvz-pc-accountbar, ' +
'#pvz-pc-side-settings-button, .pvz-pc-side-settings-button, .pvz-restored-topbar, ' +
'#pvz-mobile-topbar, .pvz-mobile-topbar, .pvz-timeless-custom-topbar, ' +
'.pvz-timeless-custom-search-form, .pvz-timeless-custom-search-input'
).forEach(function (el) {
if (el && el.parentNode) el.parentNode.removeChild(el);
});
}
function pvzGroupTopbarTabs() {
var bar = document.getElementById('pvz-mobile-topbar');
var strip;
var buttons;
if (!bar) return;
strip = bar.querySelector('.pvz-topbar-tab-strip');
if (!strip) {
strip = document.createElement('div');
strip.className = 'pvz-topbar-tab-strip';
strip.setAttribute('aria-label', '상단 도구');
}
buttons = [
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="browse"]'),
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="settings"]'),
bar.querySelector('.pvz-mobile-nav-button[data-pvz-mobile-panel="account"]')
].filter(Boolean);
if (!buttons.length) return;
if (strip.parentNode !== bar) {
bar.insertBefore(strip, bar.firstChild);
}
buttons.forEach(function (button) {
if (button.parentNode !== strip) {
strip.appendChild(button);
}
});
}
function pvzEnsureTopbar() {
var old = document.getElementById('pvz-mobile-topbar');
var bar;
var menuButton;
var searchForm;
var searchInput;
var alertsWrap;
var alertsButton;
var alertsPopover;
var alertsHeader;
var alertsBody;
var alertsFooter;
var allAlerts;
var preferencesLink;
var accountButton;
var settingsButton;
if (!document.body) return;
document.documentElement.classList.add('pvz-modern-ui');
pvzCleanupOldBars();
/* 데스크톱은 pvz-desktop-utilitybar만 사용한다. 예전 pvz-mobile-topbar 검색바는 만들지 않는다. */
if (window.matchMedia && window.matchMedia('(min-width: 721px)').matches) {
if (old && old.parentNode) old.parentNode.removeChild(old);
return;
}
/* 모바일 폭에서만 기존 상단바가 검색만 남거나 버튼이 빠지는 문제를 막기 위해 매번 재생성한다. */
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
bar = document.createElement('header');
bar.id = 'pvz-mobile-topbar';
bar.className = 'pvz-mobile-topbar';
bar.setAttribute('data-pvz-topbar', 'true');
bar.setAttribute('aria-label', '식좀위키 상단 바');
bar.style.setProperty('position', 'absolute', 'important');
bar.style.setProperty('top', '0', 'important');
bar.style.setProperty('right', '0', 'important');
bar.style.setProperty('left', 'auto', 'important');
bar.style.setProperty('transform', 'none', 'important');
menuButton = document.createElement('button');
menuButton.type = 'button';
menuButton.className = 'pvz-mobile-nav-button';
menuButton.title = '둘러보기';
menuButton.setAttribute('aria-label', '둘러보기');
menuButton.setAttribute('data-pvz-mobile-panel', 'browse');
pvzSetIconOnly(menuButton, 'pvz-icon-menu');
searchForm = document.createElement('form');
searchForm.className = 'pvz-mobile-search-form';
searchForm.setAttribute('role', 'search');
searchInput = document.createElement('input');
searchInput.id = 'pvz-mobile-search-input';
searchInput.className = 'pvz-mobile-search-input';
searchInput.type = 'search';
searchInput.placeholder = '문서 검색';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', function () {
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('focus', function () {
document.documentElement.classList.add('pvz-mobile-search-focused');
pvzPositionSearchPanel();
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('blur', function () {
window.setTimeout(function () {
document.documentElement.classList.remove('pvz-mobile-search-focused');
}, 200);
});
searchInput.addEventListener('keydown', pvzHandleSearchKeydown);
searchForm.addEventListener('submit', function (event) {
event.preventDefault();
if (searchInput.value.replace(/\s+/g, ' ').trim()) {
window.location.href = pvzGetSearchUrl(searchInput.value);
}
});
searchForm.appendChild(searchInput);
alertsWrap = document.createElement('div');
alertsWrap.id = 'pvz-alerts-wrap';
alertsWrap.className = 'pvz-alerts-wrap';
alertsButton = document.createElement('button');
alertsButton.type = 'button';
alertsButton.id = 'pvz-alerts-button';
alertsButton.className = 'pvz-mobile-nav-button';
alertsButton.title = '알림';
alertsButton.setAttribute('aria-label', '알림');
pvzSetIconOnly(alertsButton, 'pvz-icon-bell');
alertsPopover = document.createElement('div');
alertsPopover.id = 'pvz-alerts-popover';
alertsPopover.className = 'pvz-alerts-popover';
alertsHeader = document.createElement('div');
alertsHeader.className = 'pvz-alerts-popover-header';
alertsHeader.appendChild(pvzCreateIcon('pvz-icon-bell'));
var headerText = document.createElement('span');
headerText.textContent = '알림';
alertsHeader.appendChild(headerText);
alertsBody = document.createElement('div');
alertsBody.className = 'pvz-alerts-popover-body';
alertsBody.textContent = '새 알림이 없습니다.';
alertsFooter = document.createElement('div');
alertsFooter.className = 'pvz-alerts-popover-footer';
allAlerts = document.createElement('a');
allAlerts.href = pvzGetUrl('Special:Notifications');
allAlerts.textContent = '전체 알림';
preferencesLink = document.createElement('a');
preferencesLink.href = pvzGetUrl('Special:Preferences');
preferencesLink.textContent = '환경설정';
alertsFooter.appendChild(allAlerts);
alertsFooter.appendChild(preferencesLink);
alertsPopover.appendChild(alertsHeader);
alertsPopover.appendChild(alertsBody);
alertsPopover.appendChild(alertsFooter);
alertsWrap.appendChild(alertsButton);
alertsWrap.appendChild(alertsPopover);
accountButton = document.createElement('button');
accountButton.type = 'button';
accountButton.className = 'pvz-mobile-nav-button';
accountButton.title = '계정';
accountButton.setAttribute('aria-label', '계정');
accountButton.setAttribute('data-pvz-mobile-panel', 'account');
pvzSetIconOnly(accountButton, 'pvz-icon-account');
settingsButton = document.createElement('button');
settingsButton.type = 'button';
settingsButton.className = 'pvz-mobile-nav-button';
settingsButton.title = '화면 설정';
settingsButton.setAttribute('aria-label', '화면 설정');
settingsButton.setAttribute('data-pvz-mobile-panel', 'settings');
pvzSetIconOnly(settingsButton, 'pvz-icon-settings');
if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) {
bar.appendChild(menuButton);
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(settingsButton);
bar.appendChild(accountButton);
} else {
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(accountButton);
}
document.body.insertBefore(bar, document.body.firstChild);
pvzCreateSearchPanel();
if (!document.getElementById('pvz-mobile-panel-browse')) pvzCreatePanels();
pvzPositionSearchPanel();
}
function pvzEnsureDesktopUtilityBar() {
var old = document.getElementById('pvz-desktop-utilitybar');
var bar;
var searchForm;
var searchInput;
var alertsWrap;
var alertsButton;
var alertsPopover;
var alertsHeader;
var alertsBody;
var alertsFooter;
var allAlerts;
var preferencesLink;
var accountButton;
if (!document.body) return;
/* 모바일에서는 기존 모바일 상단바를 사용한다. */
if (window.matchMedia && window.matchMedia('(max-width: 720px)').matches) {
if (old && old.parentNode) old.parentNode.removeChild(old);
return;
}
if (old) {
if (
old.querySelector('#pvz-desktop-search-input') &&
old.querySelector('#pvz-desktop-alerts-button') &&
old.querySelector('#pvz-desktop-account-button')
) {
return;
}
if (old.parentNode) old.parentNode.removeChild(old);
}
bar = document.createElement('div');
bar.id = 'pvz-desktop-utilitybar';
bar.className = 'pvz-desktop-utilitybar';
bar.setAttribute('aria-label', '식좀위키 데스크톱 유틸리티 바');
searchForm = document.createElement('form');
searchForm.className = 'pvz-desktop-search-form';
searchForm.setAttribute('role', 'search');
searchInput = document.createElement('input');
searchInput.id = 'pvz-desktop-search-input';
searchInput.className = 'pvz-desktop-search-input';
searchInput.type = 'search';
searchInput.placeholder = '문서 검색';
searchInput.autocomplete = 'off';
searchInput.addEventListener('input', function () {
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('focus', function () {
document.documentElement.classList.add('pvz-mobile-search-focused');
pvzPositionSearchPanel();
pvzScheduleSearchSuggestions(searchInput.value);
});
searchInput.addEventListener('blur', function () {
window.setTimeout(function () {
document.documentElement.classList.remove('pvz-mobile-search-focused');
}, 200);
});
searchInput.addEventListener('keydown', pvzHandleSearchKeydown);
searchForm.addEventListener('submit', function (event) {
event.preventDefault();
if (searchInput.value.replace(/\s+/g, ' ').trim()) {
window.location.href = pvzGetSearchUrl(searchInput.value);
}
});
searchForm.appendChild(searchInput);
alertsWrap = document.createElement('div');
alertsWrap.id = 'pvz-desktop-alerts-wrap';
alertsWrap.className = 'pvz-desktop-alerts-wrap';
alertsButton = document.createElement('button');
alertsButton.type = 'button';
alertsButton.id = 'pvz-desktop-alerts-button';
alertsButton.className = 'pvz-desktop-util-button';
alertsButton.title = '알림';
alertsButton.setAttribute('aria-label', '알림');
pvzSetIconOnly(alertsButton, 'pvz-icon-bell');
alertsPopover = document.createElement('div');
alertsPopover.id = 'pvz-desktop-alerts-popover';
alertsPopover.className = 'pvz-desktop-alerts-popover';
alertsHeader = document.createElement('div');
alertsHeader.className = 'pvz-desktop-alerts-popover-header';
alertsHeader.textContent = '알림';
alertsBody = document.createElement('div');
alertsBody.className = 'pvz-desktop-alerts-popover-body';
alertsBody.textContent = '새 알림이 없습니다.';
alertsFooter = document.createElement('div');
alertsFooter.className = 'pvz-desktop-alerts-popover-footer';
allAlerts = document.createElement('a');
allAlerts.href = pvzGetUrl('Special:Notifications');
allAlerts.textContent = '전체 알림';
preferencesLink = document.createElement('a');
preferencesLink.href = pvzGetUrl('Special:Preferences');
preferencesLink.textContent = '환경설정';
alertsFooter.appendChild(allAlerts);
alertsFooter.appendChild(preferencesLink);
alertsPopover.appendChild(alertsHeader);
alertsPopover.appendChild(alertsBody);
alertsPopover.appendChild(alertsFooter);
alertsWrap.appendChild(alertsButton);
alertsWrap.appendChild(alertsPopover);
alertsButton.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
alertsWrap.classList.toggle('is-open');
});
accountButton = document.createElement('button');
accountButton.type = 'button';
accountButton.id = 'pvz-desktop-account-button';
accountButton.className = 'pvz-desktop-util-button pvz-mobile-nav-button';
accountButton.title = '계정';
accountButton.setAttribute('aria-label', '계정');
accountButton.setAttribute('data-pvz-mobile-panel', 'account');
pvzSetIconOnly(accountButton, 'pvz-icon-account');
/* 왼쪽부터 검색바, 알림, 계정. 따라서 오른쪽 끝 기준은 계정 → 알림 → 검색바. */
bar.appendChild(searchForm);
bar.appendChild(alertsWrap);
bar.appendChild(accountButton);
document.body.insertBefore(bar, document.body.firstChild);
pvzCreateSearchPanel();
if (!document.getElementById('pvz-mobile-panel-account')) pvzCreatePanels();
pvzPositionSearchPanel();
}
function pvzEnsureLogo() {
var old = document.getElementById('pvz-page-logo-link');
var topbar = document.getElementById('pvz-mobile-topbar');
var logo;
var img;
if (old || !document.body) return;
logo = document.createElement('a');
logo.id = 'pvz-page-logo-link';
logo.className = 'pvz-page-logo-link';
logo.href = pvzGetUrl('식물 vs 좀비 위키');
logo.title = '식물 vs 좀비 위키';
logo.setAttribute('aria-label', '식물 vs 좀비 위키로 이동');
img = document.createElement('img');
img.src = '/wiki/Special:Redirect/file/PvZKR_Wiki_Logo2.png';
img.alt = 'Plants vs. Zombies 위키';
img.loading = 'eager';
logo.appendChild(img);
if (topbar && topbar.parentNode === document.body && topbar.nextSibling) document.body.insertBefore(logo, topbar.nextSibling);
else if (topbar && topbar.parentNode === document.body) document.body.appendChild(logo);
else document.body.insertBefore(logo, document.body.firstChild);
}
function pvzCreateSideLink(label, href) {
var a = document.createElement('a');
a.className = 'pvz-board-side-link';
a.href = href;
a.textContent = label;
return a;
}
function pvzCreateSideSection(title, items) {
var section = document.createElement('section');
var h = document.createElement('div');
var paper = document.createElement('div');
section.className = 'pvz-board-side-section pvz-side-section-' + String(title || '').replace(/\s+/g, '-');
h.className = 'pvz-board-side-title';
h.textContent = title;
paper.className = 'pvz-board-side-paper';
items.forEach(function (item) { paper.appendChild(pvzCreateSideLink(item.label, item.href)); });
section.appendChild(h);
section.appendChild(paper);
return section;
}
function pvzCreateSideNav() {
var nav = document.createElement('aside');
var pageName = pvzGetPageName();
var printableHref = window.location.href.split('#')[0] + (window.location.href.indexOf('?') === -1 ? '?printable=yes' : '&printable=yes');
nav.className = 'pvz-board-side-nav';
nav.setAttribute('aria-label', '위키 보조창');
nav.appendChild(pvzCreateSideSection('둘러보기', [
{ label: '최근 바뀜', href: pvzGetUrl('Special:RecentChanges') },
{ label: '임의 문서', href: pvzGetUrl('Special:Random') },
{ label: '분류', href: pvzGetUrl('Special:Categories') },
{ label: '미디어위키 도움말', href: pvzGetUrl('Help:Contents') }
]));
nav.appendChild(pvzCreateSideSection('문서 도구', [
{ label: '새 문서', href: pvzGetUrl('Special:NewPages') },
{ label: '파일 올리기', href: pvzGetUrl('Special:Upload') },
{ label: '여기로 링크하는 문서', href: pvzGetUrl('Special:WhatLinksHere/' + pageName) },
{ label: '인쇄용 판', href: printableHref },
{ label: '문서 정보', href: pvzGetUrl(pageName) + '?action=info' }
]));
nav.appendChild(pvzCreateSideSection('위키 관리', [
{ label: '특수 문서', href: pvzGetUrl('Special:SpecialPages') },
{ label: '핵심 설정 관리', href: pvzGetUrl('Special:ManageWiki/settings') },
{ label: '확장 기능 관리', href: pvzGetUrl('Special:ManageWiki/extensions') },
{ label: '이름공간 관리', href: pvzGetUrl('Special:ManageWiki/namespaces') },
{ label: '백업 관리', href: pvzGetUrl('Special:DataDump') }
]));
return nav;
}
function pvzCreateRelatedPanel() {
var panel = document.createElement('aside');
var title = document.createElement('div');
var list = document.createElement('div');
panel.id = 'pvz-board-related-panel';
panel.className = 'pvz-board-related-panel';
panel.setAttribute('aria-label', '관련 문서');
title.className = 'pvz-board-related-title';
title.textContent = '관련 문서';
list.id = 'pvz-board-related-list';
list.className = 'pvz-board-related-list';
panel.appendChild(title);
panel.appendChild(list);
return panel;
}
function pvzCreateRelatedCard(titleText, href, thumb) {
var link = document.createElement('a');
var media = document.createElement('div');
var titleBar = document.createElement('div');
var title = document.createElement('span');
link.className = 'pvz-related-card';
link.href = href;
media.className = 'pvz-related-card-media';
if (thumb) {
var img = document.createElement('img');
img.src = thumb;
img.alt = '';
img.loading = 'lazy';
media.appendChild(img);
}
titleBar.className = 'pvz-related-card-titlebar';
title.className = 'pvz-related-card-title';
title.textContent = titleText || '문서 이름';
titleBar.appendChild(title);
link.appendChild(media);
link.appendChild(titleBar);
return link;
}
function pvzCollectRelatedTitles(limit) {
var current = pvzGetPageName().replace(/_/g, ' ');
var contentText = document.getElementById('mw-content-text');
var links = contentText ? contentText.querySelectorAll('a[href]') : [];
var seen = {};
var results = [];
pvzArray(links).forEach(function (link) {
var href = link.getAttribute('href') || '';
var title = '';
if (!href || results.length >= limit) return;
if (href.indexOf('#') === 0) return;
if (href.indexOf('/wiki/') === 0) {
title = decodeURIComponent(href.replace(/^\/wiki\//, '').split('#')[0].split('?')[0]).replace(/_/g, ' ');
} else if (link.dataset && link.dataset.title) {
title = String(link.dataset.title).replace(/_/g, ' ');
}
if (!title || title === current) return;
if (/^(Special|File|파일|Category|분류|Template|틀|Help|User|사용자|Talk|토론):/i.test(title)) return;
if (seen[title]) return;
seen[title] = true;
results.push(title);
});
return results.slice(0, limit);
}
function pvzRenderRelatedCards(items) {
var list = document.getElementById('pvz-board-related-list');
if (!list) return;
list.innerHTML = '';
if (!items || !items.length) items = [{ title: '대문', href: pvzGetUrl('대문'), thumb: '' }];
items.forEach(function (item) { list.appendChild(pvzCreateRelatedCard(item.title, item.href, item.thumb)); });
}
function pvzLoadRelatedDocs() {
var titles = pvzCollectRelatedTitles(12);
var url;
pvzRenderRelatedCards(titles.map(function (title) { return { title: title, href: pvzGetUrl(title), thumb: '' }; }));
if (!titles.length || !window.fetch) return;
url = pvzGetApiUrl() + '?action=query&format=json&formatversion=2&prop=pageimages&piprop=thumbnail&pithumbsize=260&titles=' + encodeURIComponent(titles.join('|'));
fetch(url, { credentials: 'same-origin' })
.then(function (res) { return res.json(); })
.then(function (data) {
var pages = data && data.query && Array.isArray(data.query.pages) ? data.query.pages : [];
var map = {};
pages.forEach(function (page) { if (page && page.title) map[page.title] = page.thumbnail && page.thumbnail.source ? page.thumbnail.source : ''; });
pvzRenderRelatedCards(titles.map(function (title) { return { title: title, href: pvzGetUrl(title), thumb: map[title] || '' }; }));
})
.catch(function () {});
}
function pvzSetRelatedOpen(open) {
document.documentElement.classList.toggle('pvz-related-open', !!open);
if (document.body) document.body.classList.toggle('pvz-related-open', !!open);
var button = document.getElementById('pvz-side-toggle');
if (button) {
button.setAttribute('aria-expanded', open ? 'true' : 'false');
button.setAttribute('aria-label', open ? '관련 문서 닫기' : '관련 문서 열기');
button.setAttribute('title', open ? '관련 문서 닫기' : '관련 문서 열기');
button.innerHTML = '';
button.appendChild(pvzCreateIcon(open ? 'pvz-icon-chevron-left' : 'pvz-icon-chevron-right'));
}
}
function pvzEnsureSideToggle() {
var button = document.getElementById('pvz-side-toggle');
if (button || !document.body) return;
button = document.createElement('button');
button.type = 'button';
button.id = 'pvz-side-toggle';
button.setAttribute('aria-expanded', 'false');
button.setAttribute('aria-label', '관련 문서 열기');
button.setAttribute('title', '관련 문서 열기');
button.appendChild(pvzCreateIcon('pvz-icon-chevron-right'));
document.body.appendChild(button);
}
function pvzSetAlertsOpen(open) {
var wrap = document.getElementById('pvz-alerts-wrap');
if (wrap) wrap.classList.toggle('is-open', !!open);
}
function pvzToggleAlertsOpen() {
var wrap = document.getElementById('pvz-alerts-wrap');
if (!wrap) return;
pvzSetAlertsOpen(!wrap.classList.contains('is-open'));
}
function pvzHeadingText(heading) {
var clone;
if (!heading) return '';
clone = heading.cloneNode(true);
clone.querySelectorAll('.mw-editsection, .mw-headline-number, .toctogglecheckbox').forEach(function (el) {
el.remove();
});
return pvzText(clone);
}
function pvzSafeAnchor(text, index) {
var id = String(text || '')
.trim()
.replace(/\s+/g, '-')
.replace(/[^\w가-힣ぁ-んァ-ン一-龥-]/g, '');
if (!id) id = 'section-' + index;
return 'pvz-section-' + id + '-' + index;
}
function pvzHeadingId(heading, text, index) {
var headline = heading.querySelector ? heading.querySelector('.mw-headline') : null;
var id;
if (headline && headline.id) return headline.id;
if (heading.id) return heading.id;
id = pvzSafeAnchor(text, index);
heading.id = id;
return id;
}
function pvzSkipHeading(heading) {
var text = pvzHeadingText(heading);
if (!text) return true;
if (
heading.closest('.pvz-custom-toc-wrap') ||
heading.closest('.toc') ||
heading.closest('#toc') ||
heading.closest('table') ||
heading.closest('.infobox') ||
heading.closest('.navbox') ||
heading.closest('.metadata') ||
heading.closest('.catlinks') ||
heading.closest('.mw-references-wrap') ||
heading.closest('.reference')
) {
return true;
}
return text === '목차' || text === 'Contents' || text === '각주' ||
text === 'References' || text === '분류';
}
function pvzIsTextParagraph(el) {
if (!el || el.tagName !== 'P') return false;
return (el.textContent || '').replace(/\s+/g, ' ').trim().length > 0;
}
function pvzCreateCustomToc() {
var contentText = document.getElementById('mw-content-text');
var contentRoot = contentText && (
contentText.querySelector('.mw-parser-output') || contentText
);
var existingWrap;
var nativeTocs;
var headings;
var leadParagraph;
var wrap;
var toc;
var head;
var title;
var toggle;
var list;
var counters = [0, 0, 0, 0, 0];
function getDepth(heading) {
var level = parseInt(heading.tagName.replace('H', ''), 10);
var depth = level - 1;
if (depth < 1) depth = 1;
if (depth > 5) depth = 5;
return depth;
}
function getNumber(depth) {
var i;
var parts = [];
counters[depth - 1] += 1;
for (i = depth; i < counters.length; i += 1) {
counters[i] = 0;
}
for (i = 0; i < depth; i += 1) {
if (!counters[i]) counters[i] = 1;
parts.push(counters[i]);
}
return parts.join('.');
}
function isExcludedLeadContainer(element) {
if (!element || !element.matches) return false;
return element.matches(
'table, figure, .thumb, .tright, .tleft, .floatright, .floatleft, .floatnone, ' +
'.infobox, .pvz-cp-infobox, .pvz-character-infobox, .character-infobox, ' +
'.navbox, .metadata, .catlinks, .mw-references-wrap, ' +
'.pvz-message, .pvz-custom-toc-wrap, #toc, .toc'
);
}
function isLeadParagraph(element) {
var clone;
var text;
/*
첫 문단은 .mw-parser-output의 직접 자식인 <p>만 인정한다.
틀이나 정보상자 내부의 <p>를 첫 문장으로 오인하지 않는다.
*/
if (!element || element.parentNode !== contentRoot || element.tagName !== 'P') {
return false;
}
if (isExcludedLeadContainer(element)) return false;
/*
TemplateStyles/ResourceLoader가 넣은 style 요소나
CSS 전용 문단은 실제 본문 문단으로 취급하지 않는다.
*/
if (
element.querySelector(
'style, script, template, noscript, ' +
'link[rel="mw-deduplicated-inline-style"], ' +
'[data-mw-deduplicate]'
)
) {
return false;
}
clone = element.cloneNode(true);
clone.querySelectorAll(
'figure, .thumb, .mw-file-description, .mw-default-size, ' +
'.mw-image-border, .reference, sup, .mw-editsection, ' +
'style, script, template, noscript, link, [data-mw-deduplicate]'
).forEach(function (node) {
node.remove();
});
text = (clone.textContent || '').replace(/\s+/g, ' ').trim();
/*
일부 파서 구조에서는 CSS가 순수 텍스트 노드로 남을 수 있으므로
대표적인 CSS 시작 패턴도 추가로 제외한다.
*/
if (
/^(?:\.mw-parser-output\b|@media\b|@supports\b|@layer\b|:root\b)/.test(text) &&
/[{};]/.test(text)
) {
return false;
}
return text.length > 0;
}
function findLeadParagraph() {
var children;
var i;
var child;
if (!contentRoot) return null;
children = pvzArray(contentRoot.children);
for (i = 0; i < children.length; i += 1) {
child = children[i];
if (isExcludedLeadContainer(child)) continue;
if (isLeadParagraph(child)) {
return child;
}
}
return null;
}
function placeTocAfterLead(tocWrap, paragraph) {
if (
!tocWrap ||
!paragraph ||
paragraph.parentNode !== contentRoot
) {
return false;
}
if (
tocWrap.parentNode === contentRoot &&
paragraph.nextSibling === tocWrap
) {
return true;
}
contentRoot.insertBefore(tocWrap, paragraph.nextSibling);
return true;
}
if (!contentRoot || contentRoot.querySelector('.pvz-no-custom-toc')) return;
leadParagraph = findLeadParagraph();
/*
실제 첫 본문 문단이 확인되기 전에는 목차를 임의 위치에 생성하지 않는다.
틀:작성중이나 캐릭터 정보상자 내부 문단은 첫 문단 후보에서 제외한다.
*/
if (!leadParagraph) return;
existingWrap = contentRoot.querySelector('.pvz-custom-toc-wrap');
if (existingWrap) {
placeTocAfterLead(existingWrap, leadParagraph);
return;
}
headings = pvzArray(
contentRoot.querySelectorAll('h2, h3, h4, h5, h6')
).filter(function (heading) {
return !pvzSkipHeading(heading);
});
if (headings.length < 2) return;
nativeTocs = pvzArray(
contentRoot.querySelectorAll('#toc, .toc')
).filter(function (node) {
return node && !(
node.classList &&
node.classList.contains('pvz-custom-toc')
);
});
nativeTocs.forEach(function (old) {
if (old && old.parentNode) {
old.parentNode.removeChild(old);
}
});
wrap = document.createElement('div');
wrap.className = 'pvz-custom-toc-wrap';
toc = document.createElement('nav');
toc.className = 'pvz-custom-toc';
toc.setAttribute('aria-label', '목차');
head = document.createElement('div');
head.className = 'pvz-custom-toc-head';
title = document.createElement('div');
title.className = 'pvz-custom-toc-title';
title.textContent = '목차';
toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'pvz-custom-toc-toggle';
toggle.setAttribute('aria-expanded', 'true');
toggle.textContent = '접기';
toggle.addEventListener('click', function (event) {
var collapsed;
event.preventDefault();
collapsed = !toc.classList.contains('is-collapsed');
toc.classList.toggle('is-collapsed', collapsed);
toggle.setAttribute(
'aria-expanded',
collapsed ? 'false' : 'true'
);
toggle.textContent = collapsed ? '펼치기' : '접기';
});
head.appendChild(title);
head.appendChild(toggle);
list = document.createElement('ol');
list.className = 'pvz-custom-toc-list';
headings.forEach(function (heading, index) {
var depth = getDepth(heading);
var level = depth + 1;
var text = pvzHeadingText(heading);
var anchor = pvzHeadingId(heading, text, index + 1);
var number = getNumber(depth);
var item = document.createElement('li');
var link = document.createElement('a');
var numberSpan = document.createElement('span');
var textSpan = document.createElement('span');
item.className =
'pvz-custom-toc-item pvz-custom-toc-level-' +
level +
' pvz-custom-toc-depth-' +
depth;
link.href = '#' + anchor;
numberSpan.className = 'pvz-custom-toc-number';
numberSpan.textContent = number;
textSpan.className = 'pvz-custom-toc-text';
textSpan.textContent = text;
link.appendChild(numberSpan);
link.appendChild(textSpan);
item.appendChild(link);
list.appendChild(item);
});
toc.appendChild(head);
toc.appendChild(list);
wrap.appendChild(toc);
placeTocAfterLead(wrap, leadParagraph);
}
function pvzEnsureRelatedPanel() {
var panel = document.getElementById('pvz-board-related-panel') ||
document.querySelector('.pvz-board-related-panel');
if (!document.body) return panel;
if (!panel) {
panel = pvzCreateRelatedPanel();
}
/* 관련 문서 창은 왼쪽 레일 안에 두지 않고 body 직속으로 둔다.
이렇게 해야 blur 막이나 본문창보다 항상 위에 올라올 수 있다. */
if (panel.parentNode !== document.body) {
document.body.appendChild(panel);
}
return panel;
}
function pvzBuildBoard() {
var contentText = document.getElementById('mw-content-text');
var boardRoot;
var frameOuter;
var frameThird;
var frameSecond;
var frameInner;
var boardBody;
var paper;
var paperMain;
var paperMeta;
var sideNav;
var relatedPanel;
var leftRail;
var logo;
var catlinks;
var firstChild;
if (!contentText || !document.body) return;
pvzEnsureSideToggle();
if (document.body.classList.contains('pvz-board-ready')) {
logo = document.getElementById('pvz-page-logo-link');
boardRoot = document.getElementById('bodyContent') || document.querySelector('#content') || contentText.parentElement;
if (boardRoot && boardRoot.classList.contains('pvz-board-root')) {
leftRail = boardRoot.querySelector('.pvz-board-left-rail');
sideNav = boardRoot.querySelector('.pvz-board-side-nav');
relatedPanel = pvzEnsureRelatedPanel();
if (!leftRail) { leftRail = document.createElement('div'); leftRail.className = 'pvz-board-left-rail'; boardRoot.insertBefore(leftRail, boardRoot.firstChild); }
if (logo && logo.parentNode !== leftRail) leftRail.appendChild(logo);
if (!sideNav) leftRail.appendChild(pvzCreateSideNav());
relatedPanel = pvzEnsureRelatedPanel();
}
pvzLoadRelatedDocs();
pvzCreateCustomToc();
return;
}
boardRoot = document.getElementById('bodyContent') || document.querySelector('#content') || contentText.parentElement;
if (!boardRoot) return;
boardRoot.classList.add('pvz-board-root');
logo = document.getElementById('pvz-page-logo-link');
if (logo && logo.parentNode !== boardRoot) boardRoot.insertBefore(logo, boardRoot.firstChild);
frameOuter = document.createElement('div'); frameOuter.className = 'pvz-board-frame-outer-real';
frameThird = document.createElement('div'); frameThird.className = 'pvz-board-frame-third-real';
frameSecond = document.createElement('div'); frameSecond.className = 'pvz-board-frame-second-real';
frameInner = document.createElement('div'); frameInner.className = 'pvz-board-frame-inner-real';
boardBody = document.createElement('div'); boardBody.className = 'pvz-board-body';
paper = document.createElement('div'); paper.className = 'pvz-board-paper';
paperMain = document.createElement('div'); paperMain.className = 'pvz-board-paper-main';
paperMeta = document.createElement('div'); paperMeta.className = 'pvz-board-paper-meta';
sideNav = pvzCreateSideNav(); relatedPanel = pvzEnsureRelatedPanel();
leftRail = document.createElement('div'); leftRail.className = 'pvz-board-left-rail';
if (logo) leftRail.appendChild(logo);
leftRail.appendChild(sideNav);
firstChild = boardRoot.firstChild;
boardRoot.insertBefore(leftRail, firstChild || null);
boardRoot.insertBefore(frameOuter, leftRail.nextSibling || null);
paperMain.appendChild(contentText);
catlinks = document.getElementById('catlinks') || document.querySelector('.catlinks');
if (catlinks && !paperMeta.contains(catlinks)) paperMeta.appendChild(catlinks);
paper.appendChild(paperMain); paper.appendChild(paperMeta);
boardBody.appendChild(pvzCreateBoardHead()); boardBody.appendChild(paper);
frameInner.appendChild(boardBody); frameSecond.appendChild(frameInner); frameThird.appendChild(frameSecond); frameOuter.appendChild(frameThird);
document.body.classList.add('pvz-board-ready');
document.documentElement.classList.add('pvz-desktop-booted');
pvzLoadRelatedDocs();
pvzCreateCustomToc();
}
function pvzMarkBoardActionActive(target) {
var button = pvzClosest(target, '.pvz-board-btn, .pvz-board-more > summary');
if (!button) return;
document.querySelectorAll('.pvz-board-btn.is-clicked, .pvz-board-more > summary.is-clicked').forEach(function (el) {
el.classList.remove('is-clicked');
});
button.classList.add('is-clicked');
}
function pvzBindEvents() {
if (window.PVZ_VECTOR_EVENTS_BOUND) return;
window.PVZ_VECTOR_EVENTS_BOUND = true;
document.addEventListener('click', function (event) {
var panelButton = pvzClosest(event.target, '.pvz-mobile-nav-button[data-pvz-mobile-panel], .pvz-desktop-util-button[data-pvz-mobile-panel]');
var panel = pvzClosest(event.target, '.pvz-mobile-sheet');
var panelBackdrop = pvzClosest(event.target, '#pvz-mobile-panel-backdrop');
var suggestionsPanel = pvzClosest(event.target, '#pvz-search-suggestions');
var searchInput = pvzClosest(event.target, '#pvz-mobile-search-input');
var alertsButton = pvzClosest(event.target, '#pvz-alerts-button');
var alertsWrap = pvzClosest(event.target, '#pvz-alerts-wrap');
var desktopAlertsWrap = pvzClosest(event.target, '#pvz-desktop-alerts-wrap');
var sideToggle = pvzClosest(event.target, '#pvz-side-toggle');
var boardAction = pvzClosest(event.target, '.pvz-board-btn, .pvz-board-more > summary');
var name;
if (sideToggle) {
event.preventDefault();
event.stopPropagation();
pvzSetRelatedOpen(!document.body.classList.contains('pvz-related-open'));
return;
}
if (boardAction) {
pvzMarkBoardActionActive(boardAction);
}
if (alertsButton) {
event.preventDefault();
event.stopPropagation();
pvzToggleAlertsOpen();
return;
}
if (panelButton) {
event.preventDefault();
event.stopPropagation();
name = panelButton.getAttribute('data-pvz-mobile-panel');
pvzTogglePanel(name);
return;
}
if (panelBackdrop) {
event.preventDefault();
pvzClosePanels();
return;
}
if (!suggestionsPanel && !searchInput) pvzCloseSuggestions();
if (!alertsWrap) pvzSetAlertsOpen(false);
if (!desktopAlertsWrap) {
var desktopAlerts = document.getElementById('pvz-desktop-alerts-wrap');
if (desktopAlerts) desktopAlerts.classList.remove('is-open');
}
if (document.body.classList.contains('pvz-related-open') && !pvzClosest(event.target, '.pvz-board-related-panel') && !sideToggle) {
pvzSetRelatedOpen(false);
}
if (!panel && !panelButton && !panelBackdrop) pvzClosePanels();
}, true);
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
pvzCloseSuggestions();
pvzClosePanels();
pvzSetAlertsOpen(false);
var desktopAlerts = document.getElementById('pvz-desktop-alerts-wrap');
if (desktopAlerts) desktopAlerts.classList.remove('is-open');
pvzSetRelatedOpen(false);
}
});
window.addEventListener('resize', function () {
pvzSyncClasses();
pvzPositionSearchPanel();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzLoadRelatedDocs();
});
window.addEventListener('scroll', function () {
pvzPositionSearchPanel();
}, true);
}
function pvzInit() {
pvzSyncClasses();
pvzApplyPreferences();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzBuildBoard();
pvzBindEvents();
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 120);
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 600);
window.setTimeout(function () {
pvzSyncClasses();
pvzEnsureTopbar();
pvzEnsureDesktopUtilityBar();
pvzEnsureLogo();
pvzCreateCustomToc();
}, 1400);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function () {
pvzInit();
});
}
pvzReady(pvzInit);
}());