편집 요약 없음 |
편집 요약 없음 |
||
| 48번째 줄: | 48번째 줄: | ||
} | } | ||
var retryTimer = 0; | var retryTimer = 0; | ||
var geometryWaitTimer = 0; | |||
var resizeTimer = 0; | var resizeTimer = 0; | ||
var guideLayoutAnimationFrame = 0; | var guideLayoutAnimationFrame = 0; | ||
| 738번째 줄: | 739번째 줄: | ||
function render(portal, suppliedGeometry) { | function render(portal, suppliedGeometry) { | ||
/* | |||
* FrameGeometry(window.CLBI.frame)가 아직 로드되지 않았으면 렌더를 미룬다. | |||
* 이 파일이 FrameGeometry.js보다 먼저 실행될 수 있으므로, 준비되지 않은 | |||
* 상태에서 geometryApi().readWidth(...) 등을 호출하면 undefined 참조로 죽는다. | |||
* 준비될 때까지 짧게 재시도한다. | |||
*/ | |||
var api = geometryApi(); | |||
if (!api || !api.FRAME || typeof api.readWidth !== 'function') { | |||
window.clearTimeout(geometryWaitTimer); | |||
geometryWaitTimer = window.setTimeout(function () { | |||
geometryWaitTimer = 0; | |||
render(portal, suppliedGeometry); | |||
}, 50); | |||
return false; | |||
} | |||
var mount = ensureMount(portal); | var mount = ensureMount(portal); | ||
var width = | var width = api.readWidth(mount); | ||
var guideActive = portal.getAttribute('data-main-body-screen') === 'guide'; | var guideActive = portal.getAttribute('data-main-body-screen') === 'guide'; | ||
var geometry = suppliedGeometry || portal.GuideNavGeometry || getGuideGeometry(guideActive); | var geometry = suppliedGeometry || portal.GuideNavGeometry || getGuideGeometry(guideActive); | ||
| 755번째 줄: | 772번째 줄: | ||
if (!button) return false; | if (!button) return false; | ||
points = button.points; | points = button.points; | ||
surfacePoints = guideLayer(button, geometry, | surfacePoints = guideLayer(button, geometry, api.FRAME.BORDER); | ||
insetPoints = guideLayer( | insetPoints = guideLayer( | ||
button, | button, | ||
geometry, | geometry, | ||
api.FRAME.BORDER + api.FRAME.BEVEL | |||
); | ); | ||
center = centerOfPoints(surfacePoints); | center = centerOfPoints(surfacePoints); | ||
2026년 7월 17일 (금) 10:58 판
/* =========================================
BottomGuideNav
대문 하단 「길라잡이」 SVG 버튼
========================================= */
/*
상단 CategoryNav와 분리해 로드한다.
FrameGeometry.js가 공유한 설정–프로젝트–기업 및 공동체 seam을 사용하며,
상단 SVG나 화면 rect를 다시 읽지 않는다.
*/
(function (window, document, mw) {
'use strict';
var SVG_NS = 'http://www.w3.org/2000/svg';
var XLINK_NS = 'http://www.w3.org/1999/xlink';
var COMPONENT = 'category-guide-nav';
/*
* FrameGeometry(window.CLBI.frame) 의존은 반드시 렌더 시점에 지연 접근한다.
* 모듈 로드 순서상 이 파일이 FrameGeometry.js보다 먼저 실행될 수 있으므로,
* 최상단에서 FRAME_API.FRAME 을 즉시 읽으면 undefined 참조로 파일 전체가 죽는다.
* (다른 프레임 모듈들과 동일하게 geometryApi() 함수를 통해 접근한다.)
*/
function geometryApi() {
return window.CLBI && window.CLBI.frame;
}
/* 세로 규격은 매 렌더 시 FrameGeometry 상수에서 새로 파생한다. */
function guideDims() {
var f = geometryApi().FRAME;
var top = f.FRAME_FACE;
var bottom = f.FRAME_FACE;
var height = f.BUTTON_FACE;
var gap = f.GAP;
return {
HEIGHT: height,
TOP_FRAME: top,
BOTTOM_FRAME: bottom,
GAP: gap,
ROW_HEIGHT: top + gap + height + gap + bottom,
BUTTON_TOP: top + gap,
BUTTON_BOTTOM: top + gap + height,
COMPACT_HEIGHT: height,
COMPACT_TOP_FRAME: top,
COMPACT_BOTTOM_FRAME: bottom
};
}
var retryTimer = 0;
var geometryWaitTimer = 0;
var resizeTimer = 0;
var guideLayoutAnimationFrame = 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 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;
}
revealManifesto(document);
function createSvgElement(name) {
return geometryApi().create(name);
}
function buildWikiPath(title) {
if (typeof window.buildWikiPath === 'function') {
return window.buildWikiPath(title);
}
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}
function createPolygon(className, points) {
return geometryApi().polygon(className, points);
}
function createLine(className, start, end) {
return geometryApi().line(className, start, end);
}
function createRect(className, x, y, width, height) {
return geometryApi().rect(className, x, y, width, height);
}
function centerOfPoints(points) {
return [geometryApi().centerX(points), Math.round((points[0][1] + points[2][1]) / 2)];
}
function shiftY(points, amount) {
return points.map(function (point) {
return [Math.round(point[0]), Math.round(point[1] + amount)];
});
}
function makeGuideGeometry(height, topFrame, bottomFrame, sideFrameProgress) {
var gap = geometryApi().FRAME.GAP;
height = Math.round(height);
topFrame = Math.round(topFrame);
bottomFrame = Math.round(bottomFrame);
return {
height: height,
topFrame: topFrame,
bottomFrame: bottomFrame,
buttonTop: topFrame + gap,
buttonBottom: topFrame + gap + height,
rowHeight: topFrame + gap + height + gap + bottomFrame,
sideFrameProgress: Math.max(0, Math.min(1, Number(sideFrameProgress || 0)))
};
}
function getGuideGeometry(compact) {
var d = guideDims();
return compact
? makeGuideGeometry(d.COMPACT_HEIGHT, d.COMPACT_TOP_FRAME, d.COMPACT_BOTTOM_FRAME, 1)
: makeGuideGeometry(d.HEIGHT, d.TOP_FRAME, d.BOTTOM_FRAME, 0);
}
function interpolateGuideGeometry(from, to, progress) {
function between(start, end) {
return start + (end - start) * progress;
}
return makeGuideGeometry(
between(from.height, to.height),
between(from.topFrame, to.topFrame),
between(from.bottomFrame, to.bottomFrame),
between(from.sideFrameProgress, to.sideFrameProgress)
);
}
function applyGuideRowHeight(mount, nav, svg, geometry) {
var height = geometry.rowHeight + 'px';
[mount, nav, svg].forEach(function (node) {
if (!node || !node.style) return;
node.style.setProperty('height', height, 'important');
node.style.setProperty('min-height', height, 'important');
node.style.setProperty('max-height', height, 'important');
});
}
function requestGuideFrame(callback) {
if (typeof window.requestAnimationFrame === 'function') {
return window.requestAnimationFrame(callback);
}
return window.setTimeout(function () { callback(Date.now()); }, 16);
}
function cancelGuideFrame(handle) {
if (!handle) return;
if (typeof window.cancelAnimationFrame === 'function') {
window.cancelAnimationFrame(handle);
} else {
window.clearTimeout(handle);
}
}
function animateGuideLayout(portal, compact) {
var from = portal && portal.GuideNavGeometry
? portal.GuideNavGeometry
: getGuideGeometry(!compact);
var to = getGuideGeometry(compact);
var startedAt = null;
var duration = 340;
if (!portal) return;
if (guideLayoutAnimationFrame) {
cancelGuideFrame(guideLayoutAnimationFrame);
guideLayoutAnimationFrame = 0;
}
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
portal.GuideNavGeometry = to;
render(portal, to);
return;
}
function step(now) {
var elapsed;
var progress;
var eased;
if (startedAt === null) startedAt = now;
elapsed = Math.min(1, (now - startedAt) / duration);
eased = 1 - Math.pow(1 - elapsed, 3);
progress = interpolateGuideGeometry(from, to, eased);
portal.GuideNavGeometry = progress;
render(portal, progress);
if (elapsed < 1) {
guideLayoutAnimationFrame = requestGuideFrame(step);
} else {
portal.GuideNavGeometry = to;
guideLayoutAnimationFrame = 0;
}
}
guideLayoutAnimationFrame = requestGuideFrame(step);
}
function buildGuideButton(geometry) {
var seams = geometryApi().seams;
var leftTop;
var rightTop;
var leftDir;
var rightDir;
if (!seams || !seams.width) return null;
leftTop = geometryApi().slantX(
seams.settingsLeftTopX,
seams.buttonBottom,
seams.settingsLeftDir,
0
);
rightTop = geometryApi().slantX(
seams.corpRightTopX,
seams.buttonBottom,
seams.corpRightDir,
0
);
leftDir = -seams.settingsLeftDir;
rightDir = -seams.corpRightDir;
return {
leftTopX:leftTop,
rightTopX:rightTop,
leftDir:leftDir,
rightDir:rightDir,
points:shiftY(
geometryApi().segment(leftTop, rightTop, geometry.height, leftDir, rightDir, 0),
geometry.buttonTop
)
};
}
function guideLayer(button, geometry, amount) {
return shiftY(
geometryApi().segment(
button.leftTopX + amount,
button.rightTopX - amount,
geometry.height - amount * 2,
button.leftDir,
button.rightDir,
amount % geometryApi().FRAME.SLANT_RISE
),
geometry.buttonTop + amount
);
}
function appendContinuousBottomFrame(svg, button, width, geometry) {
var frame = geometryApi().FRAME;
var group = createSvgElement('g');
var leftTarget = button.points[0][0] - frame.FRAME_FACE - frame.GAP;
var rightTarget = button.points[1][0] + frame.FRAME_FACE + frame.GAP;
var leftBase = leftTarget - geometryApi().slantX(0, geometry.buttonTop, button.leftDir, 0);
var rightBase = rightTarget - geometryApi().slantX(0, geometry.buttonTop, button.rightDir, 0);
var outer = geometryApi().segment(
leftBase,
rightBase,
geometry.rowHeight,
button.leftDir,
button.rightDir,
0
);
var well = geometryApi().segmentAt(
leftBase,
rightBase,
geometry.topFrame,
geometry.rowHeight - geometry.bottomFrame,
button.leftDir,
button.rightDir,
0,
frame.FRAME_FACE,
frame.FRAME_FACE
);
var bevel = geometryApi().segmentAt(
leftBase,
rightBase,
frame.BORDER + frame.BEVEL,
geometry.rowHeight - frame.BORDER - frame.BEVEL,
button.leftDir,
button.rightDir,
0,
frame.BORDER + frame.BEVEL,
frame.BORDER + frame.BEVEL
);
group.setAttribute('class', 'portal-guide-arch-frame');
group.setAttribute('pointer-events', 'none');
group.appendChild(createPolygon('portal-guide-arch-outer', outer));
group.appendChild(createPolygon('portal-guide-arch-well', well));
group.appendChild(createLine('portal-guide-arch-edge', outer[3], outer[0]));
group.appendChild(createLine('portal-guide-arch-edge', outer[0], outer[1]));
group.appendChild(createLine('portal-guide-arch-edge', outer[1], outer[2]));
group.appendChild(createLine('portal-guide-arch-highlight', bevel[0], bevel[1]));
group.appendChild(createLine('portal-guide-arch-highlight', bevel[1], bevel[2]));
group.appendChild(createLine('portal-guide-arch-shadow', bevel[3], bevel[0]));
group.appendChild(createLine('portal-guide-arch-inner-highlight', well[0], well[1]));
group.appendChild(createLine('portal-guide-arch-inner-highlight', well[1], well[2]));
group.appendChild(createLine('portal-guide-arch-inner-shadow', well[3], well[0]));
svg.appendChild(group);
}
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.classList.toggle('is-active', !hidden);
screen.hidden = hidden;
screen.setAttribute('aria-hidden', hidden ? 'true' : 'false');
}
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.setAttributeNS(XLINK_NS, 'xlink: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');
guide.classList.add('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');
}
if (previous !== active) {
animateGuideLayout(portal, active === 'guide');
}
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;
var target;
if (!portal) return false;
screens = ensureBodyScreens(portal);
target = screens.active === 'guide' ? 'manifesto' : 'guide';
return activateBodyScreen(portal, target);
}
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();
}
}
/*
이전 실행본의 ResizeObserver가 오래된 문서 링크를 다시 그려도
전역 SPA 링크 처리보다 먼저 화면 전환으로 고정한다.
*/
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');
}
/*
길라잡이는 우물 안의 독립 버튼이 아니라
main-body-panel 하단 프레임에 융합된 돌출부다.
*/
if (mount.parentNode !== layout.panel) {
layout.panel.appendChild(mount);
}
return mount;
}
function render(portal, suppliedGeometry) {
/*
* FrameGeometry(window.CLBI.frame)가 아직 로드되지 않았으면 렌더를 미룬다.
* 이 파일이 FrameGeometry.js보다 먼저 실행될 수 있으므로, 준비되지 않은
* 상태에서 geometryApi().readWidth(...) 등을 호출하면 undefined 참조로 죽는다.
* 준비될 때까지 짧게 재시도한다.
*/
var api = geometryApi();
if (!api || !api.FRAME || typeof api.readWidth !== 'function') {
window.clearTimeout(geometryWaitTimer);
geometryWaitTimer = window.setTimeout(function () {
geometryWaitTimer = 0;
render(portal, suppliedGeometry);
}, 50);
return false;
}
var mount = ensureMount(portal);
var width = api.readWidth(mount);
var guideActive = portal.getAttribute('data-main-body-screen') === 'guide';
var geometry = suppliedGeometry || portal.GuideNavGeometry || getGuideGeometry(guideActive);
if (!width) return false;
portal.GuideNavGeometry = geometry;
var button = buildGuideButton(geometry);
var points;
var surfacePoints;
var insetPoints;
var center;
if (!button) return false;
points = button.points;
surfacePoints = guideLayer(button, geometry, api.FRAME.BORDER);
insetPoints = guideLayer(
button,
geometry,
api.FRAME.BORDER + api.FRAME.BEVEL
);
center = centerOfPoints(surfacePoints);
var nav = document.createElement('nav');
var svg = createSvgElement('svg');
var anchor = createSvgElement('a');
var buttonGeometry = createSvgElement('g');
var label = guideActive ? '돌아가기' : '길라잡이';
var targetScreen = guideActive ? 'manifesto' : 'guide';
var href = guideActive ? '#main-manifesto-screen' : '#main-guide-screen';
var text = createSvgElement('text');
var outer = createPolygon('portal-cat-edge', points);
var surface = createPolygon('portal-cat-surface', surfacePoints);
var hit = createPolygon('portal-cat-hit', points);
nav.className = 'portal-category-nav portal-guide-nav is-continuous-weld';
nav.setAttribute('aria-label', label);
svg.setAttribute('class', 'portal-category-svg portal-guide-svg');
svg.setAttribute('viewBox', '0 0 ' + width + ' ' + geometry.rowHeight);
svg.setAttribute('width', '100%');
svg.setAttribute('height', geometry.rowHeight);
svg.setAttribute('preserveAspectRatio', 'none');
appendContinuousBottomFrame(svg, button, width, geometry);
anchor.setAttribute('class', 'portal-cat-anchor portal-guide-anchor');
anchor.setAttribute('href', href);
anchor.setAttributeNS(XLINK_NS, 'xlink: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');
buttonGeometry.setAttribute('class', 'portal-guide-button-geometry');
anchor.addEventListener('click', function (event) {
if (
event &&
(event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
) {
return;
}
if (toggleBodyScreen(portal)) {
if (event) event.preventDefault();
}
});
outer.setAttribute('pointer-events', 'none');
surface.setAttribute('pointer-events', 'none');
hit.setAttribute('pointer-events', 'all');
text.setAttribute('class', 'portal-cat-label portal-guide-label');
text.setAttribute('x', Math.round(center[0]));
text.setAttribute('y', Math.round((geometry.buttonTop + geometry.buttonBottom) / 2) + 1);
text.setAttribute('font-size', '12');
text.setAttribute('font-weight', '700');
text.setAttribute('text-anchor', 'middle');
text.setAttribute('dominant-baseline', 'middle');
text.setAttribute('pointer-events', 'none');
text.textContent = label;
buttonGeometry.appendChild(outer);
buttonGeometry.appendChild(surface);
buttonGeometry.appendChild(createLine(
'portal-cat-inset portal-cat-inset-top',
insetPoints[0],
insetPoints[1]
));
buttonGeometry.appendChild(createLine(
'portal-cat-inset portal-cat-inset-right',
insetPoints[1],
insetPoints[2]
));
buttonGeometry.appendChild(createLine(
'portal-cat-inset portal-cat-inset-bottom',
insetPoints[2],
insetPoints[3]
));
buttonGeometry.appendChild(createLine(
'portal-cat-inset portal-cat-inset-left',
insetPoints[3],
insetPoints[0]
));
buttonGeometry.appendChild(hit);
anchor.appendChild(buttonGeometry);
anchor.appendChild(text);
svg.appendChild(anchor);
nav.appendChild(svg);
mount.replaceChildren(nav);
applyGuideRowHeight(mount, nav, svg, geometry);
mount.setAttribute('data-bottom-guide-rendered', '1');
mount.setAttribute('data-bottom-guide-width', String(width));
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);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
boot(document);
}, { once: true });
} else {
boot(document);
}
/* 일반 링크 진입과 새로고침의 최종 로드 시점도 명시적으로 보장한다. */
window.addEventListener('load', function () {
boot(document);
}, { once: true });
/* bfcache 복원과 브라우저 뒤로/앞으로 이동에서도 새 DOM만 초기화한다. */
window.addEventListener('pageshow', function () {
boot(document);
});
if (mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
boot($content && $content[0] ? $content[0] : document);
});
}
window.BottomGuideNav = {
version: '20260717-shared-frame-reverse-slant-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-svg')),
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));