Динамический документ
Добавьте раздел, выберите его, удалите последний или очистите документ. После изменений вызывается refresh(). Отключение освобождает экземпляр, повторное включение создаёт новый.
Подготовка примера…
Разделов нет. Нажмите «Добавить раздел».
<div class="spy-demo" id="spy-api-demo">
<div class="spy-demo__controls">
<button class="btn btn-primary btn-sm" type="button" id="spy-api-add">Добавить раздел</button>
<button class="btn btn-surface btn-sm" type="button" id="spy-api-remove">Удалить последний</button>
<button class="btn btn-surface btn-sm" type="button" id="spy-api-clear">Очистить документ</button>
<button class="btn btn-surface btn-sm" type="button" id="spy-api-toggle">Отключить VGSpy</button>
</div>
<p id="spy-api-status" role="status">Подготовка примера…</p>
<nav class="spy-demo__nav" id="spy-api-nav" aria-label="Динамические разделы"></nav>
<div class="spy-demo__scroll" id="spy-api-scroll" tabindex="0" role="region" aria-label="Динамический документ">
<p class="spy-demo__empty" id="spy-api-empty">Разделов нет. Нажмите «Добавить раздел».</p>
</div>
<div>
<strong>Журнал activate.vg.spy</strong>
<ol class="spy-demo__log" id="spy-api-log" aria-live="polite"></ol>
</div>
</div>
/**
* Описание: жизненный цикл VGSpy на динамическом документе.
* Возможности: добавление и удаление секций, пустое состояние, refresh, dispose и события.
*/
import { VGSpy } from 'vgapp';
function initSpyDemo() {
const demo = document.getElementById('spy-api-demo');
if (!demo || demo.dataset.initialized) return;
demo.dataset.initialized = 'true';
const nav = document.getElementById('spy-api-nav');
const root = document.getElementById('spy-api-scroll');
const empty = document.getElementById('spy-api-empty');
const status = document.getElementById('spy-api-status');
const log = document.getElementById('spy-api-log');
const add = document.getElementById('spy-api-add');
const remove = document.getElementById('spy-api-remove');
const clear = document.getElementById('spy-api-clear');
const toggle = document.getElementById('spy-api-toggle');
let instance = null;
let sequence = 0;
const update = () => {
const count = nav.children.length;
empty.hidden = count > 0;
remove.disabled = clear.disabled = count === 0;
add.disabled = count >= 6;
toggle.textContent = instance ? 'Отключить VGSpy' : 'Включить VGSpy';
status.textContent = (instance ? 'VGSpy включён' : 'VGSpy отключён') + ' · Разделов: ' + count;
};
const appendSection = () => {
if (nav.children.length >= 6) return;
const number = ++sequence;
const section = document.createElement('section');
section.id = 'spy-api-section-' + number;
section.className = 'spy-demo__section';
const heading = document.createElement('h4');
heading.textContent = 'Раздел ' + number;
const text = document.createElement('p');
text.textContent = 'Этот раздел добавлен в DOM. После вставки refresh() обновляет ссылки и наблюдение.';
section.append(heading, text);
root.append(section);
const link = document.createElement('button');
link.type = 'button';
link.dataset.vgTarget = '#' + section.id;
link.textContent = heading.textContent;
nav.append(link);
update();
instance?.refresh();
};
// Слушатель приложения сохраняется при dispose() и повторном создании VGSpy.
nav.addEventListener('activate.vg.spy', event => {
const item = document.createElement('li');
item.textContent = event.relatedTarget.textContent + ' · ' + event.relatedTarget.dataset.vgTarget;
log.prepend(item);
while (log.children.length > 8) log.lastElementChild.remove();
});
appendSection();
appendSection();
instance = VGSpy.getOrCreateInstance(nav, { target: root });
update();
add.addEventListener('click', appendSection);
remove.addEventListener('click', () => {
const link = nav.lastElementChild;
if (!link) return;
document.querySelector(link.dataset.vgTarget)?.remove();
link.remove();
update();
instance?.refresh();
});
clear.addEventListener('click', () => {
nav.replaceChildren();
root.querySelectorAll('.spy-demo__section').forEach(section => section.remove());
update();
instance?.refresh();
});
toggle.addEventListener('click', () => {
if (instance) {
instance.dispose();
instance = null;
} else {
instance = VGSpy.getOrCreateInstance(nav, { target: root });
}
update();
});
}
initSpyDemo();
/* Композиция демо; семантические токены OKAUX на сайте используют префикс vg. */
.spy-demo {
display: grid;
gap: 1rem;
min-width: 0;
}
.spy-demo__nav, .spy-demo__controls {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
gap: .5rem;
}
.spy-demo__nav a, .spy-demo__nav button {
display: inline-flex;
padding: .5rem .75rem;
border: 1px solid var(--vg-border-default);
border-radius: .5rem;
background: var(--vg-background-primary);
color: var(--vg-text-primary);
text-decoration: none;
font: inherit;
}
.spy-demo__nav .active {
background: var(--vg-background-tertiary);
border-color: var(--vg-border-active);
box-shadow: inset 0 -3px var(--vg-border-active);
font-weight: 700;
}
.spy-demo__nav :is(a, button):focus-visible {
outline: 2px solid var(--vg-border-active);
outline-offset: 2px;
}
.spy-demo__nav .nav {
display: grid;
gap: .25rem;
padding: .5rem 0 0 1rem;
}
.spy-demo__nav .vg-dropdown-container {
display: grid;
gap: .25rem;
padding: .5rem;
}
.spy-demo__scroll {
height: 20rem;
overflow-y: auto;
overscroll-behavior: contain;
border: 1px solid var(--vg-border-default);
border-radius: .75rem;
background: var(--vg-background-primary);
color: var(--vg-text-primary);
}
.spy-demo__section {
min-height: 18rem;
padding: 1.25rem;
border-bottom: 1px solid var(--vg-border-default);
overflow-wrap: anywhere;
}
.spy-demo__section:nth-child(even) { background: var(--vg-background-secondary); }
.spy-demo__section h4 { margin: 0 0 .75rem; }
.spy-demo__log {
max-height: 10rem;
overflow: auto;
margin: 0;
padding-left: 1.5rem;
color: var(--vg-text-secondary);
overflow-wrap: anywhere;
}
.spy-demo__empty { padding: 1.25rem; color: var(--vg-text-secondary); }
@media (max-width: 575px) {
.spy-demo__scroll { height: 17rem; }
.spy-demo__section { min-height: 16rem; padding: 1rem; }
}