(Install package: clbiwiki-portal-frame-pixel-stack-20260717 / js/BottomGuideNav.js) |
(Install package: clbiwiki-portal-frame-fixed / js/BottomGuideNav.js) 태그: 수동 되돌리기 |
||
| (같은 사용자의 중간 판 3개는 보이지 않습니다) | |||
| 1번째 줄: | 1번째 줄: | ||
/* ========================================= | /* ========================================= | ||
BottomGuideNav | BottomGuideNav | ||
대문 하단 「길라잡이」 | 대문 하단 「길라잡이」 canvas 픽셀 버튼 | ||
========================================= */ | ========================================= */ | ||
(function (window, document, mw) { | (function (window, document, mw) { | ||
'use strict'; | 'use strict'; | ||
var COMPONENT = 'category-guide-nav'; | var COMPONENT = 'category-guide-nav'; | ||
var retryTimer = 0; | var retryTimer = 0; | ||
var geometryWaitTimer = 0; | var geometryWaitTimer = 0; | ||
var resizeTimer = 0; | var resizeTimer = 0; | ||
var observer = null; | var observer = null; | ||
var manifestoIntroPromise = null; | var manifestoIntroPromise = null; | ||
| 58번째 줄: | 16번째 줄: | ||
var MANIFESTO_INTRO_VERSION = '20260713-divider-equal-gap-005'; | var MANIFESTO_INTRO_VERSION = '20260713-divider-equal-gap-005'; | ||
var GUIDE_DUMMY_ITEMS = [ | var GUIDE_DUMMY_ITEMS = [ | ||
{ key: 'start', label: '처음 오셨다면' }, | { key:'start', label:'처음 오셨다면' }, | ||
{ key: 'world', label: '세계관' }, | { key:'world', label:'세계관' }, | ||
{ key: 'era', label: '시대' }, | { key:'era', label:'시대' }, | ||
{ key: 'people', label: '인물' }, | { key:'people', label:'인물' }, | ||
{ key: 'anecdote', label: '에넥도트' }, | { key:'anecdote', label:'에넥도트' }, | ||
{ key: 'participate', label: '참여 안내' } | { key:'participate', label:'참여 안내' } | ||
]; | ]; | ||
function api() { | |||
return window.CLBI && window.CLBI.frame; | |||
} | |||
function revealManifesto(root) { | function revealManifesto(root) { | ||
| 74번째 줄: | 36번째 줄: | ||
var copy; | var copy; | ||
document.documentElement.classList.remove('manifesto-intro-pending'); | document.documentElement.classList.remove('manifesto-intro-pending'); | ||
document.documentElement.classList.remove('manifesto-intro-active'); | document.documentElement.classList.remove('manifesto-intro-active'); | ||
if (!portal) return; | if (!portal) return; | ||
| 84번째 줄: | 44번째 줄: | ||
title = portal.querySelector('.main-manifesto-title'); | title = portal.querySelector('.main-manifesto-title'); | ||
copy = portal.querySelector('.main-manifesto-copy'); | copy = portal.querySelector('.main-manifesto-copy'); | ||
if (title) { | if (title) { | ||
title.style.opacity = '1'; | title.style.opacity = '1'; | ||
| 90번째 줄: | 49번째 줄: | ||
} | } | ||
if (copy) copy.style.visibility = 'visible'; | if (copy) copy.style.visibility = 'visible'; | ||
Array.prototype.forEach.call(portal.querySelectorAll('.manifesto-line'), function (line) { | Array.prototype.forEach.call(portal.querySelectorAll('.manifesto-line'), function (line) { | ||
line.style.height = '26px'; | line.style.height = '26px'; | ||
| 120번째 줄: | 78번째 줄: | ||
} | } | ||
} | } | ||
if (manifestoIntroPromise) return manifestoIntroPromise; | if (manifestoIntroPromise) return manifestoIntroPromise; | ||
| 127번째 줄: | 84번째 줄: | ||
var settled = false; | var settled = false; | ||
var timeout = 0; | var timeout = 0; | ||
function settle(ok) { | function settle(ok) { | ||
if (settled) return; | if (settled) return; | ||
| 138번째 줄: | 94번째 줄: | ||
resolve(ok); | resolve(ok); | ||
} | } | ||
script.src = '/index.php?title=' + encodeURIComponent('MediaWiki:ManifestoIntro.js') + | script.src = '/index.php?title=' + encodeURIComponent('MediaWiki:ManifestoIntro.js') + | ||
'&action=raw&ctype=text/javascript&v=' + encodeURIComponent(MANIFESTO_INTRO_VERSION) + | '&action=raw&ctype=text/javascript&v=' + encodeURIComponent(MANIFESTO_INTRO_VERSION) + | ||
| 157번째 줄: | 112번째 줄: | ||
} | } | ||
}; | }; | ||
script.onerror = function () { | script.onerror = function () { settle(false); }; | ||
timeout = window.setTimeout(function () { | timeout = window.setTimeout(function () { | ||
if (script.parentNode) script.parentNode.removeChild(script); | if (script.parentNode) script.parentNode.removeChild(script); | ||
settle(false); | settle(false); | ||
}, MANIFESTO_LOAD_TIMEOUT_MS); | }, MANIFESTO_LOAD_TIMEOUT_MS); | ||
(document.head || document.documentElement).appendChild(script); | (document.head || document.documentElement).appendChild(script); | ||
}); | }); | ||
return manifestoIntroPromise; | return manifestoIntroPromise; | ||
} | } | ||
| 473번째 줄: | 132번째 줄: | ||
panel.className = 'main-body-panel'; | panel.className = 'main-body-panel'; | ||
panel.setAttribute('data-main-body-panel-generated', '1'); | panel.setAttribute('data-main-body-panel-generated', '1'); | ||
if (topMount && topMount.parentNode) { | if (topMount && topMount.parentNode) { | ||
if (topMount.nextSibling) | if (topMount.nextSibling) topMount.parentNode.insertBefore(panel, topMount.nextSibling); | ||
else topMount.parentNode.appendChild(panel); | |||
} else { | } else { | ||
portal.appendChild(panel); | portal.appendChild(panel); | ||
} | } | ||
} | } | ||
if (!well) { | if (!well) { | ||
well = document.createElement('div'); | well = document.createElement('div'); | ||
| 493번째 줄: | 147번째 줄: | ||
panel.insertBefore(well, panel.firstChild); | panel.insertBefore(well, panel.firstChild); | ||
} | } | ||
if (manifesto && manifesto.parentNode !== well) well.insertBefore(manifesto, well.firstChild); | |||
if (manifesto && manifesto.parentNode !== well) | return { panel:panel, well:well }; | ||
return { | |||
} | } | ||
function setBodyScreenHidden(screen, hidden) { | function setBodyScreenHidden(screen, hidden) { | ||
if (!screen) return; | if (!screen) return; | ||
screen.hidden = !!hidden; | |||
screen.hidden = hidden; | |||
screen.setAttribute('aria-hidden', hidden ? 'true' : 'false'); | screen.setAttribute('aria-hidden', hidden ? 'true' : 'false'); | ||
screen.classList.toggle('is-main-body-screen-hidden', !!hidden); | |||
} | } | ||
| 520번째 줄: | 166번째 줄: | ||
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-anchor'), function (anchor) { | Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-anchor'), function (anchor) { | ||
anchor.setAttribute('href', href); | anchor.setAttribute('href', href); | ||
anchor.setAttribute('aria-label', label); | anchor.setAttribute('aria-label', label); | ||
anchor.setAttribute('aria-pressed', guideActive ? 'true' : 'false'); | anchor.setAttribute('aria-pressed', guideActive ? 'true' : 'false'); | ||
| 526번째 줄: | 171번째 줄: | ||
anchor.removeAttribute('aria-current'); | anchor.removeAttribute('aria-current'); | ||
}); | }); | ||
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-label'), function (text) { | Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-label'), function (text) { | ||
text.textContent = label; | text.textContent = label; | ||
}); | }); | ||
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-nav'), function (nav) { | Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-nav'), function (nav) { | ||
nav.setAttribute('aria-label', label); | nav.setAttribute('aria-label', label); | ||
| 538번째 줄: | 181번째 줄: | ||
function ensureGuideDummyMenu(guide) { | function ensureGuideDummyMenu(guide) { | ||
var menu; | var menu; | ||
if (!guide) return null; | if (!guide) return null; | ||
menu = guide.querySelector('[data-guide-dummy-menu="1"]'); | menu = guide.querySelector('[data-guide-dummy-menu="1"]'); | ||
if (menu) return menu; | if (menu) return menu; | ||
| 548번째 줄: | 189번째 줄: | ||
menu.setAttribute('data-guide-dummy-menu', '1'); | menu.setAttribute('data-guide-dummy-menu', '1'); | ||
menu.setAttribute('aria-label', '길라잡이 항목 예시'); | menu.setAttribute('aria-label', '길라잡이 항목 예시'); | ||
GUIDE_DUMMY_ITEMS.forEach(function (item, index) { | GUIDE_DUMMY_ITEMS.forEach(function (item, index) { | ||
var wrapper = document.createElement('div'); | var wrapper = document.createElement('div'); | ||
| 555번째 줄: | 195번째 줄: | ||
var label = document.createElement('span'); | var label = document.createElement('span'); | ||
var arrow = document.createElement('span'); | var arrow = document.createElement('span'); | ||
wrapper.className = 'main-guide-menu-item'; | wrapper.className = 'main-guide-menu-item'; | ||
wrapper.style.setProperty('--guide-menu-order', String(index)); | wrapper.style.setProperty('--guide-menu-order', String(index)); | ||
button.type = 'button'; | button.type = 'button'; | ||
button.className = 'main-guide-menu-button'; | button.className = 'main-guide-menu-button'; | ||
button.setAttribute('data-guide-dummy-button', item.key); | button.setAttribute('data-guide-dummy-button', item.key); | ||
button.setAttribute('aria-label', item.label + ' (더미 버튼)'); | button.setAttribute('aria-label', item.label + ' (더미 버튼)'); | ||
number.className = 'main-guide-menu-number'; | number.className = 'main-guide-menu-number'; | ||
number.setAttribute('aria-hidden', 'true'); | number.setAttribute('aria-hidden', 'true'); | ||
number.textContent = String(index + 1).padStart(2, '0'); | number.textContent = String(index + 1).padStart(2, '0'); | ||
label.className = 'main-guide-menu-label'; | label.className = 'main-guide-menu-label'; | ||
label.textContent = item.label; | label.textContent = item.label; | ||
arrow.className = 'main-guide-menu-arrow'; | arrow.className = 'main-guide-menu-arrow'; | ||
arrow.setAttribute('aria-hidden', 'true'); | arrow.setAttribute('aria-hidden', 'true'); | ||
arrow.textContent = '›'; | arrow.textContent = '›'; | ||
button.appendChild(number); | button.appendChild(number); | ||
button.appendChild(label); | button.appendChild(label); | ||
button.appendChild(arrow); | button.appendChild(arrow); | ||
button.addEventListener('click', function (event) { | button.addEventListener('click', function (event) { event.preventDefault(); }); | ||
wrapper.appendChild(button); | wrapper.appendChild(button); | ||
menu.appendChild(wrapper); | menu.appendChild(wrapper); | ||
}); | }); | ||
guide.insertBefore(menu, guide.firstChild); | guide.insertBefore(menu, guide.firstChild); | ||
return menu; | return menu; | ||
| 591번째 줄: | 222번째 줄: | ||
function restartGuideMenuAnimation(guide) { | function restartGuideMenuAnimation(guide) { | ||
if (!guide || !guide.querySelector('[data-guide-dummy-menu="1"]')) return; | if (!guide || !guide.querySelector('[data-guide-dummy-menu="1"]')) return; | ||
guide.classList.remove('is-guide-menu-entering'); | guide.classList.remove('is-guide-menu-entering'); | ||
void guide.offsetWidth; | void guide.offsetWidth; | ||
guide.classList.add('is-guide-menu-entering'); | guide.classList.add('is-guide-menu-entering'); | ||
| 604번째 줄: | 233번째 줄: | ||
var panels; | var panels; | ||
var active = layout.well.getAttribute('data-active-main-body-screen') || | var active = layout.well.getAttribute('data-active-main-body-screen') || | ||
portal.getAttribute('data-main-body-screen') || | portal.getAttribute('data-main-body-screen') || 'manifesto'; | ||
if (manifesto) { | if (manifesto) { | ||
| 612번째 줄: | 240번째 줄: | ||
if (!manifesto.id) manifesto.id = 'main-manifesto-screen'; | if (!manifesto.id) manifesto.id = 'main-manifesto-screen'; | ||
} | } | ||
if (!guide) { | if (!guide) { | ||
guide = document.createElement('section'); | guide = document.createElement('section'); | ||
| 623번째 줄: | 250번째 줄: | ||
layout.well.appendChild(guide); | layout.well.appendChild(guide); | ||
} else { | } else { | ||
guide.classList.add('main-body-screen' | guide.classList.add('main-body-screen', 'main-guide-screen'); | ||
if (!guide.id) guide.id = 'main-guide-screen'; | if (!guide.id) guide.id = 'main-guide-screen'; | ||
} | } | ||
ensureGuideDummyMenu(guide); | ensureGuideDummyMenu(guide); | ||
panels = Array.prototype.slice.call( | panels = Array.prototype.slice.call( | ||
layout.well.querySelectorAll('.main-body-screen[data-main-body-screen]') | layout.well.querySelectorAll('.main-body-screen[data-main-body-screen]') | ||
); | ); | ||
if (active !== 'guide') active = 'manifesto'; | if (active !== 'guide') active = 'manifesto'; | ||
if (!panels.some(function (panel) { | if (!panels.some(function (panel) { | ||
return panel.getAttribute('data-main-body-screen') === active; | return panel.getAttribute('data-main-body-screen') === active; | ||
})) | })) active = 'manifesto'; | ||
panels.forEach(function (panel) { | panels.forEach(function (panel) { | ||
setBodyScreenHidden( | setBodyScreenHidden(panel, panel.getAttribute('data-main-body-screen') !== active); | ||
}); | }); | ||
layout.well.setAttribute('data-main-body-screen-controller-ready', '1'); | layout.well.setAttribute('data-main-body-screen-controller-ready', '1'); | ||
layout.well.setAttribute('data-active-main-body-screen', active); | layout.well.setAttribute('data-active-main-body-screen', active); | ||
| 656번째 줄: | 272번째 줄: | ||
restartGuideMenuAnimation(guide); | restartGuideMenuAnimation(guide); | ||
} | } | ||
return { | return { | ||
well: layout.well, | well:layout.well, | ||
panels: panels, | panels:panels, | ||
manifesto: manifesto, | manifesto:manifesto, | ||
guide: guide, | guide:guide, | ||
active: active | active:active | ||
}; | }; | ||
} | } | ||
| 671번째 줄: | 286번째 줄: | ||
var active; | var active; | ||
var event; | var event; | ||
if (!portal) return false; | if (!portal) return false; | ||
screens = ensureBodyScreens(portal); | screens = ensureBodyScreens(portal); | ||
screenName = screenName === 'guide' ? 'guide' : 'manifesto'; | screenName = screenName === 'guide' ? 'guide' : 'manifesto'; | ||
| 679번째 줄: | 292번째 줄: | ||
return panel.getAttribute('data-main-body-screen') === screenName; | return panel.getAttribute('data-main-body-screen') === screenName; | ||
})) return false; | })) return false; | ||
previous = screens.active; | previous = screens.active; | ||
active = screenName; | active = screenName; | ||
screens.panels.forEach(function (panel) { | screens.panels.forEach(function (panel) { | ||
setBodyScreenHidden( | setBodyScreenHidden(panel, panel.getAttribute('data-main-body-screen') !== active); | ||
}); | }); | ||
screens.well.setAttribute('data-active-main-body-screen', active); | screens.well.setAttribute('data-active-main-body-screen', active); | ||
portal.setAttribute('data-main-body-screen', active); | portal.setAttribute('data-main-body-screen', active); | ||
portal.classList.toggle('is-guide-screen-active', active === 'guide'); | portal.classList.toggle('is-guide-screen-active', active === 'guide'); | ||
updateGuideButtonState(portal, active); | updateGuideButtonState(portal, active); | ||
if (active === 'guide' && previous !== active) restartGuideMenuAnimation(screens.guide); | |||
if (active === 'guide' && previous !== active) | else if (active !== 'guide' && screens.guide) screens.guide.classList.remove('is-guide-menu-entering'); | ||
render(portal); | |||
if (previous !== active) { | if (previous !== active) { | ||
try { | try { | ||
event = new CustomEvent('mainbodyscreenchange', { | event = new CustomEvent('mainbodyscreenchange', { | ||
bubbles: true, | bubbles:true, | ||
detail: { screen: active, previous: previous } | detail:{ screen:active, previous:previous } | ||
}); | }); | ||
portal.dispatchEvent(event); | portal.dispatchEvent(event); | ||
} catch (error) {} | } catch (error) {} | ||
} | } | ||
return true; | return true; | ||
} | } | ||
| 720번째 줄: | 318번째 줄: | ||
function toggleBodyScreen(portal) { | function toggleBodyScreen(portal) { | ||
var screens; | var screens; | ||
if (!portal) return false; | if (!portal) return false; | ||
screens = ensureBodyScreens(portal); | screens = ensureBodyScreens(portal); | ||
return activateBodyScreen(portal, screens.active === 'guide' ? 'manifesto' : 'guide'); | |||
} | } | ||
| 732번째 줄: | 327번째 줄: | ||
var anchor; | var anchor; | ||
var portal; | var portal; | ||
if (!event || event.__mainGuideScreenHandled) return; | if (!event || event.__mainGuideScreenHandled) return; | ||
if (event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; | if (event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; | ||
target = event.target; | target = event.target; | ||
anchor = target && target.closest | anchor = target && target.closest | ||
| 741번째 줄: | 334번째 줄: | ||
: null; | : null; | ||
if (!anchor) return; | if (!anchor) return; | ||
portal = anchor.closest ? anchor.closest('.main-portal') : null; | portal = anchor.closest ? anchor.closest('.main-portal') : null; | ||
if (!portal || !toggleBodyScreen(portal)) return; | if (!portal || !toggleBodyScreen(portal)) return; | ||
event.__mainGuideScreenHandled = true; | event.__mainGuideScreenHandled = true; | ||
event.preventDefault(); | event.preventDefault(); | ||
event.stopPropagation(); | event.stopPropagation(); | ||
if (typeof event.stopImmediatePropagation === 'function') | if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); | ||
} | } | ||
if (window.BottomGuideNavScreenCaptureHandler) { | if (window.BottomGuideNavScreenCaptureHandler) { | ||
document.removeEventListener('click', window.BottomGuideNavScreenCaptureHandler, true); | document.removeEventListener('click', window.BottomGuideNavScreenCaptureHandler, true); | ||
| 766번째 줄: | 351번째 줄: | ||
var layout = ensureBodyPanel(portal); | var layout = ensureBodyPanel(portal); | ||
var mount = portal.querySelector('[data-component="' + COMPONENT + '"]'); | var mount = portal.querySelector('[data-component="' + COMPONENT + '"]'); | ||
ensureBodyScreens(portal); | ensureBodyScreens(portal); | ||
if (!mount) { | if (!mount) { | ||
mount = document.createElement('div'); | mount = document.createElement('div'); | ||
| 774번째 줄: | 357번째 줄: | ||
mount.setAttribute('data-bottom-guide-generated', '1'); | mount.setAttribute('data-bottom-guide-generated', '1'); | ||
} | } | ||
if (mount.parentNode !== layout.panel) layout.panel.appendChild(mount); | |||
return mount; | |||
} | |||
/ | function guideButton(seams) { | ||
var frame = api(); | |||
var d = frame.DIMENSIONS; | |||
var step = Math.floor(d.BUTTON_EDGE_BOTTOM / frame.FRAME.SLANT_RISE); | |||
var left = seams.settingsLeftOriginX + step; | |||
var right = seams.corpRightOriginX - step; | |||
return { | |||
x:Math.round(left), | |||
y:d.BUTTON_EDGE_TOP, | |||
btnW:Math.max(4, Math.round(right - left)), | |||
pixelHeight:d.BUTTON_TOTAL, | |||
slant:'both' | |||
}; | |||
} | |||
function draw(canvas, button, label, hover) { | |||
var frame = api(); | |||
var ctx = canvas.getContext('2d'); | |||
var image = frame.createImage(ctx, canvas.width, canvas.height, 'BG'); | |||
var grid; | |||
var mask; | |||
var center; | |||
frame.paintStandardFrame(image, canvas.width, canvas.height); | |||
grid = frame.buildReverseButtonPixels( | |||
button.btnW, | |||
frame.FRAME.BUTTON_FACE, | |||
button.slant, | |||
0 | |||
); | |||
frame.paintGrid(image, canvas.width, canvas.height, grid, button.x, button.y, true); | |||
ctx.putImageData(image, 0, 0); | |||
mask = frame.buttonMask( | |||
button.btnW, | |||
frame.FRAME.BUTTON_FACE - 1 - Math.floor(frame.FRAME.BUTTON_FACE / 2), | |||
button.slant, | |||
0 | |||
); | |||
center = button.x + Math.round((mask.left + mask.right) / 2); | |||
frame.drawLabel( | |||
ctx, | |||
label, | |||
center, | |||
Math.round((frame.DIMENSIONS.BUTTON_SURFACE_TOP + frame.DIMENSIONS.BUTTON_SURFACE_BOTTOM) / 2) + 1, | |||
hover, | |||
canvas.width <= 820 ? 11 : 12 | |||
); | |||
} | } | ||
function render(portal | function render(portal) { | ||
var frame = api(); | |||
var seams; | |||
var mount; | |||
var width; | |||
var guideActive; | |||
var label; | |||
var | var targetScreen; | ||
if (! | var href; | ||
var button; | |||
var nav; | |||
var inner; | |||
var canvas; | |||
var hitLayer; | |||
var anchor; | |||
var hiddenLabel; | |||
var redraw; | |||
if (!frame || !frame.FRAME || typeof frame.readRenderWidth !== 'function') { | |||
window.clearTimeout(geometryWaitTimer); | window.clearTimeout(geometryWaitTimer); | ||
geometryWaitTimer = window.setTimeout(function () { | geometryWaitTimer = window.setTimeout(function () { | ||
geometryWaitTimer = 0; | geometryWaitTimer = 0; | ||
render(portal | render(portal); | ||
}, 50); | }, 50); | ||
return false; | return false; | ||
} | } | ||
seams = frame.seams; | |||
if (!seams || !seams.width) return false; | |||
mount = ensureMount(portal); | |||
width = seams.width || frame.readRenderWidth(mount); | |||
if (!width) return false; | if (!width) return false; | ||
portal. | guideActive = portal.getAttribute('data-main-body-screen') === 'guide'; | ||
label = guideActive ? '돌아가기' : '길라잡이'; | |||
targetScreen = guideActive ? 'manifesto' : 'guide'; | |||
href = guideActive ? '#main-manifesto-screen' : '#main-guide-screen'; | |||
button = guideButton(seams); | |||
if ( | |||
mount.getAttribute('data-bottom-guide-width') === String(width) && | |||
mount.getAttribute('data-bottom-guide-label') === label && | |||
mount.getAttribute('data-bottom-guide-scale') === String(frame.getShellScale()) && | |||
mount.querySelector('.portal-guide-canvas') | |||
) { | |||
return true; | |||
} | |||
nav = document.createElement('nav'); | |||
nav.className = 'portal-category-nav portal-guide-nav portal-frame-canvas-nav is-continuous-weld'; | |||
nav.setAttribute('aria-label', label); | |||
nav.__frameHover = false; | |||
inner = document.createElement('div'); | |||
inner.className = 'portal-frame-counter-inner portal-guide-counter-inner'; | |||
inner.style.width = width + 'px'; | |||
canvas = frame.createCanvas('portal-frame-canvas portal-guide-canvas', width, frame.DIMENSIONS.ROW_HEIGHT); | |||
canvas.setAttribute('data-frame-width', String(width)); | |||
canvas.setAttribute('data-frame-height', String(frame.DIMENSIONS.ROW_HEIGHT)); | |||
hitLayer = document.createElement('div'); | |||
hitLayer.className = 'portal-frame-hit-layer'; | |||
hitLayer.style.width = width + 'px'; | |||
hitLayer.style.height = frame.DIMENSIONS.ROW_HEIGHT + 'px'; | |||
anchor. | anchor = document.createElement('a'); | ||
anchor. | anchor.className = 'portal-frame-hit portal-guide-anchor'; | ||
anchor.href = href; | |||
anchor.setAttribute('aria-label', label); | anchor.setAttribute('aria-label', label); | ||
anchor.setAttribute('role', 'button'); | anchor.setAttribute('role', 'button'); | ||
| 862번째 줄: | 483번째 줄: | ||
anchor.setAttribute('data-main-body-target', targetScreen); | anchor.setAttribute('data-main-body-target', targetScreen); | ||
anchor.setAttribute('aria-controls', 'main-manifesto-screen main-guide-screen'); | anchor.setAttribute('aria-controls', 'main-manifesto-screen main-guide-screen'); | ||
anchor.style.left = button.x + 'px'; | |||
anchor.style.top = button.y + 'px'; | |||
anchor.style.width = (button.btnW + 2) + 'px'; | |||
anchor.style.height = button.pixelHeight + 'px'; | |||
hiddenLabel = document.createElement('span'); | |||
hiddenLabel.className = 'portal-frame-sr-label portal-guide-label'; | |||
hiddenLabel.textContent = label; | |||
anchor.appendChild(hiddenLabel); | |||
redraw = function () { draw(canvas, button, label, nav.__frameHover); }; | |||
anchor.addEventListener('mouseenter', function () { | |||
nav.__frameHover = true; | |||
redraw(); | |||
}); | |||
anchor.addEventListener('mouseleave', function () { | |||
nav.__frameHover = false; | |||
redraw(); | |||
}); | |||
anchor.addEventListener('focus', function () { | |||
nav.__frameHover = true; | |||
redraw(); | |||
}); | |||
anchor.addEventListener('blur', function () { | |||
nav.__frameHover = false; | |||
redraw(); | |||
}); | |||
anchor.addEventListener('click', function (event) { | anchor.addEventListener('click', function (event) { | ||
if ( | if (event && (event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) return; | ||
if (toggleBodyScreen(portal) && event) event.preventDefault(); | |||
if (toggleBodyScreen(portal) | |||
}); | }); | ||
hitLayer.appendChild(anchor); | |||
inner.appendChild(canvas); | |||
inner.appendChild(hitLayer); | |||
nav.appendChild(inner); | |||
mount.replaceChildren(nav); | |||
frame.applyCounterScale( | |||
[mount, nav], | |||
inner, | |||
frame.DIMENSIONS.ROW_HEIGHT | |||
); | |||
redraw(); | |||
mount.setAttribute('data-bottom-guide-rendered', 'canvas'); | |||
mount.setAttribute('data-bottom-guide-rendered', ' | |||
mount.setAttribute('data-bottom-guide-width', String(width)); | mount.setAttribute('data-bottom-guide-width', String(width)); | ||
mount.setAttribute('data-bottom-guide-label', label); | |||
mount.setAttribute('data-bottom-guide-scale', String(frame.getShellScale())); | |||
return true; | return true; | ||
} | } | ||
| 936번째 줄: | 542번째 줄: | ||
function schedule(root, attempt) { | function schedule(root, attempt) { | ||
window.clearTimeout(retryTimer); | window.clearTimeout(retryTimer); | ||
retryTimer = window.setTimeout(function () { | retryTimer = window.setTimeout(function () { | ||
var portal = findPortal(root); | var portal = findPortal(root); | ||
if (!portal) return; | if (!portal) return; | ||
if (!render(portal) && attempt < 20) { | if (!render(portal) && attempt < 20) { | ||
schedule(root, attempt + 1); | schedule(root, attempt + 1); | ||
return; | return; | ||
} | } | ||
if (observer) observer.disconnect(); | if (observer) observer.disconnect(); | ||
if (window.ResizeObserver) { | if (window.ResizeObserver) { | ||
observer = new ResizeObserver(function () { | observer = new ResizeObserver(function () { | ||
window.clearTimeout(resizeTimer); | window.clearTimeout(resizeTimer); | ||
resizeTimer = window.setTimeout(function () { | resizeTimer = window.setTimeout(function () { render(portal); }, 60); | ||
}); | }); | ||
observer.observe(portal); | observer.observe(portal); | ||
var topMount = portal.querySelector('[data-component="category-nav"]'); | var topMount = portal.querySelector('[data-component="category-nav"]'); | ||
| 972번째 줄: | 570번째 줄: | ||
} | } | ||
revealManifesto(document); | |||
if (document.readyState === 'loading') { | if (document.readyState === 'loading') { | ||
document.addEventListener('DOMContentLoaded', function () { | document.addEventListener('DOMContentLoaded', function () { boot(document); }, { once:true }); | ||
} else { | } else { | ||
boot(document); | boot(document); | ||
} | } | ||
window.addEventListener('load', function () { boot(document); }, { once:true }); | |||
window.addEventListener('pageshow', function () { boot(document); }); | |||
window.addEventListener('load', function () { | if (api()) { | ||
api().onSeams(function () { schedule(document, 0); }); | |||
api().onScale(function () { schedule(document, 0); }); | |||
} | |||
window.addEventListener('pageshow', function () { | |||
if (mw && mw.hook) { | if (mw && mw.hook) { | ||
mw.hook('wikipage.content').add(function ($content) { | mw.hook('wikipage.content').add(function ($content) { | ||
| 997번째 줄: | 589번째 줄: | ||
window.BottomGuideNav = { | window.BottomGuideNav = { | ||
version: '20260717-pixel- | version:'20260717-canvas-pixel-001', | ||
render: function () { | render:function () { | ||
var portal = document.querySelector('.main-portal'); | var portal = document.querySelector('.main-portal'); | ||
return portal ? render(portal) : false; | return portal ? render(portal) : false; | ||
}, | }, | ||
activate: function (screenName) { | activate:function (screenName) { | ||
var portal = document.querySelector('.main-portal'); | var portal = document.querySelector('.main-portal'); | ||
return portal ? activateBodyScreen(portal, screenName) : false; | return portal ? activateBodyScreen(portal, screenName) : false; | ||
}, | }, | ||
toggle: function () { | toggle:function () { | ||
var portal = document.querySelector('.main-portal'); | var portal = document.querySelector('.main-portal'); | ||
return portal ? toggleBodyScreen(portal) : false; | return portal ? toggleBodyScreen(portal) : false; | ||
}, | }, | ||
status: function () { | status:function () { | ||
var portal = document.querySelector('.main-portal'); | var portal = document.querySelector('.main-portal'); | ||
var mount = portal && portal.querySelector('[data-component="' + COMPONENT + '"]'); | var mount = portal && portal.querySelector('[data-component="' + COMPONENT + '"]'); | ||
return { | return { | ||
loaded: true, | loaded:true, | ||
portal: !!portal, | portal:!!portal, | ||
mount: !!mount, | mount:!!mount, | ||
rendered: !!(mount && mount.querySelector('.portal-guide- | rendered:!!(mount && mount.querySelector('.portal-guide-canvas')), | ||
generated: !!(mount && mount.getAttribute('data-bottom-guide-generated')), | generated:!!(mount && mount.getAttribute('data-bottom-guide-generated')), | ||
panel: !!(portal && portal.querySelector('.main-body-panel')), | panel:!!(portal && portal.querySelector('.main-body-panel')), | ||
well: !!(portal && portal.querySelector('.main-body-well')), | well:!!(portal && portal.querySelector('.main-body-well')), | ||
insidePanel: !!(mount && mount.parentNode && mount.parentNode.classList.contains('main-body-panel')), | insidePanel:!!(mount && mount.parentNode && mount.parentNode.classList.contains('main-body-panel')), | ||
welded: !!(mount && mount.querySelector('.portal-guide-nav.is-continuous-weld')), | welded:!!(mount && mount.querySelector('.portal-guide-nav.is-continuous-weld')), | ||
activeScreen: portal ? portal.getAttribute('data-main-body-screen') || 'manifesto' : '', | activeScreen:portal ? portal.getAttribute('data-main-body-screen') || 'manifesto' : '', | ||
guideScreen: !!(portal && portal.querySelector('[data-main-body-screen="guide"]')), | guideScreen:!!(portal && portal.querySelector('[data-main-body-screen="guide"]')), | ||
width: mount ? mount.getAttribute('data-bottom-guide-width') || '' : '' | width:mount ? mount.getAttribute('data-bottom-guide-width') || '' : '' | ||
}; | }; | ||
} | } | ||
}; | }; | ||
}(window, document, window.mw)); | }(window, document, window.mw)); | ||
2026년 7월 17일 (금) 17:49 기준 최신판
/* =========================================
BottomGuideNav
대문 하단 「길라잡이」 canvas 픽셀 버튼
========================================= */
(function (window, document, mw) {
'use strict';
var COMPONENT = 'category-guide-nav';
var retryTimer = 0;
var geometryWaitTimer = 0;
var resizeTimer = 0;
var observer = null;
var manifestoIntroPromise = null;
var MANIFESTO_LOAD_TIMEOUT_MS = 5000;
var MANIFESTO_INTRO_VERSION = '20260713-divider-equal-gap-005';
var GUIDE_DUMMY_ITEMS = [
{ key:'start', label:'처음 오셨다면' },
{ key:'world', label:'세계관' },
{ key:'era', label:'시대' },
{ key:'people', label:'인물' },
{ key:'anecdote', label:'에넥도트' },
{ key:'participate', label:'참여 안내' }
];
function api() {
return window.CLBI && window.CLBI.frame;
}
function revealManifesto(root) {
var scope = root && root.querySelector ? root : document;
var portal = scope.matches && scope.matches('.main-portal')
? scope
: scope.querySelector('.main-portal') || document.querySelector('.main-portal');
var title;
var copy;
document.documentElement.classList.remove('manifesto-intro-pending');
document.documentElement.classList.remove('manifesto-intro-active');
if (!portal) return;
portal.classList.remove('manifesto-intro-prepared');
portal.classList.remove('manifesto-intro-running');
title = portal.querySelector('.main-manifesto-title');
copy = portal.querySelector('.main-manifesto-copy');
if (title) {
title.style.opacity = '1';
title.style.borderBottom = '0';
}
if (copy) copy.style.visibility = 'visible';
Array.prototype.forEach.call(portal.querySelectorAll('.manifesto-line'), function (line) {
line.style.height = '26px';
line.style.maxHeight = '26px';
line.style.opacity = '1';
line.style.overflow = 'visible';
line.style.clipPath = 'inset(0 0 0 0)';
});
Array.prototype.forEach.call(portal.querySelectorAll('.manifesto-line-inner'), function (inner) {
inner.style.opacity = '1';
inner.style.transform = 'translateY(0)';
});
}
function loadManifestoIntro(root) {
if (
window.ManifestoIntro &&
window.ManifestoIntro.version === MANIFESTO_INTRO_VERSION &&
typeof window.ManifestoIntro.init === 'function'
) {
try {
return Promise.resolve(window.ManifestoIntro.init(root || document)).catch(function () {
revealManifesto(root);
return false;
});
} catch (error) {
revealManifesto(root);
return Promise.resolve(false);
}
}
if (manifestoIntroPromise) return manifestoIntroPromise;
manifestoIntroPromise = new Promise(function (resolve) {
var script = document.createElement('script');
var settled = false;
var timeout = 0;
function settle(ok) {
if (settled) return;
settled = true;
window.clearTimeout(timeout);
if (!ok) {
revealManifesto(root);
manifestoIntroPromise = null;
}
resolve(ok);
}
script.src = '/index.php?title=' + encodeURIComponent('MediaWiki:ManifestoIntro.js') +
'&action=raw&ctype=text/javascript&v=' + encodeURIComponent(MANIFESTO_INTRO_VERSION) +
'&_manifestoFresh=' + encodeURIComponent(String(Date.now()));
script.async = false;
script.setAttribute('data-manifesto-intro-script', '1');
script.onload = function () {
if (
window.ManifestoIntro &&
window.ManifestoIntro.version === MANIFESTO_INTRO_VERSION &&
typeof window.ManifestoIntro.init === 'function'
) {
Promise.resolve(window.ManifestoIntro.init(root || document))
.then(function () { settle(true); })
.catch(function () { settle(false); });
} else {
settle(false);
}
};
script.onerror = function () { settle(false); };
timeout = window.setTimeout(function () {
if (script.parentNode) script.parentNode.removeChild(script);
settle(false);
}, MANIFESTO_LOAD_TIMEOUT_MS);
(document.head || document.documentElement).appendChild(script);
});
return manifestoIntroPromise;
}
function ensureBodyPanel(portal) {
var panel = portal.querySelector('.main-body-panel');
var well = portal.querySelector('.main-body-well');
var topMount = portal.querySelector('[data-component="category-nav"]');
var manifesto = portal.querySelector('.main-manifesto');
if (!panel) {
panel = document.createElement('div');
panel.className = 'main-body-panel';
panel.setAttribute('data-main-body-panel-generated', '1');
if (topMount && topMount.parentNode) {
if (topMount.nextSibling) topMount.parentNode.insertBefore(panel, topMount.nextSibling);
else topMount.parentNode.appendChild(panel);
} else {
portal.appendChild(panel);
}
}
if (!well) {
well = document.createElement('div');
well.className = 'main-body-well';
well.setAttribute('data-main-body-well-generated', '1');
panel.insertBefore(well, panel.firstChild);
} else if (well.parentNode !== panel) {
panel.insertBefore(well, panel.firstChild);
}
if (manifesto && manifesto.parentNode !== well) well.insertBefore(manifesto, well.firstChild);
return { panel:panel, well:well };
}
function setBodyScreenHidden(screen, hidden) {
if (!screen) return;
screen.hidden = !!hidden;
screen.setAttribute('aria-hidden', hidden ? 'true' : 'false');
screen.classList.toggle('is-main-body-screen-hidden', !!hidden);
}
function updateGuideButtonState(portal, active) {
var guideActive = active === 'guide';
var label = guideActive ? '돌아가기' : '길라잡이';
var target = guideActive ? 'manifesto' : 'guide';
var href = guideActive ? '#main-manifesto-screen' : '#main-guide-screen';
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-anchor'), function (anchor) {
anchor.setAttribute('href', href);
anchor.setAttribute('aria-label', label);
anchor.setAttribute('aria-pressed', guideActive ? 'true' : 'false');
anchor.setAttribute('data-main-body-target', target);
anchor.removeAttribute('aria-current');
});
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-label'), function (text) {
text.textContent = label;
});
Array.prototype.forEach.call(portal.querySelectorAll('.portal-guide-nav'), function (nav) {
nav.setAttribute('aria-label', label);
});
}
function ensureGuideDummyMenu(guide) {
var menu;
if (!guide) return null;
menu = guide.querySelector('[data-guide-dummy-menu="1"]');
if (menu) return menu;
menu = document.createElement('nav');
menu.className = 'main-guide-menu';
menu.setAttribute('data-guide-dummy-menu', '1');
menu.setAttribute('aria-label', '길라잡이 항목 예시');
GUIDE_DUMMY_ITEMS.forEach(function (item, index) {
var wrapper = document.createElement('div');
var button = document.createElement('button');
var number = document.createElement('span');
var label = document.createElement('span');
var arrow = document.createElement('span');
wrapper.className = 'main-guide-menu-item';
wrapper.style.setProperty('--guide-menu-order', String(index));
button.type = 'button';
button.className = 'main-guide-menu-button';
button.setAttribute('data-guide-dummy-button', item.key);
button.setAttribute('aria-label', item.label + ' (더미 버튼)');
number.className = 'main-guide-menu-number';
number.setAttribute('aria-hidden', 'true');
number.textContent = String(index + 1).padStart(2, '0');
label.className = 'main-guide-menu-label';
label.textContent = item.label;
arrow.className = 'main-guide-menu-arrow';
arrow.setAttribute('aria-hidden', 'true');
arrow.textContent = '›';
button.appendChild(number);
button.appendChild(label);
button.appendChild(arrow);
button.addEventListener('click', function (event) { event.preventDefault(); });
wrapper.appendChild(button);
menu.appendChild(wrapper);
});
guide.insertBefore(menu, guide.firstChild);
return menu;
}
function restartGuideMenuAnimation(guide) {
if (!guide || !guide.querySelector('[data-guide-dummy-menu="1"]')) return;
guide.classList.remove('is-guide-menu-entering');
void guide.offsetWidth;
guide.classList.add('is-guide-menu-entering');
}
function ensureBodyScreens(portal) {
var layout = ensureBodyPanel(portal);
var manifesto = layout.well.querySelector('.main-manifesto');
var guide = layout.well.querySelector('[data-main-body-screen="guide"]');
var panels;
var active = layout.well.getAttribute('data-active-main-body-screen') ||
portal.getAttribute('data-main-body-screen') || 'manifesto';
if (manifesto) {
manifesto.classList.add('main-body-screen');
manifesto.setAttribute('data-main-body-screen', 'manifesto');
if (!manifesto.id) manifesto.id = 'main-manifesto-screen';
}
if (!guide) {
guide = document.createElement('section');
guide.className = 'main-body-screen main-guide-screen';
guide.setAttribute('data-main-body-screen', 'guide');
guide.setAttribute('data-main-guide-screen-generated', '1');
guide.setAttribute('aria-label', '길라잡이');
guide.setAttribute('role', 'region');
guide.id = 'main-guide-screen';
layout.well.appendChild(guide);
} else {
guide.classList.add('main-body-screen', 'main-guide-screen');
if (!guide.id) guide.id = 'main-guide-screen';
}
ensureGuideDummyMenu(guide);
panels = Array.prototype.slice.call(
layout.well.querySelectorAll('.main-body-screen[data-main-body-screen]')
);
if (active !== 'guide') active = 'manifesto';
if (!panels.some(function (panel) {
return panel.getAttribute('data-main-body-screen') === active;
})) active = 'manifesto';
panels.forEach(function (panel) {
setBodyScreenHidden(panel, panel.getAttribute('data-main-body-screen') !== active);
});
layout.well.setAttribute('data-main-body-screen-controller-ready', '1');
layout.well.setAttribute('data-active-main-body-screen', active);
portal.setAttribute('data-main-body-screen', active);
portal.classList.toggle('is-guide-screen-active', active === 'guide');
updateGuideButtonState(portal, active);
if (active === 'guide' && !guide.classList.contains('is-guide-menu-entering')) {
restartGuideMenuAnimation(guide);
}
return {
well:layout.well,
panels:panels,
manifesto:manifesto,
guide:guide,
active:active
};
}
function activateBodyScreen(portal, screenName) {
var screens;
var previous;
var active;
var event;
if (!portal) return false;
screens = ensureBodyScreens(portal);
screenName = screenName === 'guide' ? 'guide' : 'manifesto';
if (!screens.panels.some(function (panel) {
return panel.getAttribute('data-main-body-screen') === screenName;
})) return false;
previous = screens.active;
active = screenName;
screens.panels.forEach(function (panel) {
setBodyScreenHidden(panel, panel.getAttribute('data-main-body-screen') !== active);
});
screens.well.setAttribute('data-active-main-body-screen', active);
portal.setAttribute('data-main-body-screen', active);
portal.classList.toggle('is-guide-screen-active', active === 'guide');
updateGuideButtonState(portal, active);
if (active === 'guide' && previous !== active) restartGuideMenuAnimation(screens.guide);
else if (active !== 'guide' && screens.guide) screens.guide.classList.remove('is-guide-menu-entering');
render(portal);
if (previous !== active) {
try {
event = new CustomEvent('mainbodyscreenchange', {
bubbles:true,
detail:{ screen:active, previous:previous }
});
portal.dispatchEvent(event);
} catch (error) {}
}
return true;
}
function toggleBodyScreen(portal) {
var screens;
if (!portal) return false;
screens = ensureBodyScreens(portal);
return activateBodyScreen(portal, screens.active === 'guide' ? 'manifesto' : 'guide');
}
function handleGuideScreenCapture(event) {
var target;
var anchor;
var portal;
if (!event || event.__mainGuideScreenHandled) return;
if (event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
target = event.target;
anchor = target && target.closest
? target.closest('.portal-guide-anchor[data-category-key="guide"]')
: null;
if (!anchor) return;
portal = anchor.closest ? anchor.closest('.main-portal') : null;
if (!portal || !toggleBodyScreen(portal)) return;
event.__mainGuideScreenHandled = true;
event.preventDefault();
event.stopPropagation();
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
}
if (window.BottomGuideNavScreenCaptureHandler) {
document.removeEventListener('click', window.BottomGuideNavScreenCaptureHandler, true);
}
window.BottomGuideNavScreenCaptureHandler = handleGuideScreenCapture;
document.addEventListener('click', handleGuideScreenCapture, true);
function ensureMount(portal) {
var layout = ensureBodyPanel(portal);
var mount = portal.querySelector('[data-component="' + COMPONENT + '"]');
ensureBodyScreens(portal);
if (!mount) {
mount = document.createElement('div');
mount.setAttribute('data-component', COMPONENT);
mount.setAttribute('data-bottom-guide-generated', '1');
}
if (mount.parentNode !== layout.panel) layout.panel.appendChild(mount);
return mount;
}
function guideButton(seams) {
var frame = api();
var d = frame.DIMENSIONS;
var step = Math.floor(d.BUTTON_EDGE_BOTTOM / frame.FRAME.SLANT_RISE);
var left = seams.settingsLeftOriginX + step;
var right = seams.corpRightOriginX - step;
return {
x:Math.round(left),
y:d.BUTTON_EDGE_TOP,
btnW:Math.max(4, Math.round(right - left)),
pixelHeight:d.BUTTON_TOTAL,
slant:'both'
};
}
function draw(canvas, button, label, hover) {
var frame = api();
var ctx = canvas.getContext('2d');
var image = frame.createImage(ctx, canvas.width, canvas.height, 'BG');
var grid;
var mask;
var center;
frame.paintStandardFrame(image, canvas.width, canvas.height);
grid = frame.buildReverseButtonPixels(
button.btnW,
frame.FRAME.BUTTON_FACE,
button.slant,
0
);
frame.paintGrid(image, canvas.width, canvas.height, grid, button.x, button.y, true);
ctx.putImageData(image, 0, 0);
mask = frame.buttonMask(
button.btnW,
frame.FRAME.BUTTON_FACE - 1 - Math.floor(frame.FRAME.BUTTON_FACE / 2),
button.slant,
0
);
center = button.x + Math.round((mask.left + mask.right) / 2);
frame.drawLabel(
ctx,
label,
center,
Math.round((frame.DIMENSIONS.BUTTON_SURFACE_TOP + frame.DIMENSIONS.BUTTON_SURFACE_BOTTOM) / 2) + 1,
hover,
canvas.width <= 820 ? 11 : 12
);
}
function render(portal) {
var frame = api();
var seams;
var mount;
var width;
var guideActive;
var label;
var targetScreen;
var href;
var button;
var nav;
var inner;
var canvas;
var hitLayer;
var anchor;
var hiddenLabel;
var redraw;
if (!frame || !frame.FRAME || typeof frame.readRenderWidth !== 'function') {
window.clearTimeout(geometryWaitTimer);
geometryWaitTimer = window.setTimeout(function () {
geometryWaitTimer = 0;
render(portal);
}, 50);
return false;
}
seams = frame.seams;
if (!seams || !seams.width) return false;
mount = ensureMount(portal);
width = seams.width || frame.readRenderWidth(mount);
if (!width) return false;
guideActive = portal.getAttribute('data-main-body-screen') === 'guide';
label = guideActive ? '돌아가기' : '길라잡이';
targetScreen = guideActive ? 'manifesto' : 'guide';
href = guideActive ? '#main-manifesto-screen' : '#main-guide-screen';
button = guideButton(seams);
if (
mount.getAttribute('data-bottom-guide-width') === String(width) &&
mount.getAttribute('data-bottom-guide-label') === label &&
mount.getAttribute('data-bottom-guide-scale') === String(frame.getShellScale()) &&
mount.querySelector('.portal-guide-canvas')
) {
return true;
}
nav = document.createElement('nav');
nav.className = 'portal-category-nav portal-guide-nav portal-frame-canvas-nav is-continuous-weld';
nav.setAttribute('aria-label', label);
nav.__frameHover = false;
inner = document.createElement('div');
inner.className = 'portal-frame-counter-inner portal-guide-counter-inner';
inner.style.width = width + 'px';
canvas = frame.createCanvas('portal-frame-canvas portal-guide-canvas', width, frame.DIMENSIONS.ROW_HEIGHT);
canvas.setAttribute('data-frame-width', String(width));
canvas.setAttribute('data-frame-height', String(frame.DIMENSIONS.ROW_HEIGHT));
hitLayer = document.createElement('div');
hitLayer.className = 'portal-frame-hit-layer';
hitLayer.style.width = width + 'px';
hitLayer.style.height = frame.DIMENSIONS.ROW_HEIGHT + 'px';
anchor = document.createElement('a');
anchor.className = 'portal-frame-hit portal-guide-anchor';
anchor.href = href;
anchor.setAttribute('aria-label', label);
anchor.setAttribute('role', 'button');
anchor.setAttribute('aria-pressed', guideActive ? 'true' : 'false');
anchor.setAttribute('data-category-key', 'guide');
anchor.setAttribute('data-main-body-target', targetScreen);
anchor.setAttribute('aria-controls', 'main-manifesto-screen main-guide-screen');
anchor.style.left = button.x + 'px';
anchor.style.top = button.y + 'px';
anchor.style.width = (button.btnW + 2) + 'px';
anchor.style.height = button.pixelHeight + 'px';
hiddenLabel = document.createElement('span');
hiddenLabel.className = 'portal-frame-sr-label portal-guide-label';
hiddenLabel.textContent = label;
anchor.appendChild(hiddenLabel);
redraw = function () { draw(canvas, button, label, nav.__frameHover); };
anchor.addEventListener('mouseenter', function () {
nav.__frameHover = true;
redraw();
});
anchor.addEventListener('mouseleave', function () {
nav.__frameHover = false;
redraw();
});
anchor.addEventListener('focus', function () {
nav.__frameHover = true;
redraw();
});
anchor.addEventListener('blur', function () {
nav.__frameHover = false;
redraw();
});
anchor.addEventListener('click', function (event) {
if (event && (event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) return;
if (toggleBodyScreen(portal) && event) event.preventDefault();
});
hitLayer.appendChild(anchor);
inner.appendChild(canvas);
inner.appendChild(hitLayer);
nav.appendChild(inner);
mount.replaceChildren(nav);
frame.applyCounterScale(
[mount, nav],
inner,
frame.DIMENSIONS.ROW_HEIGHT
);
redraw();
mount.setAttribute('data-bottom-guide-rendered', 'canvas');
mount.setAttribute('data-bottom-guide-width', String(width));
mount.setAttribute('data-bottom-guide-label', label);
mount.setAttribute('data-bottom-guide-scale', String(frame.getShellScale()));
return true;
}
function findPortal(root) {
var scope = root && root.querySelector ? root : document;
return scope.matches && scope.matches('.main-portal')
? scope
: scope.querySelector('.main-portal');
}
function schedule(root, attempt) {
window.clearTimeout(retryTimer);
retryTimer = window.setTimeout(function () {
var portal = findPortal(root);
if (!portal) return;
if (!render(portal) && attempt < 20) {
schedule(root, attempt + 1);
return;
}
if (observer) observer.disconnect();
if (window.ResizeObserver) {
observer = new ResizeObserver(function () {
window.clearTimeout(resizeTimer);
resizeTimer = window.setTimeout(function () { render(portal); }, 60);
});
observer.observe(portal);
var topMount = portal.querySelector('[data-component="category-nav"]');
var guideMount = portal.querySelector('[data-component="' + COMPONENT + '"]');
if (topMount) observer.observe(topMount);
if (guideMount) observer.observe(guideMount);
}
}, attempt ? 60 : 0);
}
function boot(root) {
var scope = root || document;
schedule(scope, 0);
loadManifestoIntro(scope);
}
revealManifesto(document);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { boot(document); }, { once:true });
} else {
boot(document);
}
window.addEventListener('load', function () { boot(document); }, { once:true });
window.addEventListener('pageshow', function () { boot(document); });
if (api()) {
api().onSeams(function () { schedule(document, 0); });
api().onScale(function () { schedule(document, 0); });
}
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
boot($content && $content[0] ? $content[0] : document);
});
}
window.BottomGuideNav = {
version:'20260717-canvas-pixel-001',
render:function () {
var portal = document.querySelector('.main-portal');
return portal ? render(portal) : false;
},
activate:function (screenName) {
var portal = document.querySelector('.main-portal');
return portal ? activateBodyScreen(portal, screenName) : false;
},
toggle:function () {
var portal = document.querySelector('.main-portal');
return portal ? toggleBodyScreen(portal) : false;
},
status:function () {
var portal = document.querySelector('.main-portal');
var mount = portal && portal.querySelector('[data-component="' + COMPONENT + '"]');
return {
loaded:true,
portal:!!portal,
mount:!!mount,
rendered:!!(mount && mount.querySelector('.portal-guide-canvas')),
generated:!!(mount && mount.getAttribute('data-bottom-guide-generated')),
panel:!!(portal && portal.querySelector('.main-body-panel')),
well:!!(portal && portal.querySelector('.main-body-well')),
insidePanel:!!(mount && mount.parentNode && mount.parentNode.classList.contains('main-body-panel')),
welded:!!(mount && mount.querySelector('.portal-guide-nav.is-continuous-weld')),
activeScreen:portal ? portal.getAttribute('data-main-body-screen') || 'manifesto' : '',
guideScreen:!!(portal && portal.querySelector('[data-main-body-screen="guide"]')),
width:mount ? mount.getAttribute('data-bottom-guide-width') || '' : ''
};
}
};
}(window, document, window.mw));