|
|
| (같은 사용자의 중간 판 2개는 보이지 않습니다) |
| 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-shifted-insets-022';
| |
| 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 lineXAtY(topX, bottomX, y, viewBox) {
| |
| return topX + (bottomX - topX) * ((y - viewBox.y) / viewBox.height);
| |
| }
| |
| | |
| function pointString(points) {
| |
| return points.map(function (point) {
| |
| return point[0] + ',' + point[1];
| |
| }).join(' ');
| |
| }
| |
| | |
| 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 leftTop;
| |
| var leftBottom;
| |
| var rightTop;
| |
| var rightBottom;
| |
| var pillarLeftTop;
| |
| var pillarLeftBottom;
| |
| var pillarRightTop;
| |
| var pillarRightBottom;
| |
| var top;
| |
| var bottom;
| |
| var group;
| |
| var surface;
| |
| var leftInset;
| |
| var rightInset;
| |
| var label;
| |
| | |
| if (!source || points.length < 4 || !viewBox) return false;
| |
| if (previous && previous.parentNode) previous.parentNode.removeChild(previous);
| |
| | |
| extension = getVerticalExtension(svg, viewBox);
| |
| top = viewBox.y - extension.top;
| |
| bottom = viewBox.y + viewBox.height + extension.bottom;
| |
| | |
| /*
| |
| CategoryNav의 점 순서는 좌상→우상→우하→좌하다.
| |
| 원래 project 버튼의 좌우 사선을 그대로 사용하면 양옆 버튼이 이미 확보한
| |
| 2px 간격도 전 높이에서 그대로 유지된다. 기둥 구간은 같은 기울기로
| |
| 위·아래 프레임까지 직선 연장한다.
| |
| */
| |
| leftTop = points[0][0];
| |
| rightTop = points[1][0];
| |
| rightBottom = points[2][0];
| |
| leftBottom = points[3][0];
| |
| pillarLeftTop = lineXAtY(leftTop, leftBottom, top, viewBox);
| |
| pillarLeftBottom = lineXAtY(leftTop, leftBottom, bottom, viewBox);
| |
| pillarRightTop = lineXAtY(rightTop, rightBottom, top, viewBox);
| |
| pillarRightBottom = lineXAtY(rightTop, rightBottom, bottom, viewBox);
| |
| | |
| if (
| |
| !(pillarRightTop > pillarLeftTop) ||
| |
| !(pillarRightBottom > pillarLeftBottom)
| |
| ) return false;
| |
| | |
| 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('polygon', 'portal-category-pillar-surface');
| |
| surface.setAttribute('points', pointString([
| |
| [pillarLeftTop, top],
| |
| [pillarRightTop, top],
| |
| [pillarRightBottom, bottom],
| |
| [pillarLeftBottom, bottom]
| |
| ]));
| |
| group.appendChild(surface);
| |
| | |
| /*
| |
| 상하선은 만들지 않는다. 그래야 같은 #1d1d1d인 외곽 프레임과 기둥이
| |
| 끊기지 않는다. 버튼과 접하는 좌우에도 #050505 외곽선을 두지 않고,
| |
| 기존 #050505 좌표로 방향성 inset을 한 칸 바깥 이동한다.
| |
| inset이 있던 안쪽 한 칸은 별도 선을 두지 않아 #1d1d1d 본체로 돌아간다.
| |
| */
| |
| leftInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-shadow');
| |
| setLine(leftInset, pillarLeftTop + 0.5, top, pillarLeftBottom + 0.5, bottom);
| |
| group.appendChild(leftInset);
| |
| | |
| rightInset = createSvgElement('line', 'portal-category-pillar-line portal-category-pillar-highlight');
| |
| setLine(rightInset, pillarRightTop - 0.5, top, pillarRightBottom - 0.5, bottom);
| |
| group.appendChild(rightInset);
| |
| | |
| label = createSvgElement('text', 'portal-cat-label portal-category-pillar-label');
| |
| label.setAttribute('x', String((leftTop + rightTop + rightBottom + leftBottom) / 4));
| |
| 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)); |