미디어위키:PortalSectionNav.js

Nxdsxn (토론 | 기여)님의 2026년 7월 17일 (금) 10:35 판 (Install package: clbiwiki-portal-frame-display-fix-20260717 / js/PortalSectionNav.js)

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

  • 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
  • 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
  • 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
  • 오페라: Ctrl-F5를 입력.
/* =========================================
PortalSectionNav
대문 상단 카테고리 하단 부착형 섹션 버튼
========================================= */

(function (window, document, mw) {
    'use strict';

    var XLINK_NS = 'http://www.w3.org/1999/xlink';
    var COMPONENT = 'portal-section-nav';
    var VERSION = '20260717-shared-frame-phase-001';
    var renderTimer = 0;
    var ITEMS = [
        { key:'black-ice', label:'블랙 아이스', title:'대문' },
        { key:'anecdote', label:'에넥도트', href:'/index.php/특수:모든문서?namespace=3000' }
    ];

    function api() {
        return window.CLBI && window.CLBI.frame;
    }

    function buildWikiPath(title) {
        if (typeof window.buildWikiPath === 'function') return window.buildWikiPath(title);
        return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
    }

    function ensureMount(portal, topMount) {
        var mount = portal.querySelector('[data-component="' + COMPONENT + '"]');
        if (!mount) {
            mount = document.createElement('div');
            mount.setAttribute('data-component', COMPONENT);
        }
        if (mount.parentNode !== topMount.parentNode || topMount.nextSibling !== mount) {
            topMount.parentNode.insertBefore(mount, topMount.nextSibling);
        }
        return mount;
    }

    function outerGeometry(seams) {
        var frame = api();
        var d = frame.DIMENSIONS;
        var phase = seams.bottomStartAbsY % frame.FRAME.SLANT_RISE;
        var leftSeamX = frame.slantX(
            seams.settingsLeftTopX,
            seams.bottomStartAbsY,
            seams.settingsLeftDir,
            0
        );
        var rightSeamX = frame.slantX(
            seams.corpRightTopX,
            seams.bottomStartAbsY,
            seams.corpRightDir,
            0
        );

        return {
            phase:phase,
            leftSeamX:leftSeamX,
            rightSeamX:rightSeamX,
            leftDir:seams.settingsLeftDir,
            rightDir:seams.corpRightDir,
            points:frame.segment(
                leftSeamX,
                rightSeamX,
                d.ROW_HEIGHT,
                seams.settingsLeftDir,
                seams.corpRightDir,
                phase
            )
        };
    }

    function appendFrame(svg, geometry) {
        var frame = api();
        var f = frame.FRAME;
        var d = frame.DIMENSIONS;
        var group = frame.create('g', 'portal-section-frame');
        var surface = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            f.BORDER,
            d.ROW_HEIGHT - f.BORDER,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.BORDER,
            f.BORDER
        );
        var well = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            f.FRAME_FACE,
            d.ROW_HEIGHT - f.FRAME_FACE,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.FRAME_FACE,
            f.FRAME_FACE
        );
        var joinCover = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            0,
            d.OVERLAP,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            0,
            0
        );
        var sideBevel = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.OVERLAP,
            d.ROW_HEIGHT - f.BORDER,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.BORDER + f.BEVEL,
            f.BORDER + f.BEVEL
        );

        group.appendChild(frame.polygon('portal-section-frame-edge', geometry.points));
        group.appendChild(frame.polygon('portal-section-frame-surface', surface));
        group.appendChild(frame.polygon('portal-section-frame-well', well));
        group.appendChild(frame.polygon('portal-section-join-side-cover', joinCover));
        group.appendChild(frame.line('portal-section-frame-shadow', sideBevel[3], sideBevel[0]));
        group.appendChild(frame.line('portal-section-frame-highlight', sideBevel[1], sideBevel[2]));
        group.appendChild(frame.line('portal-section-well-shadow', well[0], well[1]));
        group.appendChild(frame.line('portal-section-well-shadow', well[1], well[2]));
        group.appendChild(frame.line('portal-section-well-highlight', well[3], well[0]));
        group.appendChild(frame.line('portal-section-well-highlight', well[2], well[3]));
        svg.appendChild(group);
    }

    function splitButtons(geometry) {
        var frame = api();
        var f = frame.FRAME;
        var d = frame.DIMENSIONS;
        var leftBase = geometry.leftSeamX + f.FRAME_FACE + f.GAP;
        var rightBase = geometry.rightSeamX - f.FRAME_FACE - f.GAP;
        var leftTop = frame.slantX(leftBase, d.BUTTON_TOP, geometry.leftDir, geometry.phase);
        var rightTop = frame.slantX(rightBase, d.BUTTON_TOP, geometry.rightDir, geometry.phase);
        var topMid = Math.round((leftTop + rightTop) / 2);
        var halfGap = f.GAP / 2;

        return [
            {
                leftTopX:leftBase,
                rightTopX:topMid - halfGap,
                leftDir:geometry.leftDir,
                rightDir:0,
                phase:geometry.phase
            },
            {
                leftTopX:topMid + halfGap,
                rightTopX:rightBase,
                leftDir:0,
                rightDir:geometry.rightDir,
                phase:geometry.phase
            }
        ];
    }

    function appendButton(svg, item, button) {
        var frame = api();
        var f = frame.FRAME;
        var d = frame.DIMENSIONS;
        var points = frame.segmentAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_TOP,
            d.BUTTON_BOTTOM,
            button.leftDir,
            button.rightDir,
            button.phase,
            0,
            0
        );
        var surface = frame.segmentAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_TOP + f.BORDER,
            d.BUTTON_BOTTOM - f.BORDER,
            button.leftDir,
            button.rightDir,
            button.phase,
            f.BORDER,
            f.BORDER
        );
        var bevel = frame.segmentAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_TOP + f.BORDER + f.BEVEL,
            d.BUTTON_BOTTOM - f.BORDER - f.BEVEL,
            button.leftDir,
            button.rightDir,
            button.phase,
            f.BORDER + f.BEVEL,
            f.BORDER + f.BEVEL
        );
        var anchor = frame.create('a', 'portal-cat-anchor portal-section-anchor');
        var group = frame.create('g', 'portal-section-button-geometry');
        var hit = frame.polygon('portal-cat-hit', points);
        var text = frame.create('text', 'portal-cat-label portal-section-label');
        var href = item.href || buildWikiPath(item.title);

        anchor.setAttribute('data-category-key', item.key);
        anchor.setAttribute('href', href);
        anchor.setAttributeNS(XLINK_NS, 'xlink:href', href);
        anchor.setAttribute('aria-label', item.label);
        group.appendChild(frame.polygon('portal-cat-edge', points));
        group.appendChild(frame.polygon('portal-cat-surface', surface));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-top', bevel[0], bevel[1]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-right', bevel[1], bevel[2]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-bottom', bevel[2], bevel[3]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-left', bevel[3], bevel[0]));
        hit.setAttribute('pointer-events', 'all');
        group.appendChild(hit);
        anchor.appendChild(group);

        text.setAttribute('x', frame.centerX(surface));
        text.setAttribute('y', Math.round((d.BUTTON_TOP + d.BUTTON_BOTTOM) / 2) + 1);
        text.setAttribute('font-size', '12');
        text.setAttribute('font-weight', '700');
        text.setAttribute('text-anchor', 'middle');
        text.setAttribute('dominant-baseline', 'middle');
        text.textContent = item.label;
        anchor.appendChild(text);
        svg.appendChild(anchor);
    }

    function renderPortal(portal, seams) {
        var frame = api();
        var current = seams || (frame && frame.seams);
        var topMount;
        var mount;
        var nav;
        var svg;
        var geometry;
        var buttons;

        if (!frame || !portal || !current || !current.width) return false;
        topMount = portal.querySelector('[data-component="category-nav"]');
        if (!topMount) return false;
        mount = ensureMount(portal, topMount);
        geometry = outerGeometry(current);
        buttons = splitButtons(geometry);
        nav = document.createElement('nav');
        nav.className = 'portal-section-nav';
        nav.setAttribute('aria-label', '작품 영역');
        svg = frame.create('svg', 'portal-section-svg');
        svg.setAttribute('viewBox', '0 0 ' + current.width + ' ' + frame.DIMENSIONS.ROW_HEIGHT);
        svg.setAttribute('width', '100%');
        svg.setAttribute('height', frame.DIMENSIONS.ROW_HEIGHT);
        svg.setAttribute('preserveAspectRatio', 'none');
        svg.setAttribute('data-frame-width', current.width);

        appendFrame(svg, geometry);
        ITEMS.forEach(function (item, index) { appendButton(svg, item, buttons[index]); });
        nav.appendChild(svg);
        mount.replaceChildren(nav);
        mount.setAttribute('data-portal-section-rendered', '1');
        mount.setAttribute('data-portal-section-width', String(current.width));
        mount.setAttribute('data-portal-section-phase', String(geometry.phase));
        return true;
    }

    function collectPortals(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var portals = [];
        if (scope.matches && scope.matches('.main-portal')) portals.push(scope);
        Array.prototype.forEach.call(scope.querySelectorAll('.main-portal'), function (portal) {
            if (portals.indexOf(portal) === -1) portals.push(portal);
        });
        return portals;
    }

    function renderAll(root, seams) {
        var rendered = 0;
        collectPortals(root).forEach(function (portal) {
            if (renderPortal(portal, seams)) rendered += 1;
        });
        return rendered;
    }

    function schedule(root, seams) {
        window.clearTimeout(renderTimer);
        renderTimer = window.setTimeout(function () {
            renderTimer = 0;
            renderAll(root || document, seams);
        }, 0);
    }

    if (api()) api().onSeams(function (seams) { schedule(document, seams); });
    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function (content) {
            schedule(content && content[0] ? content[0] : document);
        });
    }

    window.PortalSectionNav = {
        version:VERSION,
        init:renderAll,
        render:renderPortal,
        renderAll:renderAll
    };
}(window, document, window.mw));