미디어위키:CategoryNav.js

Nxdsxn (토론 | 기여)님의 2026년 7월 11일 (토) 14:43 판 (Install package: clbiwiki-mainpage-remove-person-gap2-20260711 (1) / js/CategoryNav.js)

참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/* =========================================
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: '군_정치집단' }
    ];

    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);
        });
    }
}());