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

(Install package: clbiwiki-mainpage-category-project-pillar-20260716 / js/CategoryPillar.js)
 
(Install package: clbiwiki-portal-frame-fixed / js/CategoryPillar.js)
 
(같은 사용자의 중간 판 5개는 보이지 않습니다)
1번째 줄: 1번째 줄:
/* =========================================
/* =========================================
CategoryPillar
CategoryPillar
대문 상단 카테고리의 중앙 「프로젝트」 기둥
canvas CategoryNav 내부 프로젝트 기둥 호환 모듈
========================================= */
========================================= */
/*
CategoryNav.js가 만든 네 개의 실제 버튼은 그대로 둔다.
중앙 project 항목의 SVG 좌표만 읽어서 클릭 가능한 버튼을 제거하고,
상하 우물을 관통해 #1d1d1d 외곽 프레임에 이어지는 비조작 기둥으로 바꾼다.
*/


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


    var SVG_NS = 'http://www.w3.org/2000/svg';
     function renderAll(root) {
    var VERSION = '20260716-category-project-pillar-019';
    var resizeTimer = 0;
    var renderTimer = 0;
    var observer = null;
 
     function createSvgElement(name, className) {
        var node = document.createElementNS(SVG_NS, name);
        if (className) node.setAttribute('class', className);
        return node;
    }
 
    function parsePoints(value) {
        var points = [];
 
        String(value || '').trim().split(/\s+/).forEach(function (pair) {
            var xy = pair.split(',');
            var x = Number(xy[0]);
            var y = Number(xy[1]);
 
            if (xy.length === 2 && Number.isFinite(x) && Number.isFinite(y)) {
                points.push([x, y]);
            }
        });
 
        return points;
    }
 
    function getViewBox(svg) {
        var base = svg && svg.viewBox && svg.viewBox.baseVal;
        var raw;
        var values;
 
        if (base && base.width > 0 && base.height > 0) {
            return {
                x:base.x,
                y:base.y,
                width:base.width,
                height:base.height
            };
        }
 
        raw = String(svg.getAttribute('viewBox') || '').trim();
        values = raw.split(/[\s,]+/).map(Number);
        if (
            values.length === 4 &&
            values.every(Number.isFinite) &&
            values[2] > 0 &&
            values[3] > 0
        ) {
            return { x:values[0], y:values[1], width:values[2], height:values[3] };
        }
 
        return null;
    }
 
    function getVerticalExtension(svg, viewBox) {
        var nav = svg.closest ? svg.closest('.portal-category-nav') : svg.parentNode;
        var style;
        var rect;
        var renderedHeight;
        var paddingTop;
        var paddingBottom;
        var scale;
 
        if (!nav || !window.getComputedStyle) return { top:3, bottom:3 };
 
        style = window.getComputedStyle(nav);
        rect = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null;
        renderedHeight = rect && rect.height ? rect.height : Number(svg.clientHeight || 0);
        paddingTop = parseFloat(style.paddingTop) || 0;
        paddingBottom = parseFloat(style.paddingBottom) || 0;
        scale = renderedHeight > 0 ? viewBox.height / renderedHeight : 1;
 
        return {
            top:paddingTop > 0 ? paddingTop * scale : 3,
            bottom:paddingBottom > 0 ? paddingBottom * scale : 3
        };
    }
 
    function setLine(line, x1, y1, x2, y2) {
        line.setAttribute('x1', String(x1));
        line.setAttribute('y1', String(y1));
        line.setAttribute('x2', String(x2));
        line.setAttribute('y2', String(y2));
    }
 
    function findProjectSource(svg) {
        var direct = svg.querySelector(
            '.portal-cat-anchor[data-category-key="project"]'
        );
        var match = null;
 
        if (direct) return direct;
 
        Array.prototype.some.call(svg.querySelectorAll('.portal-cat-anchor'), function (anchor) {
            var label = anchor.querySelector('.portal-cat-label');
            if (label && String(label.textContent || '').trim() === '프로젝트') {
                match = anchor;
                return true;
            }
            return false;
        });
 
        return match;
    }
 
    function renderSvg(svg) {
        var source = findProjectSource(svg);
        var edge = source && source.querySelector('.portal-cat-edge');
        var sourceLabel = source && source.querySelector('.portal-cat-label');
        var points = edge ? parsePoints(edge.getAttribute('points')) : [];
        var viewBox = getViewBox(svg);
        var previous = svg.querySelector('.portal-category-pillar');
        var extension;
        var left;
        var right;
        var top;
        var bottom;
        var group;
        var surface;
        var leftEdge;
        var leftInset;
        var rightEdge;
        var rightInset;
        var label;
 
        if (!source || points.length < 4 || !viewBox) return false;
        if (previous && previous.parentNode) previous.parentNode.removeChild(previous);
 
        /*
        기존 project 다각형의 상·하가 모두 차지하던 공통 폭만 사용한다.
        사선의 가장 넓은 폭을 직사각형으로 펴면 이웃 버튼 위를 덮게 되므로,
        좌측 두 점 중 안쪽 값과 우측 두 점 중 안쪽 값을 기둥 경계로 삼는다.
        CategoryNav의 점 순서는 좌상→우상→우하→좌하다.
        */
        left = Math.max(points[0][0], points[3][0]);
        right = Math.min(points[1][0], points[2][0]);
        if (!(right > left)) {
            left = Math.min.apply(null, points.map(function (point) { return point[0]; }));
            right = Math.max.apply(null, points.map(function (point) { return point[0]; }));
        }
        if (!(right > left)) return false;
 
        extension = getVerticalExtension(svg, viewBox);
        top = viewBox.y - extension.top;
        bottom = viewBox.y + viewBox.height + extension.bottom;
 
        group = createSvgElement('g', 'portal-category-pillar');
        group.setAttribute('data-category-key', 'project');
        group.setAttribute('aria-label', sourceLabel ? sourceLabel.textContent : '프로젝트');
        group.setAttribute('pointer-events', 'none');
 
        surface = createSvgElement('rect', 'portal-category-pillar-surface');
        surface.setAttribute('x', String(left));
        surface.setAttribute('y', String(top));
        surface.setAttribute('width', String(right - left));
        surface.setAttribute('height', String(bottom - top));
        group.appendChild(surface);
 
        /*
        상하선은 만들지 않는다. 그래야 같은 #1d1d1d인 외곽 프레임과 기둥이
        끊기지 않는다. 좌우에만 #050505 경계와 방향성 inset을 둔다.
        */
        leftEdge = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-edge');
        setLine(leftEdge, left + 0.5, top, left + 0.5, bottom);
        group.appendChild(leftEdge);
 
        leftInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-shadow');
        setLine(leftInset, left + 1.5, top, left + 1.5, bottom);
        group.appendChild(leftInset);
 
        rightEdge = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-edge');
        setLine(rightEdge, right - 0.5, top, right - 0.5, bottom);
        group.appendChild(rightEdge);
 
        rightInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-highlight');
        setLine(rightInset, right - 1.5, top, right - 1.5, bottom);
        group.appendChild(rightInset);
 
        label = createSvgElement('text', 'portal-cat-label portal-category-pillar-label');
        label.setAttribute('x', String((left + right) / 2));
        label.setAttribute('y', String(viewBox.y + viewBox.height / 2));
        label.textContent = sourceLabel ? sourceLabel.textContent : '프로젝트';
        group.appendChild(label);
 
        source.setAttribute('aria-hidden', 'true');
        source.setAttribute('tabindex', '-1');
        source.classList.add('portal-category-pillar-source');
        svg.appendChild(group);
        svg.classList.add('has-category-pillar');
        return true;
    }
 
    function collectSvgs(root) {
         var scope = root && root.querySelectorAll ? root : document;
         var scope = root && root.querySelectorAll ? root : document;
         var output = [];
         return scope.querySelectorAll(
 
             '.main-portal [data-component="category-nav"] .portal-category-canvas'
        if (scope.matches && scope.matches('.portal-category-svg')) output.push(scope);
        ).length;
        Array.prototype.forEach.call(
             scope.querySelectorAll('.main-portal [data-component="category-nav"] .portal-category-svg'),
            function (svg) { output.push(svg); }
        );
        return output;
     }
     }


     function renderAll(root) {
     window.CategoryPillar = {
        var rendered = 0;
         version:'20260717-canvas-integrated-001',
        collectSvgs(root).forEach(function (svg) {
         init:renderAll,
            if (renderSvg(svg)) rendered += 1;
         renderAll:renderAll,
        });
        render:function () { return renderAll(document) > 0; }
        return rendered;
     };
    }
 
    function scheduleRender(root) {
        window.clearTimeout(renderTimer);
        renderTimer = window.setTimeout(function () {
            renderTimer = 0;
            renderAll(root || document);
        }, 0);
    }
 
    function observe() {
         if (observer || !window.MutationObserver || !document.documentElement) return;
 
        observer = new window.MutationObserver(function (records) {
            var relevant = records.some(function (record) {
                return Array.prototype.some.call(record.addedNodes || [], function (node) {
                    return node && node.nodeType === 1 && (
                        (node.matches && node.matches('.portal-category-svg')) ||
                        (node.querySelector && node.querySelector('.portal-category-svg'))
                    );
                });
            });
 
            if (relevant) scheduleRender(document);
         });
 
         observer.observe(document.documentElement, { childList:true, subtree:true });
    }
 
    function init(root) {
        renderAll(root || document);
        observe();
     }


     if (mw && mw.hook) {
     if (mw && mw.hook) {
         mw.hook('wikipage.content').add(function (content) {
         mw.hook('wikipage.content').add(function (content) {
             scheduleRender(content && content[0] ? content[0] : document);
             renderAll(content && content[0] ? content[0] : document);
         });
         });
     }
     }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { init(document); });
    } else {
        init(document);
    }
    window.addEventListener('resize', function () {
        window.clearTimeout(resizeTimer);
        resizeTimer = window.setTimeout(function () {
            resizeTimer = 0;
            renderAll(document);
        }, 80);
    });
    window.CategoryPillar = {
        version:VERSION,
        init:init,
        render:renderSvg,
        renderAll:renderAll
    };
}(window, document, window.mw));
}(window, document, window.mw));

2026년 7월 17일 (금) 17:49 기준 최신판

/* =========================================
CategoryPillar
canvas CategoryNav 내부 프로젝트 기둥 호환 모듈
========================================= */

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

    function renderAll(root) {
        var scope = root && root.querySelectorAll ? root : document;
        return scope.querySelectorAll(
            '.main-portal [data-component="category-nav"] .portal-category-canvas'
        ).length;
    }

    window.CategoryPillar = {
        version:'20260717-canvas-integrated-001',
        init:renderAll,
        renderAll:renderAll,
        render:function () { return renderAll(document) > 0; }
    };

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function (content) {
            renderAll(content && content[0] ? content[0] : document);
        });
    }
}(window, document, window.mw));