init
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=1920, initial-scale=1" />
|
||||
<title>DNF UI Component Library — Abstracted Preview</title>
|
||||
<link rel="icon" href="src-web/shared/img/sprite_interface.NPK/windowcommon.chn.img/301.PNG" />
|
||||
<link rel="stylesheet" href="src-web/index.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section class="board" id="board-1">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">Foundations & Construction Anatomy</h1>
|
||||
<div class="board-subtitle">截图定义视觉语法,素材定义构造方式;组件不包含任何游戏截图裁片</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>01 / 06</strong>FOUNDATIONS · TOKENS · ASSETS</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="component-reference" data-component-src="src-web/foundations/index.html#colors"></div>
|
||||
|
||||
<div class="grid cols-3" style="height:350px;margin-bottom:18px;">
|
||||
<div class="component-reference" data-component-src="src-web/button/index.html#construction"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/dialog/index.html#construction"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/shared/index.html#glyphs"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-2" style="height:276px;">
|
||||
<div class="component-reference" data-component-src="src-web/foundations/index.html#typography"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/foundations/index.html#architecture"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · TOKEN-FIRST</span><span>SEMANTIC STATES ≠ ITEM QUALITY</span></div>
|
||||
</section>
|
||||
|
||||
<section class="board" id="board-2">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">Core Controls</h1>
|
||||
<div class="board-subtitle">可替换文案、图标和状态;保持深蓝动作面、黄铜边线与紧凑密度</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>02 / 06</strong>BUTTON · FIELD · SELECTION · RATE</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="component-reference" data-component-src="src-web/button/index.html#showcase"></div>
|
||||
|
||||
<div class="grid cols-2" style="height:430px;margin-bottom:18px;">
|
||||
<div class="component-reference" data-component-src="src-web/input/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/select/index.html#showcase"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-2" style="height:292px;">
|
||||
<div class="component-reference" data-component-src="src-web/form-controls/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/slider/index.html#showcase"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · NORMALIZED COMPONENTS</span><span>TREE SELECTED = OUTLINE ONLY · QUALITY TONE PRESERVED</span></div>
|
||||
</section>
|
||||
|
||||
<section class="board" id="board-3">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">Navigation & Overlays</h1>
|
||||
<div class="board-subtitle">相同行为共享 primitive,不同 anatomy 保持独立 visual family</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>03 / 06</strong>TABS · DISCLOSURE · MENU · WINDOW</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="grid cols-2" style="height:275px;margin-bottom:18px;">
|
||||
<div class="component-reference" data-component-src="src-web/tabs/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/menu/index.html#showcase"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-3" style="height:265px;margin-bottom:18px;">
|
||||
<div class="component-reference" data-component-src="src-web/navigation/index.html#favorite-drawer"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/navigation/index.html#settings-nav"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/data-section/index.html#showcase"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-2" style="height:442px;">
|
||||
<div class="component-reference" data-component-src="src-web/dialog/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/scrollbar/index.html#showcase"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · VISUAL FAMILY BOUNDARIES</span><span>DIALOG BODY SHELL ≠ CONTENT WELL</span></div>
|
||||
</section>
|
||||
|
||||
<section class="board" id="board-4">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">Data Display & Product Patterns</h1>
|
||||
<div class="board-subtitle">由小组件组成设置表单、数据表、稀有度槽位和状态反馈,不把整张界面做成组件</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>04 / 06</strong>FORM ROW · TABLE · BADGE · QUALITY</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="grid cols-2" style="height:560px;margin-bottom:18px;grid-template-columns:.92fr 1.28fr;">
|
||||
<div class="component-reference" data-component-src="src-web/settings/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/data-table/index.html#composition"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid cols-3" style="height:390px;grid-template-columns:1.25fr .8fr 1fr;">
|
||||
<div class="component-reference" data-component-src="src-web/rarity/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/rarity/index.html#badges"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/foundations/index.html#evidence"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · PRODUCT PATTERNS</span><span>SMALL COMPONENTS → COMPOSED DNF INTERFACES</span></div>
|
||||
</section>
|
||||
|
||||
<section class="board" id="board-5">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">Search & Complex Dialog Composition</h1>
|
||||
<div class="board-subtitle">搜索组件保持原生输入语法;复杂弹窗由 WindowDialog、List、Table 与 Plain Scrollbar 组合</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>05 / 06</strong>AUTOCOMPLETE · SEARCH TREE · DIALOG PRESET</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="component-reference" data-component-src="src-web/autocomplete/index.html#showcase"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/complex-dialog/index.html#preset"></div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · COMPOSITION TEMPLATE</span><span>WINDOW PRIMITIVE + LIST + TABLE + PLAIN SCROLLBAR</span></div>
|
||||
</section>
|
||||
|
||||
<section class="board" id="board-6">
|
||||
<header class="board-header">
|
||||
<div>
|
||||
<div class="board-kicker">DNF UI Component Library / Abstracted Preview</div>
|
||||
<h1 class="board-title">DataTable Density & Item Variants</h1>
|
||||
<div class="board-subtitle">同一表格语义,拆分技能明细、紧凑记录、DataTable ItemRow 与无表头 ShopItemRow</div>
|
||||
</div>
|
||||
<div class="board-index"><strong>06 / 06</strong>SORT · COMPACT · ITEM ROW · SHOP ITEM</div>
|
||||
</header>
|
||||
|
||||
<div class="content table-variant-board">
|
||||
<div class="component-reference" data-component-src="src-web/data-table/index.html#skill"></div>
|
||||
|
||||
<div class="grid cols-3 table-variant-grid">
|
||||
<div class="component-reference" data-component-src="src-web/data-table/index.html#compact"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/data-table/index.html#item"></div>
|
||||
|
||||
<div class="component-reference" data-component-src="src-web/shop-item-row/index.html#showcase"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-note"><span>PREVIEW V4 · DATATABLE VARIANT FAMILIES</span><span>SORTABLE HEADER + SELECTABLE ROW + SUMMARY + GROUP NOTE</span></div>
|
||||
</section>
|
||||
|
||||
<script src="src-web/shared/component-loader.js"></script>
|
||||
<script>
|
||||
window.componentsReady.then(() => {
|
||||
document.querySelectorAll('.panel').forEach((panel) => {
|
||||
const windows = [...panel.querySelectorAll('.window')];
|
||||
if (!windows.length) return;
|
||||
|
||||
const focusWindow = (activeWindow) => {
|
||||
windows.forEach((windowNode) => {
|
||||
const isActive = windowNode === activeWindow;
|
||||
windowNode.classList.toggle('is-focused', isActive);
|
||||
windowNode.classList.toggle('is-unfocused', !isActive);
|
||||
});
|
||||
};
|
||||
|
||||
windows.forEach((windowNode) => {
|
||||
const titlebar = windowNode.querySelector(':scope > .window-titlebar');
|
||||
if (titlebar && !titlebar.querySelector('.window-title-effect')) {
|
||||
const effect = document.createElement('img');
|
||||
effect.className = 'window-title-effect';
|
||||
effect.src = 'src-web/dialog/img/sprite_interface.NPK/windowheadereffect.img/0.PNG';
|
||||
effect.alt = '';
|
||||
effect.setAttribute('aria-hidden', 'true');
|
||||
titlebar.appendChild(effect);
|
||||
}
|
||||
windowNode.addEventListener('pointerdown', () => focusWindow(windowNode));
|
||||
});
|
||||
|
||||
focusWindow(windows[0]);
|
||||
});
|
||||
|
||||
let windowHeaderEffectFrame = 0;
|
||||
window.setInterval(() => {
|
||||
windowHeaderEffectFrame = (windowHeaderEffectFrame + 1) % 45;
|
||||
document.querySelectorAll('.window.is-focused > .window-titlebar .window-title-effect').forEach((effect) => {
|
||||
effect.src = `src-web/dialog/img/sprite_interface.NPK/windowheadereffect.img/${windowHeaderEffectFrame}.PNG`;
|
||||
});
|
||||
}, 66);
|
||||
|
||||
document.querySelectorAll('[data-tabs]').forEach((tabs) => {
|
||||
const marker = tabs.querySelector(':scope > .tab-marker');
|
||||
const equalizeItems = () => {
|
||||
const labels = [...tabs.querySelectorAll('.game-tab .tab-label')];
|
||||
if (!labels.length) return;
|
||||
const widestLabel = Math.max(0, ...labels.map((label) => label.getBoundingClientRect().width));
|
||||
const tabsStyle = getComputedStyle(tabs);
|
||||
const buttonStyle = getComputedStyle(labels[0].closest('.game-tab'));
|
||||
const groupPadding = parseFloat(tabsStyle.paddingLeft) + parseFloat(tabsStyle.paddingRight);
|
||||
const itemPadding = parseFloat(buttonStyle.paddingLeft) + parseFloat(buttonStyle.paddingRight);
|
||||
const contentWidth = Math.ceil(widestLabel + itemPadding);
|
||||
const availableWidth = Math.max(0, tabs.clientWidth - groupPadding);
|
||||
const itemWidth = Math.min(contentWidth, availableWidth / labels.length);
|
||||
tabs.style.setProperty('--tab-item-width', `${itemWidth}px`);
|
||||
};
|
||||
const positionMarker = (tab, animate = true) => {
|
||||
if (!marker || !tab) return;
|
||||
const tabsBox = tabs.getBoundingClientRect();
|
||||
const tabBox = tab.getBoundingClientRect();
|
||||
if (!animate) marker.classList.remove('is-ready');
|
||||
marker.style.left = `${tabBox.left - tabsBox.left + tabBox.width / 2}px`;
|
||||
if (!marker.classList.contains('is-ready')) {
|
||||
requestAnimationFrame(() => marker.classList.add('is-ready'));
|
||||
}
|
||||
};
|
||||
equalizeItems();
|
||||
positionMarker(tabs.querySelector('.game-tab.selected'), false);
|
||||
document.fonts?.ready.then(() => {
|
||||
equalizeItems();
|
||||
positionMarker(tabs.querySelector('.game-tab.selected'), false);
|
||||
});
|
||||
tabs.querySelectorAll('.game-tab').forEach((tab) => {
|
||||
tab.addEventListener('click', () => {
|
||||
tabs.querySelectorAll('.game-tab').forEach((item) => {
|
||||
item.classList.remove('selected');
|
||||
});
|
||||
tab.classList.add('selected');
|
||||
positionMarker(tab);
|
||||
});
|
||||
});
|
||||
if ('ResizeObserver' in window) {
|
||||
new ResizeObserver(() => {
|
||||
equalizeItems();
|
||||
positionMarker(tabs.querySelector('.game-tab.selected'), false);
|
||||
}).observe(tabs);
|
||||
}
|
||||
});
|
||||
|
||||
const onKeyboardActivate = (element, action) => {
|
||||
element?.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
action(event);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-select-demo]').forEach((select) => {
|
||||
const trigger = select.querySelector('[role="combobox"]');
|
||||
const value = select.querySelector('[data-select-value]');
|
||||
const setOpen = (open) => {
|
||||
select.classList.toggle('is-open', open);
|
||||
trigger?.setAttribute('aria-expanded', String(open));
|
||||
};
|
||||
const toggle = () => setOpen(!select.classList.contains('is-open'));
|
||||
trigger?.addEventListener('click', toggle);
|
||||
onKeyboardActivate(trigger, toggle);
|
||||
select.querySelectorAll('[role="option"]').forEach((option) => {
|
||||
const choose = () => {
|
||||
select.querySelectorAll('[role="option"]').forEach((item) => {
|
||||
item.classList.toggle('selected', item === option);
|
||||
item.classList.remove('focused');
|
||||
item.setAttribute('aria-selected', String(item === option));
|
||||
});
|
||||
if (value) value.textContent = option.textContent;
|
||||
setOpen(false);
|
||||
trigger?.focus();
|
||||
};
|
||||
option.addEventListener('click', choose);
|
||||
onKeyboardActivate(option, choose);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-tree-select]').forEach((treeSelect) => {
|
||||
const trigger = treeSelect.querySelector('[role="combobox"]');
|
||||
const value = treeSelect.querySelector('[data-tree-select-value]');
|
||||
const parent = treeSelect.querySelector('.tree-parent');
|
||||
const setOpen = (open) => {
|
||||
treeSelect.classList.toggle('is-open', open);
|
||||
trigger?.setAttribute('aria-expanded', String(open));
|
||||
};
|
||||
const toggle = () => setOpen(!treeSelect.classList.contains('is-open'));
|
||||
trigger?.addEventListener('click', toggle);
|
||||
onKeyboardActivate(trigger, toggle);
|
||||
const toggleBranch = () => {
|
||||
const collapsed = !treeSelect.classList.contains('is-tree-collapsed');
|
||||
treeSelect.classList.toggle('is-tree-collapsed', collapsed);
|
||||
parent?.setAttribute('aria-expanded', String(!collapsed));
|
||||
};
|
||||
parent?.addEventListener('click', toggleBranch);
|
||||
onKeyboardActivate(parent, toggleBranch);
|
||||
treeSelect.querySelectorAll('.tree-select-item:not(.tree-parent)').forEach((item) => {
|
||||
const choose = () => {
|
||||
treeSelect.querySelectorAll('.tree-select-item:not(.tree-parent)').forEach((option) => {
|
||||
option.classList.toggle('is-selected', option === item);
|
||||
option.setAttribute('aria-selected', String(option === item));
|
||||
});
|
||||
if (value) value.textContent = item.textContent;
|
||||
setOpen(false);
|
||||
trigger?.focus();
|
||||
};
|
||||
item.addEventListener('click', choose);
|
||||
onKeyboardActivate(item, choose);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-checkbox]').forEach((checkbox) => {
|
||||
const toggle = () => {
|
||||
const checked = checkbox.getAttribute('aria-checked') !== 'true';
|
||||
checkbox.setAttribute('aria-checked', String(checked));
|
||||
checkbox.querySelector('.checkbox-control')?.classList.toggle('is-checked', checked);
|
||||
const label = checkbox.querySelector('[data-control-label]');
|
||||
if (label) label.textContent = `交互示例 · ${checked ? '已选择' : '未选择'}`;
|
||||
};
|
||||
checkbox.addEventListener('click', toggle);
|
||||
onKeyboardActivate(checkbox, toggle);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-radio-group]').forEach((group) => {
|
||||
const radios = [...group.querySelectorAll('[data-radio]')];
|
||||
const selectRadio = (radio) => {
|
||||
radios.forEach((item) => {
|
||||
const selected = item === radio;
|
||||
item.setAttribute('aria-checked', String(selected));
|
||||
item.tabIndex = selected ? 0 : -1;
|
||||
});
|
||||
radio.focus();
|
||||
};
|
||||
radios.forEach((radio, index) => {
|
||||
radio.addEventListener('click', () => selectRadio(radio));
|
||||
radio.addEventListener('keydown', (event) => {
|
||||
if (!['ArrowDown', 'ArrowRight', 'ArrowUp', 'ArrowLeft'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const direction = event.key === 'ArrowDown' || event.key === 'ArrowRight' ? 1 : -1;
|
||||
selectRadio(radios[(index + direction + radios.length) % radios.length]);
|
||||
});
|
||||
onKeyboardActivate(radio, () => selectRadio(radio));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-switch]').forEach((switchControl) => {
|
||||
const label = switchControl.querySelector('[data-control-label]');
|
||||
const render = (checked) => {
|
||||
if (label) label.textContent = `Interactive · ${checked ? 'On' : 'Off'}`;
|
||||
};
|
||||
const toggle = () => {
|
||||
const checked = switchControl.getAttribute('aria-checked') !== 'true';
|
||||
switchControl.setAttribute('aria-checked', String(checked));
|
||||
render(checked);
|
||||
};
|
||||
switchControl.addEventListener('click', toggle);
|
||||
onKeyboardActivate(switchControl, toggle);
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-rate]').forEach((rate) => {
|
||||
const stars = [...rate.querySelectorAll('[data-rate-item]')];
|
||||
const render = (value) => stars.forEach((star, index) => {
|
||||
star.src = `src-web/form-controls/img/sprite_interface2_popup_menu.NPK/popup_icon_npc.img/${index < value ? 126 : 149}.PNG`;
|
||||
});
|
||||
const setValue = (value) => {
|
||||
const next = Math.max(0, Math.min(5, value));
|
||||
rate.setAttribute('aria-valuenow', String(next));
|
||||
render(next);
|
||||
};
|
||||
stars.forEach((star, index) => {
|
||||
star.addEventListener('pointerenter', () => render(index + 1));
|
||||
star.addEventListener('click', () => setValue(index + 1));
|
||||
});
|
||||
rate.addEventListener('pointerleave', () => render(Number(rate.getAttribute('aria-valuenow'))));
|
||||
rate.addEventListener('keydown', (event) => {
|
||||
const current = Number(rate.getAttribute('aria-valuenow'));
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowUp') setValue(current + 1);
|
||||
else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') setValue(current - 1);
|
||||
else if (event.key === 'Home') setValue(0);
|
||||
else if (event.key === 'End') setValue(5);
|
||||
else return;
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-slider]').forEach((slider) => {
|
||||
const track = slider.querySelector('.slider-track');
|
||||
const valueLabel = slider.querySelector('.slider-value');
|
||||
const setValue = (value) => {
|
||||
const next = Math.max(0, Math.min(100, Math.round(value)));
|
||||
slider.style.setProperty('--value', `${next}%`);
|
||||
slider.setAttribute('aria-valuenow', String(next));
|
||||
if (valueLabel) valueLabel.textContent = `${next}%`;
|
||||
};
|
||||
const setFromPointer = (event) => {
|
||||
const rect = track.getBoundingClientRect();
|
||||
setValue(((event.clientX - rect.left) / rect.width) * 100);
|
||||
};
|
||||
slider.addEventListener('pointerdown', (event) => {
|
||||
slider.classList.add('is-dragging');
|
||||
slider.setPointerCapture(event.pointerId);
|
||||
setFromPointer(event);
|
||||
});
|
||||
slider.addEventListener('pointermove', (event) => {
|
||||
if (slider.classList.contains('is-dragging')) setFromPointer(event);
|
||||
});
|
||||
const stopDragging = () => slider.classList.remove('is-dragging');
|
||||
slider.addEventListener('pointerup', stopDragging);
|
||||
slider.addEventListener('pointercancel', stopDragging);
|
||||
slider.addEventListener('keydown', (event) => {
|
||||
const current = Number(slider.getAttribute('aria-valuenow'));
|
||||
if (event.key === 'ArrowRight' || event.key === 'ArrowUp') setValue(current + 1);
|
||||
else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') setValue(current - 1);
|
||||
else if (event.key === 'Home') setValue(0);
|
||||
else if (event.key === 'End') setValue(100);
|
||||
else return;
|
||||
event.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-autocomplete]').forEach((autocomplete) => {
|
||||
const trigger = autocomplete.querySelector('[role="combobox"]');
|
||||
const menu = autocomplete.querySelector('[role="listbox"]');
|
||||
const value = autocomplete.querySelector('[data-autocomplete-value]');
|
||||
const setOpen = (open) => {
|
||||
if (menu) menu.hidden = !open;
|
||||
trigger?.setAttribute('aria-expanded', String(open));
|
||||
};
|
||||
const toggle = () => setOpen(trigger?.getAttribute('aria-expanded') !== 'true');
|
||||
trigger?.addEventListener('click', toggle);
|
||||
onKeyboardActivate(trigger, toggle);
|
||||
autocomplete.querySelectorAll('[role="option"]').forEach((option) => {
|
||||
const choose = () => {
|
||||
autocomplete.querySelectorAll('[role="option"]').forEach((item) => {
|
||||
item.classList.toggle('selected', item === option);
|
||||
item.setAttribute('aria-selected', String(item === option));
|
||||
});
|
||||
if (value) value.textContent = option.textContent;
|
||||
setOpen(false);
|
||||
trigger?.focus();
|
||||
};
|
||||
option.addEventListener('click', choose);
|
||||
onKeyboardActivate(option, choose);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-cascader]').forEach((cascader) => {
|
||||
const trigger = cascader.querySelector('.cascader-trigger.primary');
|
||||
const value = cascader.querySelector('.cascader-value');
|
||||
trigger?.addEventListener('click', () => cascader.classList.toggle('is-open'));
|
||||
cascader.querySelectorAll('.cascader-option').forEach((option) => {
|
||||
option.addEventListener('click', () => {
|
||||
cascader.querySelectorAll('.cascader-option').forEach((item) => item.classList.remove('focused'));
|
||||
option.classList.add('focused');
|
||||
value.textContent = option.textContent;
|
||||
cascader.classList.remove('is-open');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const setDisclosureState = (section, expanded) => {
|
||||
const trigger = section.querySelector('.disclosure-head');
|
||||
const body = section.querySelector('.disclosure-body');
|
||||
const icon = trigger?.querySelector('img');
|
||||
const state = trigger?.querySelector('.disclosure-state');
|
||||
trigger?.setAttribute('aria-expanded', String(expanded));
|
||||
section.classList.toggle('is-collapsed', !expanded);
|
||||
if (body) body.hidden = !expanded;
|
||||
if (icon) icon.src = expanded ? icon.dataset.expandedIcon : icon.dataset.collapsedIcon;
|
||||
if (state) state.textContent = expanded ? '已展开' : '未展开';
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-disclosure]').forEach((section) => {
|
||||
const trigger = section.querySelector('.disclosure-head');
|
||||
trigger?.addEventListener('click', () => {
|
||||
const expanded = trigger.getAttribute('aria-expanded') !== 'true';
|
||||
if (expanded) {
|
||||
section.closest('.data-section-stack')?.querySelectorAll('[data-disclosure]').forEach((sibling) => {
|
||||
if (sibling !== section) setDisclosureState(sibling, false);
|
||||
});
|
||||
}
|
||||
setDisclosureState(section, expanded);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-favorite-drawer]').forEach((drawer) => {
|
||||
const trigger = drawer.querySelector('.disclosure-head');
|
||||
const body = drawer.querySelector('.disclosure-body');
|
||||
const state = drawer.querySelector('.favorite-drawer-state');
|
||||
const arrow = drawer.querySelector('.favorite-drawer-arrow');
|
||||
trigger?.addEventListener('click', () => {
|
||||
const expanded = trigger.getAttribute('aria-expanded') !== 'true';
|
||||
trigger.setAttribute('aria-expanded', String(expanded));
|
||||
drawer.classList.toggle('is-collapsed', !expanded);
|
||||
if (body) body.hidden = !expanded;
|
||||
if (state) state.textContent = expanded ? '已展开' : '已收起';
|
||||
if (arrow) arrow.textContent = expanded ? '▲' : '▼';
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-number-input]').forEach((numberInput) => {
|
||||
const value = numberInput.querySelector('[data-number-value]');
|
||||
numberInput.querySelectorAll('[data-step]').forEach((control) => {
|
||||
control.addEventListener('click', () => {
|
||||
const next = Number(value?.textContent || 0) + Number(control.dataset.step || 0);
|
||||
if (value) value.textContent = String(Math.max(0, next));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-sortable-table]').forEach((table) => {
|
||||
table.querySelectorAll('.sortable-header').forEach((header) => {
|
||||
header.addEventListener('click', () => {
|
||||
const wasSorted = header.classList.contains('is-sorted');
|
||||
const wasDescending = header.getAttribute('aria-sort') === 'descending';
|
||||
table.querySelectorAll('.sortable-header').forEach((item) => {
|
||||
item.classList.remove('is-sorted');
|
||||
item.removeAttribute('aria-sort');
|
||||
const glyph = item.querySelector('.sort-glyph');
|
||||
if (glyph) glyph.textContent = '△';
|
||||
});
|
||||
const descending = wasSorted && !wasDescending;
|
||||
header.classList.add('is-sorted');
|
||||
header.setAttribute('aria-sort', descending ? 'descending' : 'ascending');
|
||||
const glyph = header.querySelector('.sort-glyph');
|
||||
if (glyph) glyph.textContent = descending ? '▽' : '△';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const selectTableRow = (row) => {
|
||||
row.closest('.data-table')?.querySelectorAll('[data-selectable-row]').forEach((item) => item.classList.remove('is-selected'));
|
||||
row.classList.add('is-selected');
|
||||
};
|
||||
document.querySelectorAll('[data-selectable-row]').forEach((row) => {
|
||||
row.addEventListener('click', () => selectTableRow(row));
|
||||
row.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
selectTableRow(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-selection-theme]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const panel = button.closest('.panel-body');
|
||||
const table = panel?.querySelector('[data-data-table]');
|
||||
panel?.querySelectorAll('[data-selection-theme]').forEach((item) => item.classList.toggle('is-active', item === button));
|
||||
table?.classList.toggle('selection-gold', button.dataset.selectionTheme === 'gold');
|
||||
table?.classList.toggle('selection-blue', button.dataset.selectionTheme === 'blue');
|
||||
});
|
||||
});
|
||||
|
||||
const updateDialogStatus = (control, message) => {
|
||||
const dialog = control.closest('.window');
|
||||
const status = dialog?.querySelector('.dialog-status');
|
||||
if (!status) return;
|
||||
status.textContent = message;
|
||||
status.classList.remove('is-updated');
|
||||
requestAnimationFrame(() => status.classList.add('is-updated'));
|
||||
};
|
||||
|
||||
document.querySelectorAll('[data-dialog-action]').forEach((control) => {
|
||||
control.addEventListener('click', () => {
|
||||
const message = control.dataset.dialogAction || '操作已执行';
|
||||
updateDialogStatus(control, message);
|
||||
if (message.includes('关闭')) {
|
||||
const dialog = control.closest('.window');
|
||||
dialog?.classList.remove('is-demo-dismissed');
|
||||
requestAnimationFrame(() => dialog?.classList.add('is-demo-dismissed'));
|
||||
window.setTimeout(() => dialog?.classList.remove('is-demo-dismissed'), 300);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-dialog-toggle]').forEach((control) => {
|
||||
control.addEventListener('click', () => {
|
||||
const next = control.getAttribute('aria-pressed') !== 'true';
|
||||
control.setAttribute('aria-pressed', String(next));
|
||||
const checkbox = control.querySelector('.checkbox-control');
|
||||
checkbox?.classList.toggle('is-checked', next);
|
||||
const label = control.classList.contains('sheet-filter-button')
|
||||
? `筛选${next ? '已展开' : '已收起'}`
|
||||
: control.classList.contains('show-all')
|
||||
? `查看全部${next ? '已开启' : '已关闭'}`
|
||||
: `显示模式${next ? '已切换' : '已还原'}`;
|
||||
updateDialogStatus(control, label);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-preset-edit]').forEach((control) => {
|
||||
control.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const row = control.closest('.preset-row');
|
||||
const active = !row?.classList.contains('is-renaming');
|
||||
row?.classList.toggle('is-renaming', active);
|
||||
control.setAttribute('aria-pressed', String(active));
|
||||
updateDialogStatus(control, active ? '预设已进入重命名状态' : '预设名称已确认');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-preset-delete]').forEach((control) => {
|
||||
control.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const row = control.closest('.preset-row');
|
||||
const removed = !row?.classList.contains('is-demo-deleted');
|
||||
row?.classList.toggle('is-demo-deleted', removed);
|
||||
control.setAttribute('aria-pressed', String(removed));
|
||||
updateDialogStatus(control, removed ? '预设已标记移除,再次点击可恢复' : '预设已恢复');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user