미디어위키:PortalSectionNav.js: 두 판 사이의 차이

(Install package: clbiwiki-portal-frame-display-fix-20260717 / js/PortalSectionNav.js)
(Install package: clbiwiki-portal-frame-pixel-stack-20260717 / js/PortalSectionNav.js)
9번째 줄: 9번째 줄:
     var XLINK_NS = 'http://www.w3.org/1999/xlink';
     var XLINK_NS = 'http://www.w3.org/1999/xlink';
     var COMPONENT = 'portal-section-nav';
     var COMPONENT = 'portal-section-nav';
     var VERSION = '20260717-shared-frame-phase-001';
     var VERSION = '20260717-pixel-stack-042';
     var renderTimer = 0;
     var renderTimer = 0;
     var ITEMS = [
     var ITEMS = [
79번째 줄: 79번째 줄:
             geometry.leftSeamX,
             geometry.leftSeamX,
             geometry.rightSeamX,
             geometry.rightSeamX,
             f.BORDER,
             d.FRAME_SURFACE_TOP,
             d.ROW_HEIGHT - f.BORDER,
             d.ROW_HEIGHT - f.BORDER,
             geometry.leftDir,
             geometry.leftDir,
90번째 줄: 90번째 줄:
             geometry.leftSeamX,
             geometry.leftSeamX,
             geometry.rightSeamX,
             geometry.rightSeamX,
             f.FRAME_FACE,
             d.WELL_TOP,
             d.ROW_HEIGHT - f.FRAME_FACE,
             d.WELL2_BOTTOM,
             geometry.leftDir,
             geometry.leftDir,
             geometry.rightDir,
             geometry.rightDir,
             geometry.phase,
             geometry.phase,
             f.FRAME_FACE,
             d.WELL_TOP,
             f.FRAME_FACE
             d.WELL_TOP
         );
         );
         var joinCover = frame.segmentAt(
         var joinCover = frame.segmentAt(
109번째 줄: 109번째 줄:
             0
             0
         );
         );
         var sideBevel = frame.segmentAt(
         var frameTopRow = frame.rowAt(
             geometry.leftSeamX,
             geometry.leftSeamX,
             geometry.rightSeamX,
             geometry.rightSeamX,
             d.OVERLAP,
             d.OVERLAP,
            d.ROW_HEIGHT - f.BORDER,
             geometry.leftDir,
             geometry.leftDir,
             geometry.rightDir,
             geometry.rightDir,
             geometry.phase,
             geometry.phase,
             f.BORDER + f.BEVEL,
             f.BORDER,
             f.BORDER + f.BEVEL
            f.BORDER
        );
        var frameBottomRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.ROW_HEIGHT - f.BORDER - f.BEVEL,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
             f.BORDER,
            f.BORDER
        );
        var wellTopRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.WELL_TOP,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            d.WELL_TOP,
            d.WELL_TOP
        );
        var wellBottomRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.WELL2_BOTTOM - f.BEVEL,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            d.WELL_TOP,
            d.WELL_TOP
         );
         );


125번째 줄: 154번째 줄:
         group.appendChild(frame.polygon('portal-section-frame-well', well));
         group.appendChild(frame.polygon('portal-section-frame-well', well));
         group.appendChild(frame.polygon('portal-section-join-side-cover', joinCover));
         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-shadow', frameBottomRow[0], frameTopRow[0]));
         group.appendChild(frame.line('portal-section-frame-highlight', sideBevel[1], sideBevel[2]));
         group.appendChild(frame.line('portal-section-frame-highlight', frameTopRow[1], frameBottomRow[1]));
         group.appendChild(frame.line('portal-section-well-shadow', well[0], well[1]));
         group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[0], wellTopRow[1]));
         group.appendChild(frame.line('portal-section-well-shadow', well[1], well[2]));
         group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[1], wellBottomRow[1]));
         group.appendChild(frame.line('portal-section-well-highlight', well[3], well[0]));
         group.appendChild(frame.line('portal-section-well-highlight', wellBottomRow[0], wellTopRow[0]));
         group.appendChild(frame.line('portal-section-well-highlight', well[2], well[3]));
         group.appendChild(frame.line('portal-section-well-highlight', wellBottomRow[1], wellBottomRow[0]));
         svg.appendChild(group);
         svg.appendChild(group);
     }
     }
