편집 요약 없음 |
편집 요약 없음 |
||
| 16번째 줄: | 16번째 줄: | ||
1. span 안에 [[문서|라벨]]을 넣는 방식 | 1. span 안에 [[문서|라벨]]을 넣는 방식 | ||
- MediaWiki가 내부에 <a>를 자동 생성한다. | - MediaWiki가 내부에 <a>를 자동 생성한다. | ||
- span은 | - span은 모양을 담당하고, a는 클릭을 담당하게 된다. | ||
- 실제 클릭 영역, 호버 영역, 시각 형태가 서로 분리된다. | - 실제 클릭 영역, 호버 영역, 시각 형태가 서로 분리된다. | ||
| 47번째 줄: | 47번째 줄: | ||
처음부터 사선 polygon을 생성한다. | 처음부터 사선 polygon을 생성한다. | ||
이번 수정 | |||
----------------------------------------- | ----------------------------------------- | ||
이전 버전에서는 mount를 비우고 nav를 만든 직후 nav 폭을 측정했다. | |||
MediaWiki/SPA 환경에서는 그 순간 nav 폭이 0으로 잡힐 수 있다. | |||
그 상태에서 SVG 생성을 return하면 빈 nav만 남아 카테고리 네비가 사라진다. | |||
이번 버전은 다음을 적용한다. | |||
1. 폭이 0이면 바로 포기하지 않고 재시도한다. | |||
2. nav 자체 폭이 0이면 mount, 부모, main-portal 폭까지 fallback으로 확인한다. | |||
3. 새 SVG가 완성되기 전에는 기존 SVG를 지우지 않는다. | |||
4. 같은 폭으로 이미 렌더링된 경우에는 불필요하게 다시 그리지 않는다. | |||
*/ | */ | ||
| 61번째 줄: | 67번째 줄: | ||
var SVG_NS = 'http://www.w3.org/2000/svg'; | var SVG_NS = 'http://www.w3.org/2000/svg'; | ||
var XLINK_NS = 'http://www.w3.org/1999/xlink'; | var XLINK_NS = 'http://www.w3.org/1999/xlink'; | ||
var CATEGORY_NAV_ITEMS = [ | var CATEGORY_NAV_ITEMS = [ | ||
| 116번째 줄: | 112번째 줄: | ||
} | } | ||
]; | ]; | ||
function getCurrentLanguageSafe() { | function getCurrentLanguageSafe() { | ||
| 137번째 줄: | 126번째 줄: | ||
return 'ko'; | return 'ko'; | ||
} | } | ||
function buildPath(title) { | function buildPath(title) { | ||
| 151번째 줄: | 133번째 줄: | ||
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_')); | return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_')); | ||
} | |||
function getLabel(item, lang) { | |||
if (lang === 'ko') { | |||
return item.label; | |||
} | |||
/* | |||
다국어 구조가 확정되기 전까지는 한국어 fallback을 유지한다. | |||
부분 번역을 섞으면 네비 폭과 의미가 언어별로 흔들릴 수 있다. | |||
*/ | |||
return item.label; | |||
} | } | ||
/* | /* | ||
폭 측정 | |||
----------------------------------------- | ----------------------------------------- | ||
nav.getBoundingClientRect().width가 0일 수 있으므로 | |||
여러 단계로 fallback을 둔다. | |||
이 함수가 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') | |||
]; | |||
for (var i = 0; i < candidates.length; i++) { | |||
var el = candidates[i]; | |||
if (!el) continue; | |||
var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; | |||
var width = rect ? Math.floor(rect.width || 0) : 0; | |||
if (!width && el.clientWidth) { | |||
width = Math.floor(el.clientWidth); | |||
} | |||
if (width > 0) { | |||
return width; | |||
} | |||
} | } | ||
return | return 0; | ||
} | } | ||
| 176번째 줄: | 190번째 줄: | ||
좌표 계산 | 좌표 계산 | ||
----------------------------------------- | ----------------------------------------- | ||
프로젝트를 제외한 6개 버튼의 기준 폭은 188px이다. | |||
163px 기준에서 15% 확대한 값이다. | |||
163 * 1.15 = 187.45 이므로 실제 기준은 188px로 둔다. | |||
*/ | */ | ||
function calculateGeometry(width) { | function calculateGeometry(width) { | ||
var h = 30; | var h = 30; | ||
var idealCell = | var idealCell = 188; | ||
var minCell = 96; | var minCell = 96; | ||
var minProject = 180; | var minProject = 180; | ||
| 208번째 줄: | 216번째 줄: | ||
} | } | ||
var totalW = (cellW * 6) + projectW; | var totalW = (cellW * 6) + projectW; | ||
| 245번째 줄: | 247번째 줄: | ||
프로젝트: \ PROJECT / | 프로젝트: \ PROJECT / | ||
오른쪽군: / / / | 오른쪽군: / / / | ||
*/ | */ | ||
| 348번째 줄: | 337번째 줄: | ||
return document.createElementNS(SVG_NS, name); | return document.createElementNS(SVG_NS, name); | ||
} | } | ||
function createSegment(item, index, g, lang) { | function createSegment(item, index, g, lang) { | ||
| 372번째 줄: | 354번째 줄: | ||
polygon.setAttribute('points', pointsToString(getSegmentPoints(index, g))); | polygon.setAttribute('points', pointsToString(getSegmentPoints(index, g))); | ||
text.setAttribute('class', item.key === 'project' | text.setAttribute( | ||
'class', | |||
item.key === 'project' | |||
? 'portal-cat-label portal-cat-project-label' | |||
: 'portal-cat-label' | |||
); | ); | ||
text.setAttribute('x', getTextX(index, g)); | text.setAttribute('x', getTextX(index, g)); | ||
text.setAttribute('y', g.height / 2 + 1); | text.setAttribute('y', g.height / 2 + 1); | ||
| 385번째 줄: | 370번째 줄: | ||
return anchor; | return anchor; | ||
} | } | ||
function createDivider(x1, y1, x2, y2, side) { | function createDivider(x1, y1, x2, y2, side) { | ||
| 410번째 줄: | 388번째 줄: | ||
var h = g.height; | var h = g.height; | ||
svg.appendChild(createDivider(x[1] - c, 0, x[1], h, 'left')); | svg.appendChild(createDivider(x[1] - c, 0, x[1], h, 'left')); | ||
svg.appendChild(createDivider(x[2] - c, 0, x[2], h, 'left')); | svg.appendChild(createDivider(x[2] - c, 0, x[2], h, 'left')); | ||
svg.appendChild(createDivider(x[3] - c, 0, x[3], h, 'left')); | svg.appendChild(createDivider(x[3] - c, 0, x[3], h, 'left')); | ||
svg.appendChild(createDivider(x[4], 0, x[4] - c, h, 'right')); | svg.appendChild(createDivider(x[4], 0, x[4] - c, h, 'right')); | ||
svg.appendChild(createDivider(x[5], 0, x[5] - c, h, 'right')); | svg.appendChild(createDivider(x[5], 0, x[5] - c, h, 'right')); | ||
| 432번째 줄: | 404번째 줄: | ||
/* | /* | ||
SVG 생성 | |||
----------------------------------------- | ----------------------------------------- | ||
기존 SVG를 즉시 지우지 않고, 새 SVG를 완성한 뒤 교체한다. | |||
폭 측정이 실패하면 빈 상태로 만들지 않고 재시도한다. | |||
*/ | */ | ||
function | function buildSvg(width) { | ||
var lang = getCurrentLanguageSafe(); | var lang = getCurrentLanguageSafe(); | ||
var g = calculateGeometry( | var g = calculateGeometry(width); | ||
var svg = createSvgElement('svg'); | var svg = createSvgElement('svg'); | ||
svg.setAttribute('class', 'portal-category-svg'); | svg.setAttribute('class', 'portal-category-svg'); | ||
svg.setAttribute('viewBox', '0 0 ' + g.width + ' ' + g.height); | svg.setAttribute('viewBox', '0 0 ' + g.width + ' ' + g.height); | ||
svg.setAttribute('width', | svg.setAttribute('width', '100%'); | ||
svg.setAttribute('height', | svg.setAttribute('height', '100%'); | ||
svg.setAttribute('preserveAspectRatio', 'none'); | svg.setAttribute('preserveAspectRatio', 'none'); | ||
svg.setAttribute('aria-hidden', 'false'); | svg.setAttribute('aria-hidden', 'false'); | ||
| 463번째 줄: | 428번째 줄: | ||
appendDividers(svg, g); | appendDividers(svg, g); | ||
return { | |||
svg: svg, | |||
key: g.width + '|' + g.height + '|' + g.cellW + '|' + g.projectW + '|' + g.cut | |||
}; | |||
} | |||
function renderSvgIntoNav(nav, mount, attempt) { | |||
var measuredWidth = getRenderableWidth(nav, mount); | |||
var retry = typeof attempt === 'number' ? attempt : 0; | |||
if (!measuredWidth) { | |||
if (retry < 12) { | |||
window.setTimeout(function () { | |||
renderSvgIntoNav(nav, mount, retry + 1); | |||
}, 80); | |||
} | |||
return; | |||
} | |||
var result = buildSvg(measuredWidth); | |||
if (nav.__categoryNavRenderKey === result.key && nav.querySelector('svg')) { | |||
return; | |||
} | |||
nav.__categoryNavRenderKey = result.key; | |||
clearNode(nav); | clearNode(nav); | ||
nav.appendChild(svg); | nav.appendChild(result.svg); | ||
} | } | ||
function scheduleRender(nav) { | function scheduleRender(nav, mount) { | ||
if (!nav || nav.__categoryNavFrame) { | if (!nav || nav.__categoryNavFrame) { | ||
return; | return; | ||
| 474번째 줄: | 466번째 줄: | ||
nav.__categoryNavFrame = window.requestAnimationFrame(function () { | nav.__categoryNavFrame = window.requestAnimationFrame(function () { | ||
nav.__categoryNavFrame = null; | nav.__categoryNavFrame = null; | ||
renderSvgIntoNav(nav); | renderSvgIntoNav(nav, mount, 0); | ||
}); | }); | ||
} | } | ||
function renderCategoryNavMount(mount) { | function renderCategoryNavMount(mount) { | ||
| 506번째 줄: | 487번째 줄: | ||
mount.setAttribute('data-category-nav-rendered', '1'); | mount.setAttribute('data-category-nav-rendered', '1'); | ||
scheduleRender(nav); | scheduleRender(nav, mount); | ||
if (window.ResizeObserver) { | if (window.ResizeObserver) { | ||
var observer = new ResizeObserver(function () { | var observer = new ResizeObserver(function () { | ||
scheduleRender(nav); | scheduleRender(nav, mount); | ||
}); | }); | ||
observer.observe( | observer.observe(mount); | ||
mount.__categoryNavObserver = observer; | mount.__categoryNavObserver = observer; | ||
} else { | } else { | ||
window.addEventListener('resize', function () { | window.addEventListener('resize', function () { | ||
scheduleRender(nav); | scheduleRender(nav, mount); | ||
}); | }); | ||
} | } | ||
| 535번째 줄: | 511번째 줄: | ||
}); | }); | ||
} | } | ||
window.CLBI = window.CLBI || {}; | window.CLBI = window.CLBI || {}; | ||
2026년 5월 19일 (화) 07:55 판
/* =========================================
COASTLINE: BLACK ICE - CategoryNav
대문 카테고리 네비 SVG 생성기
========================================= */
/*
설계 목적
-----------------------------------------
이 파일은 대문 카테고리 네비를 MediaWiki 본문 위키문법이 아니라
JavaScript + SVG로 생성한다.
왜 SVG를 쓰는가
-----------------------------------------
CSS 박스 기반 구현에서 다음 문제가 반복되었다.
1. span 안에 [[문서|라벨]]을 넣는 방식
- MediaWiki가 내부에 <a>를 자동 생성한다.
- span은 모양을 담당하고, a는 클릭을 담당하게 된다.
- 실제 클릭 영역, 호버 영역, 시각 형태가 서로 분리된다.
2. <a class="...">를 본문에 직접 넣는 방식
- MediaWiki 본문에서 안정적으로 먹히지 않았다.
- 위키 문법 처리와 충돌해 코드가 무효화되는 문제가 있었다.
3. clip-path + ::after 방식
- clip-path가 검은 획 pseudo-element까지 잘라먹었다.
- 왼쪽 항목의 획이 사라지거나 일부만 보였다.
4. 버튼을 음수 margin으로 겹치는 방식
- 사선 빈 영역은 메웠지만, 호버가 옆 칸으로 넘어갔다.
5. hover overlay / divider overlay 방식
- CSS grid, absolute 좌표, 프로젝트 가변폭이 서로 다른 기준을 가져서
호버 면과 검은 획이 계속 어긋났다.
SVG 방식의 장점
-----------------------------------------
SVG에서는 각 항목을 polygon으로 만든다.
따라서 다음 요소들이 모두 같은 좌표계를 사용한다.
- 보이는 버튼 면
- 실제 hover 면
- 실제 click 면
- 검은 구분선
즉, 이전처럼 사각형 박스를 사선처럼 보이게 흉내 내는 방식이 아니라,
처음부터 사선 polygon을 생성한다.
이번 수정
-----------------------------------------
이전 버전에서는 mount를 비우고 nav를 만든 직후 nav 폭을 측정했다.
MediaWiki/SPA 환경에서는 그 순간 nav 폭이 0으로 잡힐 수 있다.
그 상태에서 SVG 생성을 return하면 빈 nav만 남아 카테고리 네비가 사라진다.
이번 버전은 다음을 적용한다.
1. 폭이 0이면 바로 포기하지 않고 재시도한다.
2. nav 자체 폭이 0이면 mount, 부모, main-portal 폭까지 fallback으로 확인한다.
3. 새 SVG가 완성되기 전에는 기존 SVG를 지우지 않는다.
4. 같은 폭으로 이미 렌더링된 경우에는 불필요하게 다시 그리지 않는다.
*/
(function () {
'use strict';
var SVG_NS = 'http://www.w3.org/2000/svg';
var XLINK_NS = 'http://www.w3.org/1999/xlink';
var CATEGORY_NAV_ITEMS = [
{
key: 'history',
label: '역사적 사건',
title: '역사적_사건',
type: 'left-start'
},
{
key: 'settings',
label: '설정',
title: '설정',
type: 'left-mid'
},
{
key: 'nations',
label: '국가 및 조합',
title: '국가_및_조합',
type: 'left-end'
},
{
key: 'project',
label: '프로젝트',
title: '프로젝트:소개',
type: 'project'
},
{
key: 'corporations',
label: '기업 및 공동체',
title: '기업_및_공동체',
type: 'right-start'
},
{
key: 'military',
label: '군, 정치집단',
title: '군_정치집단',
type: 'right-mid'
},
{
key: 'personnel',
label: '인물',
title: '인물',
type: 'right-end'
}
];
function getCurrentLanguageSafe() {
if (typeof window.getCurrentLang === 'function') {
return window.getCurrentLang();
}
var langData = document.getElementById('clbi-lang-data');
if (langData) {
return langData.getAttribute('data-lang') || 'ko';
}
return 'ko';
}
function buildPath(title) {
if (typeof window.buildWikiPath === 'function') {
return window.buildWikiPath(title);
}
return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}
function getLabel(item, lang) {
if (lang === 'ko') {
return item.label;
}
/*
다국어 구조가 확정되기 전까지는 한국어 fallback을 유지한다.
부분 번역을 섞으면 네비 폭과 의미가 언어별로 흔들릴 수 있다.
*/
return item.label;
}
/*
폭 측정
-----------------------------------------
nav.getBoundingClientRect().width가 0일 수 있으므로
여러 단계로 fallback을 둔다.
이 함수가 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')
];
for (var i = 0; i < candidates.length; i++) {
var el = candidates[i];
if (!el) continue;
var rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null;
var width = rect ? Math.floor(rect.width || 0) : 0;
if (!width && el.clientWidth) {
width = Math.floor(el.clientWidth);
}
if (width > 0) {
return width;
}
}
return 0;
}
/*
좌표 계산
-----------------------------------------
프로젝트를 제외한 6개 버튼의 기준 폭은 188px이다.
163px 기준에서 15% 확대한 값이다.
163 * 1.15 = 187.45 이므로 실제 기준은 188px로 둔다.
*/
function calculateGeometry(width) {
var h = 30;
var idealCell = 188;
var minCell = 96;
var minProject = 180;
var usableWidth = Math.max(320, width);
var cellW = idealCell;
if ((cellW * 6) + minProject > usableWidth) {
cellW = Math.floor((usableWidth - minProject) / 6);
cellW = Math.max(minCell, cellW);
}
var projectW = usableWidth - (cellW * 6);
if (projectW < 100) {
projectW = 100;
}
var totalW = (cellW * 6) + projectW;
var cut = Math.round(cellW * 0.061);
cut = Math.max(6, Math.min(10, cut));
var x0 = 0;
var x1 = cellW;
var x2 = cellW * 2;
var x3 = cellW * 3;
var x4 = x3 + projectW;
var x5 = x4 + cellW;
var x6 = x4 + (cellW * 2);
var x7 = x4 + (cellW * 3);
return {
width: totalW,
height: h,
cellW: cellW,
projectW: projectW,
cut: cut,
x: [x0, x1, x2, x3, x4, x5, x6, x7]
};
}
/*
polygon 좌표
-----------------------------------------
요구 형태:
왼쪽군: \ \ \
프로젝트: \ PROJECT /
오른쪽군: / / /
*/
function getSegmentPoints(index, g) {
var x = g.x;
var c = g.cut;
var h = g.height;
if (index === 0) {
return [
[x[0], 0],
[x[1] - c, 0],
[x[1], h],
[x[0], h]
];
}
if (index === 1) {
return [
[x[1] - c, 0],
[x[2] - c, 0],
[x[2], h],
[x[1], h]
];
}
if (index === 2) {
return [
[x[2] - c, 0],
[x[3] - c, 0],
[x[3], h],
[x[2], h]
];
}
if (index === 3) {
return [
[x[3] - c, 0],
[x[4], 0],
[x[4] - c, h],
[x[3], h]
];
}
if (index === 4) {
return [
[x[4], 0],
[x[5], 0],
[x[5] - c, h],
[x[4] - c, h]
];
}
if (index === 5) {
return [
[x[5], 0],
[x[6], 0],
[x[6] - c, h],
[x[5] - c, h]
];
}
return [
[x[6], 0],
[x[7], 0],
[x[7], h],
[x[6] - c, h]
];
}
function pointsToString(points) {
return points.map(function (point) {
return point[0] + ',' + point[1];
}).join(' ');
}
function getTextX(index, g) {
var x = g.x;
if (index === 0) return (x[0] + x[1]) / 2;
if (index === 1) return (x[1] + x[2]) / 2;
if (index === 2) return (x[2] + x[3]) / 2;
if (index === 3) return (x[3] + x[4]) / 2;
if (index === 4) return (x[4] + x[5]) / 2;
if (index === 5) return (x[5] + x[6]) / 2;
return (x[6] + x[7]) / 2;
}
function createSvgElement(name) {
return document.createElementNS(SVG_NS, name);
}
function createSegment(item, index, g, lang) {
var anchor = createSvgElement('a');
var polygon = createSvgElement('polygon');
var text = createSvgElement('text');
var href = buildPath(item.title);
var label = getLabel(item, lang);
anchor.setAttribute('class', 'portal-cat-anchor');
anchor.setAttribute('href', href);
anchor.setAttributeNS(XLINK_NS, 'xlink:href', href);
anchor.setAttribute('aria-label', label);
anchor.setAttribute('data-category-key', item.key);
polygon.setAttribute('class', 'portal-cat-shape');
polygon.setAttribute('points', pointsToString(getSegmentPoints(index, g)));
text.setAttribute(
'class',
item.key === 'project'
? 'portal-cat-label portal-cat-project-label'
: 'portal-cat-label'
);
text.setAttribute('x', getTextX(index, g));
text.setAttribute('y', g.height / 2 + 1);
text.textContent = label;
anchor.appendChild(polygon);
anchor.appendChild(text);
return anchor;
}
function createDivider(x1, y1, x2, y2, side) {
var line = createSvgElement('line');
line.setAttribute('class', 'portal-cat-divider portal-cat-divider-' + side);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
return line;
}
function appendDividers(svg, g) {
var x = g.x;
var c = g.cut;
var h = g.height;
svg.appendChild(createDivider(x[1] - c, 0, x[1], h, 'left'));
svg.appendChild(createDivider(x[2] - c, 0, x[2], h, 'left'));
svg.appendChild(createDivider(x[3] - c, 0, x[3], h, 'left'));
svg.appendChild(createDivider(x[4], 0, x[4] - c, h, 'right'));
svg.appendChild(createDivider(x[5], 0, x[5] - c, h, 'right'));
svg.appendChild(createDivider(x[6], 0, x[6] - c, h, 'right'));
}
function clearNode(node) {
while (node.firstChild) {
node.removeChild(node.firstChild);
}
}
/*
SVG 생성
-----------------------------------------
기존 SVG를 즉시 지우지 않고, 새 SVG를 완성한 뒤 교체한다.
폭 측정이 실패하면 빈 상태로 만들지 않고 재시도한다.
*/
function buildSvg(width) {
var lang = getCurrentLanguageSafe();
var g = calculateGeometry(width);
var svg = createSvgElement('svg');
svg.setAttribute('class', 'portal-category-svg');
svg.setAttribute('viewBox', '0 0 ' + g.width + ' ' + g.height);
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('aria-hidden', 'false');
CATEGORY_NAV_ITEMS.forEach(function (item, index) {
svg.appendChild(createSegment(item, index, g, lang));
});
appendDividers(svg, g);
return {
svg: svg,
key: g.width + '|' + g.height + '|' + g.cellW + '|' + g.projectW + '|' + g.cut
};
}
function renderSvgIntoNav(nav, mount, attempt) {
var measuredWidth = getRenderableWidth(nav, mount);
var retry = typeof attempt === 'number' ? attempt : 0;
if (!measuredWidth) {
if (retry < 12) {
window.setTimeout(function () {
renderSvgIntoNav(nav, mount, retry + 1);
}, 80);
}
return;
}
var result = buildSvg(measuredWidth);
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, 0);
});
}
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);
if (window.ResizeObserver) {
var observer = new ResizeObserver(function () {
scheduleRender(nav, mount);
});
observer.observe(mount);
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);
});
}
}());