(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- | 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, | ||
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, | ||
d.WELL_TOP, | |||
d. | d.WELL2_BOTTOM, | ||
geometry.leftDir, | geometry.leftDir, | ||
geometry.rightDir, | geometry.rightDir, | ||
geometry.phase, | geometry.phase, | ||
d.WELL_TOP, | |||
d.WELL_TOP | |||
); | ); | ||
var joinCover = frame.segmentAt( | var joinCover = frame.segmentAt( | ||
| 109번째 줄: | 109번째 줄: | ||
0 | 0 | ||
); | ); | ||
var | var frameTopRow = frame.rowAt( | ||
geometry.leftSeamX, | geometry.leftSeamX, | ||
geometry.rightSeamX, | geometry.rightSeamX, | ||
d.OVERLAP, | d.OVERLAP, | ||
geometry.leftDir, | geometry.leftDir, | ||
geometry.rightDir, | geometry.rightDir, | ||
geometry.phase, | geometry.phase, | ||
f.BORDER | f.BORDER, | ||
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 | |||
); | ); | ||
| 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', | group.appendChild(frame.line('portal-section-frame-shadow', frameBottomRow[0], frameTopRow[0])); | ||
group.appendChild(frame.line('portal-section-frame-highlight', | group.appendChild(frame.line('portal-section-frame-highlight', frameTopRow[1], frameBottomRow[1])); | ||
group.appendChild(frame.line('portal-section-well-shadow', | group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[0], wellTopRow[1])); | ||
group.appendChild(frame.line('portal-section-well-shadow', | group.appendChild(frame.line('portal-section-well-shadow', wellTopRow[1], wellBottomRow[1])); | ||
group.appendChild(frame.line('portal-section-well-highlight', | group.appendChild(frame.line('portal-section-well-highlight', wellBottomRow[0], wellTopRow[0])); | ||
group.appendChild(frame.line('portal-section-well-highlight', | 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 + | var leftBase = geometry.leftSeamX + d.WELL_BOTTOM; | ||
var rightBase = geometry.rightSeamX - | 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. | d.BUTTON_EDGE_TOP, | ||
d. | d.BUTTON_EDGE_BOTTOM, | ||
button.leftDir, | button.leftDir, | ||
button.rightDir, | button.rightDir, | ||
| 181번째 줄: | 210번째 줄: | ||
button.leftTopX, | button.leftTopX, | ||
button.rightTopX, | button.rightTopX, | ||
d. | d.BUTTON_SURFACE_TOP, | ||
d. | d.BUTTON_SURFACE_BOTTOM, | ||
button.leftDir, | button.leftDir, | ||
button.rightDir, | button.rightDir, | ||
| 189번째 줄: | 218번째 줄: | ||
f.BORDER | f.BORDER | ||
); | ); | ||
var | var topInset = frame.rowAt( | ||
button.leftTopX, | button.leftTopX, | ||
button.rightTopX, | button.rightTopX, | ||
d. | d.BUTTON_SURFACE_TOP, | ||
button.leftDir, | button.leftDir, | ||
button.rightDir, | button.rightDir, | ||
button.phase, | button.phase, | ||
f.BORDER | f.BORDER, | ||
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 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', | 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', | 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', | 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', | 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. | 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));