138번째 줄: 167번째 줄:
         var f = frame.FRAME;
         var f = frame.FRAME;
         var d = frame.DIMENSIONS;
         var d = frame.DIMENSIONS;
         var leftBase = geometry.leftSeamX + f.FRAME_FACE + f.GAP;
         var leftBase = geometry.leftSeamX + d.WELL_BOTTOM;
         var rightBase = geometry.rightSeamX - f.FRAME_FACE - f.GAP;
         var rightBase = geometry.rightSeamX - d.WELL_BOTTOM;
         var leftTop = frame.slantX(leftBase, d.BUTTON_TOP, geometry.leftDir, geometry.phase);
         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 rightTop = frame.slantX(rightBase, d.BUTTON_TOP, geometry.rightDir, geometry.phase);
170번째 줄: 199번째 줄:
             button.leftTopX,
             button.leftTopX,
             button.rightTopX,
             button.rightTopX,
             d.BUTTON_TOP,
             d.BUTTON_EDGE_TOP,
             d.BUTTON_BOTTOM,
             d.BUTTON_EDGE_BOTTOM,
             button.leftDir,
             button.leftDir,
             button.rightDir,
             button.rightDir,
181번째 줄: 210번째 줄:
             button.leftTopX,
             button.leftTopX,
             button.rightTopX,
             button.rightTopX,
             d.BUTTON_TOP + f.BORDER,
             d.BUTTON_SURFACE_TOP,
             d.BUTTON_BOTTOM - f.BORDER,
             d.BUTTON_SURFACE_BOTTOM,
             button.leftDir,
             button.leftDir,
             button.rightDir,
             button.rightDir,
189번째 줄: 218번째 줄:
             f.BORDER
             f.BORDER
         );
         );
         var bevel = frame.segmentAt(
         var topInset = frame.rowAt(
             button.leftTopX,
             button.leftTopX,
             button.rightTopX,
             button.rightTopX,
             d.BUTTON_TOP + f.BORDER + f.BEVEL,
             d.BUTTON_SURFACE_TOP,
            d.BUTTON_BOTTOM - f.BORDER - f.BEVEL,
             button.leftDir,
             button.leftDir,
             button.rightDir,
             button.rightDir,
             button.phase,
             button.phase,
             f.BORDER + f.BEVEL,
             f.BORDER,
             f.BORDER + f.BEVEL
            f.BORDER
        );
        var bottomInset = frame.rowAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_SURFACE_BOTTOM - f.BEVEL,
            button.leftDir,
            button.rightDir,
            button.phase,
             f.BORDER,
            f.BORDER
         );
         );
         var anchor = frame.create('a', 'portal-cat-anchor portal-section-anchor');
         var anchor = frame.create('a', 'portal-cat-anchor portal-section-anchor');
212번째 줄: 250번째 줄:
         group.appendChild(frame.polygon('portal-cat-edge', points));
         group.appendChild(frame.polygon('portal-cat-edge', points));
         group.appendChild(frame.polygon('portal-cat-surface', surface));
         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-top', topInset[0], topInset[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-right', topInset[1], bottomInset[1]));
         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-bottom', bottomInset[1], bottomInset[0]));
         group.appendChild(frame.line('portal-cat-inset portal-cat-inset-left', bevel[3], bevel[0]));
         group.appendChild(frame.line('portal-cat-inset portal-cat-inset-left', bottomInset[0], topInset[0]));
         hit.setAttribute('pointer-events', 'all');
         hit.setAttribute('pointer-events', 'all');
         group.appendChild(hit);
         group.appendChild(hit);
