|
|
| (같은 사용자의 중간 판 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)); |