편집 요약 없음 |
(Install package: clbiwiki-mainpage-trim-v3-renewal-20260711 (1) / js/CategoryNav.js) |
||
| 1번째 줄: | 1번째 줄: | ||
/* ========================================= | /* ========================================= | ||
COASTLINE: BLACK ICE - CategoryNav | COASTLINE: BLACK ICE - CategoryNav | ||
대문 카테고리 네비 | 대문 카테고리 SVG 네비 생성기 | ||
========================================= */ | ========================================= */ | ||
/* | /* | ||
새 공통 기능에는 프로젝트 접두사를 사용하지 않는다. | |||
window.CLBI.categoryNav는 기존 Common.js 호출과의 호환을 위해서만 유지한다. | |||
기존 | |||
*/ | */ | ||
| 67번째 줄: | 15번째 줄: | ||
var XLINK_NS = 'http://www.w3.org/1999/xlink'; | var XLINK_NS = 'http://www.w3.org/1999/xlink'; | ||
var | var ITEMS = [ | ||
{ | { key: 'era', label: '시대', title: '시대' }, | ||
{ key: 'settings', label: '설정', title: '설정' }, | |||
{ key: 'project', label: '프로젝트', title: '프로젝트:소개' }, | |||
{ key: 'corporations', label: '기업 및 공동체', title: '기업_및_공동체' }, | |||
{ key: 'military', label: '군, 정치집단', title: '군_정치집단' }, | |||
{ | { key: 'personnel', label: '인물', title: '인물' } | ||
{ | |||
{ | |||
{ | |||
{ | |||
]; | ]; | ||
function buildPath(title) { | function buildPath(title) { | ||
| 127번째 줄: | 32번째 줄: | ||
} | } | ||
function | function readWidth(element) { | ||
if (!element) return 0; | |||
var rect = element.getBoundingClientRect ? element.getBoundingClientRect() : null; | |||
var rect = | |||
var width = rect ? Math.floor(rect.width || 0) : 0; | var width = rect ? Math.floor(rect.width || 0) : 0; | ||
if (!width && | if (!width && element.clientWidth) { | ||
width = Math.floor( | width = Math.floor(element.clientWidth); | ||
} | } | ||
return width || 0; | return width || 0; | ||
} | } | ||
function getRenderableWidth(nav, mount) { | function getRenderableWidth(nav, mount) { | ||
| 166번째 줄: | 51번째 줄: | ||
mount ? mount.parentElement : null, | mount ? mount.parentElement : null, | ||
mount ? mount.closest('.main-portal') : null, | mount ? mount.closest('.main-portal') : null, | ||
document.querySelector('.liberty-content-main .mw-parser-output'), | document.querySelector('.liberty-content-main .mw-parser-output'), | ||
document.querySelector('.liberty-content-main'), | document.querySelector('.liberty-content-main'), | ||
| 175번째 줄: | 59번째 줄: | ||
var width = readWidth(candidates[i]); | var width = readWidth(candidates[i]); | ||
if (width > 0) | if (width > 0) return width; | ||
} | } | ||
| 186번째 줄: | 68번째 줄: | ||
return 1200; | return 1200; | ||
} | } | ||
function calculateGeometry(width) { | function calculateGeometry(width) { | ||
var | var usableWidth = Math.max(320, Math.floor(width || 0)); | ||
var | var count = ITEMS.length; | ||
var | var height = 24; | ||
var | var cut = Math.round((usableWidth / count) * 0.045); | ||
var | var top = []; | ||
var bottom = []; | |||
var half = count / 2; | |||
var i; | |||
cut = Math.max(6, Math.min(10, cut)); | |||
for (i = 0; i <= count; i++) { | |||
var x = Math.round((usableWidth * i) / count); | |||
if (i === 0 || i === count || i === half) { | |||
top.push(x); | |||
bottom.push(x); | |||
} else if (i < half) { | |||
top.push(x - cut); | |||
bottom.push(x); | |||
} else { | |||
top.push(x); | |||
bottom.push(x - cut); | |||
} | |||
} | } | ||
return { | return { | ||
width: | width: usableWidth, | ||
height: | height: height, | ||
count: count, | |||
cut: cut, | cut: cut, | ||
top: top, | |||
bottom: bottom | |||
}; | }; | ||
} | } | ||
function createSvgElement(name) { | |||
return document.createElementNS(SVG_NS, name); | |||
function | |||
} | } | ||
| 330번째 줄: | 116번째 줄: | ||
} | } | ||
function | function getSegmentPoints(index, geometry) { | ||
return [ | |||
[geometry.top[index], 0], | |||
[geometry.top[index + 1], 0], | |||
[geometry.bottom[index + 1], geometry.height], | |||
[geometry.bottom[index], geometry.height] | |||
]; | |||
} | } | ||
function | function getTextX(index, geometry) { | ||
return | return Math.round(( | ||
geometry.top[index] + | |||
geometry.top[index + 1] + | |||
geometry.bottom[index + 1] + | |||
geometry.bottom[index] | |||
) / 4); | |||
} | } | ||
function createSegment(item, index, | function createSegment(item, index, geometry) { | ||
var anchor = createSvgElement('a'); | var anchor = createSvgElement('a'); | ||
var polygon = createSvgElement('polygon'); | var polygon = createSvgElement('polygon'); | ||
var text = createSvgElement('text'); | var text = createSvgElement('text'); | ||
var href = buildPath(item.title); | var href = buildPath(item.title); | ||
anchor.setAttribute('class', 'portal-cat-anchor'); | anchor.setAttribute('class', 'portal-cat-anchor'); | ||
anchor.setAttribute('href', href); | anchor.setAttribute('href', href); | ||
anchor.setAttributeNS(XLINK_NS, 'xlink:href', href); | anchor.setAttributeNS(XLINK_NS, 'xlink:href', href); | ||
anchor.setAttribute('aria-label', label); | anchor.setAttribute('aria-label', item.label); | ||
anchor.setAttribute('data-category-key', item.key); | anchor.setAttribute('data-category-key', item.key); | ||
polygon.setAttribute('class', 'portal-cat-shape'); | polygon.setAttribute('class', 'portal-cat-shape'); | ||
polygon.setAttribute('points', pointsToString(getSegmentPoints(index, | polygon.setAttribute('points', pointsToString(getSegmentPoints(index, geometry))); | ||
polygon.setAttribute('fill', 'transparent'); | polygon.setAttribute('fill', 'transparent'); | ||
polygon.setAttribute('pointer-events', 'all'); | polygon.setAttribute('pointer-events', 'all'); | ||
text.setAttribute( | text.setAttribute('class', 'portal-cat-label'); | ||
text.setAttribute('x', getTextX(index, geometry)); | |||
text.setAttribute('y', Math.round(geometry.height / 2) + 1); | |||
text.setAttribute('x', getTextX(index, | |||
text.setAttribute('y', | |||
text.setAttribute('fill', '#e2e2e2'); | text.setAttribute('fill', '#e2e2e2'); | ||
text.setAttribute('font-size', | text.setAttribute('font-size', '12'); | ||
text.setAttribute('font-weight', '700'); | text.setAttribute('font-weight', '700'); | ||
text.setAttribute('text-anchor', 'middle'); | text.setAttribute('text-anchor', 'middle'); | ||
| 380번째 줄: | 161번째 줄: | ||
text.setAttribute('pointer-events', 'none'); | text.setAttribute('pointer-events', 'none'); | ||
text.setAttribute('paint-order', 'stroke'); | text.setAttribute('paint-order', 'stroke'); | ||
text.setAttribute('stroke', '# | text.setAttribute('stroke', '#050505'); | ||
text.setAttribute('stroke-width', '2'); | text.setAttribute('stroke-width', '2'); | ||
text.setAttribute('stroke-linejoin', 'round'); | text.setAttribute('stroke-linejoin', 'round'); | ||
text.textContent = label; | text.textContent = item.label; | ||
anchor.appendChild(polygon); | anchor.appendChild(polygon); | ||
| 391번째 줄: | 172번째 줄: | ||
} | } | ||
function createDivider( | function createDivider(index, geometry) { | ||
var line = createSvgElement('line'); | var line = createSvgElement('line'); | ||
line.setAttribute('class', 'portal-cat-divider | line.setAttribute('class', 'portal-cat-divider'); | ||
line.setAttribute('x1', | line.setAttribute('x1', geometry.top[index]); | ||
line.setAttribute('y1', | line.setAttribute('y1', 0); | ||
line.setAttribute('x2', | line.setAttribute('x2', geometry.bottom[index]); | ||
line.setAttribute('y2', | line.setAttribute('y2', geometry.height); | ||
line.setAttribute('stroke', '#050505'); | line.setAttribute('stroke', '#050505'); | ||
line.setAttribute('stroke-width', '1'); | line.setAttribute('stroke-width', '1'); | ||
| 405번째 줄: | 186번째 줄: | ||
return line; | return line; | ||
} | } | ||
function clearNode(node) { | function clearNode(node) { | ||
while (node.firstChild) | while (node.firstChild) node.removeChild(node.firstChild); | ||
} | } | ||
function buildSvg(width) { | function buildSvg(width) { | ||
var | var geometry = calculateGeometry(width); | ||
var svg = createSvgElement('svg'); | var svg = createSvgElement('svg'); | ||
var i; | |||
svg.setAttribute('class', 'portal-category-svg'); | svg.setAttribute('class', 'portal-category-svg'); | ||
svg.setAttribute('viewBox', '0 0 ' + | svg.setAttribute('viewBox', '0 0 ' + geometry.width + ' ' + geometry.height); | ||
svg.setAttribute('width', '100%'); | svg.setAttribute('width', '100%'); | ||
svg.setAttribute('height', '100%'); | svg.setAttribute('height', '100%'); | ||
| 445번째 줄: | 204번째 줄: | ||
svg.setAttribute('aria-hidden', 'false'); | svg.setAttribute('aria-hidden', 'false'); | ||
ITEMS.forEach(function (item, index) { | |||
svg.appendChild(createSegment(item, index, | svg.appendChild(createSegment(item, index, geometry)); | ||
}); | }); | ||
for (i = 1; i < geometry.count; i++) { | |||
svg.appendChild(createDivider(i, geometry)); | |||
} | |||
return { | return { | ||
svg: svg, | svg: svg, | ||
key: | key: geometry.width + '|' + geometry.height + '|' + geometry.cut + '|' + ITEMS.length | ||
}; | }; | ||
} | } | ||
function renderSvgIntoNav(nav, mount) { | function renderSvgIntoNav(nav, mount) { | ||
var | var result = buildSvg(getRenderableWidth(nav, mount)); | ||
if (nav.__categoryNavRenderKey === result.key && nav.querySelector('svg')) | if (nav.__categoryNavRenderKey === result.key && nav.querySelector('svg')) return; | ||
nav.__categoryNavRenderKey = result.key; | nav.__categoryNavRenderKey = result.key; | ||
| 471번째 줄: | 229번째 줄: | ||
function scheduleRender(nav, mount) { | function scheduleRender(nav, mount) { | ||
if (!nav || nav.__categoryNavFrame) | if (!nav || nav.__categoryNavFrame) return; | ||
nav.__categoryNavFrame = window.requestAnimationFrame(function () { | nav.__categoryNavFrame = window.requestAnimationFrame(function () { | ||
| 499번째 줄: | 255번째 줄: | ||
scheduleRender(nav, mount); | scheduleRender(nav, mount); | ||
window.setTimeout(function () { scheduleRender(nav, mount); }, 80); | |||
window.setTimeout(function () { scheduleRender(nav, mount); }, 250); | |||
window.setTimeout(function () { | |||
window.setTimeout(function () { | |||
if (window.ResizeObserver) { | if (window.ResizeObserver) { | ||
2026년 7월 11일 (토) 03:55 판
/* =========================================
COASTLINE: BLACK ICE - CategoryNav
대문 카테고리 SVG 네비 생성기
========================================= */
/*
새 공통 기능에는 프로젝트 접두사를 사용하지 않는다.
window.CLBI.categoryNav는 기존 Common.js 호출과의 호환을 위해서만 유지한다.
*/
(function () {
'use strict';
var SVG_NS = 'http://www.w3.org/2000/svg';
var XLINK_NS = 'http://www.w3.org/1999/xlink';
var ITEMS = [
{ key: 'era', label: '시대', title: '시대' },
{ key: 'settings', label: '설정', title: '설정' },
{ key: 'project', label: '프로젝트', title: '프로젝트:소개' },
{ key: 'corporations', label: '기업 및 공동체', title: '기업_및_공동체' },
{ key: 'military', label: '군, 정치집단', title: '군_정치집단' },
{ key: 'personnel', label: '인물', title: '인물' }
];
function buildPath(title) {
if (typeof window.buildWikiPath === 'function') {
return window.buildWikiPath(title);
}
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}
function readWidth(element) {
if (!element) return 0;
var rect = element.getBoundingClientRect ? element.getBoundingClientRect() : null;
var width = rect ? Math.floor(rect.width || 0) : 0;
if (!width && element.clientWidth) {
width = Math.floor(element.clientWidth);
}
return width || 0;
}
function getRenderableWidth(nav, mount) {
var candidates = [
nav,
mount,
mount ? mount.parentElement : null,
mount ? mount.closest('.main-portal') : null,
document.querySelector('.liberty-content-main .mw-parser-output'),
document.querySelector('.liberty-content-main'),
document.querySelector('.liberty-content')
];
for (var i = 0; i < candidates.length; i++) {
var width = readWidth(candidates[i]);
if (width > 0) return width;
}
if (document.documentElement && document.documentElement.clientWidth) {
return Math.max(320, Math.floor(document.documentElement.clientWidth - 48));
}
return 1200;
}
function calculateGeometry(width) {
var usableWidth = Math.max(320, Math.floor(width || 0));
var count = ITEMS.length;
var height = 24;
var cut = Math.round((usableWidth / count) * 0.045);
var top = [];
var bottom = [];
var half = count / 2;
var i;
cut = Math.max(6, Math.min(10, cut));
for (i = 0; i <= count; i++) {
var x = Math.round((usableWidth * i) / count);
if (i === 0 || i === count || i === half) {
top.push(x);
bottom.push(x);
} else if (i < half) {
top.push(x - cut);
bottom.push(x);
} else {
top.push(x);
bottom.push(x - cut);
}
}
return {
width: usableWidth,
height: height,
count: count,
cut: cut,
top: top,
bottom: bottom
};
}
function createSvgElement(name) {
return document.createElementNS(SVG_NS, name);
}
function pointsToString(points) {
return points.map(function (point) {
return point[0] + ',' + point[1];
}).join(' ');
}
function getSegmentPoints(index, geometry) {
return [
[geometry.top[index], 0],
[geometry.top[index + 1], 0],
[geometry.bottom[index + 1], geometry.height],
[geometry.bottom[index], geometry.height]
];
}
function getTextX(index, geometry) {
return Math.round((
geometry.top[index] +
geometry.top[index + 1] +
geometry.bottom[index + 1] +
geometry.bottom[index]
) / 4);
}
function createSegment(item, index, geometry) {
var anchor = createSvgElement('a');
var polygon = createSvgElement('polygon');
var text = createSvgElement('text');
var href = buildPath(item.title);
anchor.setAttribute('class', 'portal-cat-anchor');
anchor.setAttribute('href', href);
anchor.setAttributeNS(XLINK_NS, 'xlink:href', href);
anchor.setAttribute('aria-label', item.label);
anchor.setAttribute('data-category-key', item.key);
polygon.setAttribute('class', 'portal-cat-shape');
polygon.setAttribute('points', pointsToString(getSegmentPoints(index, geometry)));
polygon.setAttribute('fill', 'transparent');
polygon.setAttribute('pointer-events', 'all');
text.setAttribute('class', 'portal-cat-label');
text.setAttribute('x', getTextX(index, geometry));
text.setAttribute('y', Math.round(geometry.height / 2) + 1);
text.setAttribute('fill', '#e2e2e2');
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.setAttribute('paint-order', 'stroke');
text.setAttribute('stroke', '#050505');
text.setAttribute('stroke-width', '2');
text.setAttribute('stroke-linejoin', 'round');
text.textContent = item.label;
anchor.appendChild(polygon);
anchor.appendChild(text);
return anchor;
}
function createDivider(index, geometry) {
var line = createSvgElement('line');
line.setAttribute('class', 'portal-cat-divider');
line.setAttribute('x1', geometry.top[index]);
line.setAttribute('y1', 0);
line.setAttribute('x2', geometry.bottom[index]);
line.setAttribute('y2', geometry.height);
line.setAttribute('stroke', '#050505');
line.setAttribute('stroke-width', '1');
line.setAttribute('pointer-events', 'none');
line.setAttribute('vector-effect', 'non-scaling-stroke');
return line;
}
function clearNode(node) {
while (node.firstChild) node.removeChild(node.firstChild);
}
function buildSvg(width) {
var geometry = calculateGeometry(width);
var svg = createSvgElement('svg');
var i;
svg.setAttribute('class', 'portal-category-svg');
svg.setAttribute('viewBox', '0 0 ' + geometry.width + ' ' + geometry.height);
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('aria-hidden', 'false');
ITEMS.forEach(function (item, index) {
svg.appendChild(createSegment(item, index, geometry));
});
for (i = 1; i < geometry.count; i++) {
svg.appendChild(createDivider(i, geometry));
}
return {
svg: svg,
key: geometry.width + '|' + geometry.height + '|' + geometry.cut + '|' + ITEMS.length
};
}
function renderSvgIntoNav(nav, mount) {
var result = buildSvg(getRenderableWidth(nav, mount));
if (nav.__categoryNavRenderKey === result.key && nav.querySelector('svg')) return;
nav.__categoryNavRenderKey = result.key;
clearNode(nav);
nav.appendChild(result.svg);
}
function scheduleRender(nav, mount) {
if (!nav || nav.__categoryNavFrame) return;
nav.__categoryNavFrame = window.requestAnimationFrame(function () {
nav.__categoryNavFrame = null;
renderSvgIntoNav(nav, mount);
});
}
function renderCategoryNavMount(mount) {
if (!mount) return;
if (mount.__categoryNavObserver) {
mount.__categoryNavObserver.disconnect();
mount.__categoryNavObserver = null;
}
clearNode(mount);
var nav = document.createElement('nav');
nav.className = 'portal-category-nav';
nav.setAttribute('aria-label', 'Main categories');
mount.appendChild(nav);
mount.setAttribute('data-category-nav-rendered', '1');
scheduleRender(nav, mount);
window.setTimeout(function () { scheduleRender(nav, mount); }, 80);
window.setTimeout(function () { scheduleRender(nav, mount); }, 250);
if (window.ResizeObserver) {
var observer = new ResizeObserver(function () {
scheduleRender(nav, mount);
});
observer.observe(mount);
observer.observe(nav);
mount.__categoryNavObserver = observer;
} else {
window.addEventListener('resize', function () {
scheduleRender(nav, mount);
});
}
}
function renderAllCategoryNavs(root) {
var scope = root || document;
var mounts = scope.querySelectorAll('[data-component="category-nav"]');
Array.prototype.forEach.call(mounts, function (mount) {
renderCategoryNavMount(mount);
});
}
window.CLBI = window.CLBI || {};
window.CLBI.categoryNav = {
init: renderAllCategoryNavs,
renderMount: renderCategoryNavMount
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () {
renderAllCategoryNavs(document);
});
} else {
renderAllCategoryNavs(document);
}
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
var root = $content && $content[0] ? $content[0] : document;
renderAllCategoryNavs(root);
});
}
}());