221번째 줄: 259번째 줄:


         text.setAttribute('x', frame.centerX(surface));
         text.setAttribute('x', frame.centerX(surface));
         text.setAttribute('y', Math.round((d.BUTTON_TOP + d.BUTTON_BOTTOM) / 2) + 1);
         text.setAttribute('y', Math.round((d.BUTTON_SURFACE_TOP + d.BUTTON_SURFACE_BOTTOM) / 2) + 1);
         text.setAttribute('font-size', '12');
         text.setAttribute('font-size', '12');
         text.setAttribute('font-weight', '700');
         text.setAttribute('font-weight', '700');

2026년 7월 17일 (금) 12:00 판

/* =========================================
PortalSectionNav
대문 상단 카테고리 하단 부착형 섹션 버튼
========================================= */

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

    var XLINK_NS = 'http://www.w3.org/1999/xlink';
    var COMPONENT = 'portal-section-nav';
    var VERSION = '20260717-pixel-stack-042';
    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,
            d.FRAME_SURFACE_TOP,
            d.ROW_HEIGHT - f.BORDER,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.BORDER,
            f.BORDER
        );
        var well = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.WELL_TOP,
            d.WELL2_BOTTOM,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            d.WELL_TOP,
            d.WELL_TOP
        );
        var joinCover = frame.segmentAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            0,
            d.OVERLAP,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            0,
            0
        );
        var frameTopRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.OVERLAP,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.BORDER,
            f.BORDER
        );
        var frameBottomRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.ROW_HEIGHT - f.BORDER - f.BEVEL,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            f.BORDER,
            f.BORDER
        );
        var wellTopRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.WELL_TOP,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            d.WELL_TOP,
            d.WELL_TOP
        );
        var wellBottomRow = frame.rowAt(
            geometry.leftSeamX,
            geometry.rightSeamX,
            d.WELL2_BOTTOM - f.BEVEL,
            geometry.leftDir,
            geometry.rightDir,
            geometry.phase,
            d.WELL_TOP,
            d.WELL_TOP
        );

        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', frameBottomRow[0], frameTopRow[0]));
        group.appendChild(frame.line('portal-section-frame-highlight', frameTopRow[1], frameBottomRow[1]));
        group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[0], wellTopRow[1]));
        group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[1], wellBottomRow[1]));
        group.appendChild(frame.line('portal-section-well-highlight', wellBottomRow[0], wellTopRow[0]));
        group.appendChild(frame.line('portal-section-well-highlight', wellBottomRow[1], wellBottomRow[0]));
        svg.appendChild(group);
    }

    function splitButtons(geometry) {
        var frame = api();
        var f = frame.FRAME;
        var d = frame.DIMENSIONS;
        var leftBase = geometry.leftSeamX + d.WELL_BOTTOM;
        var rightBase = geometry.rightSeamX - d.WELL_BOTTOM;
        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_EDGE_TOP,
            d.BUTTON_EDGE_BOTTOM,
            button.leftDir,
            button.rightDir,
            button.phase,
            0,
            0
        );
        var surface = frame.segmentAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_SURFACE_TOP,
            d.BUTTON_SURFACE_BOTTOM,
            button.leftDir,
            button.rightDir,
            button.phase,
            f.BORDER,
            f.BORDER
        );
        var topInset = frame.rowAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_SURFACE_TOP,
            button.leftDir,
            button.rightDir,
            button.phase,
            f.BORDER,
            f.BORDER
        );
        var bottomInset = frame.rowAt(
            button.leftTopX,
            button.rightTopX,
            d.BUTTON_SURFACE_BOTTOM - f.BEVEL,
            button.leftDir,
            button.rightDir,
            button.phase,
            f.BORDER,
            f.BORDER
        );
        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', topInset[0], topInset[1]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-right', topInset[1], bottomInset[1]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-bottom', bottomInset[1], bottomInset[0]));
        group.appendChild(frame.line('portal-cat-inset portal-cat-inset-left', bottomInset[0], topInset[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_SURFACE_TOP + d.BUTTON_SURFACE_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));