|
|
|
@ -1283,15 +1283,26 @@ HTML = """<!doctype html>
|
|
|
|
.config-toolbar button, .config-restore-panel button { min-height: 26px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 7px; background: var(--ui-input); color: var(--ui-muted); font: 700 10px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer; transition: border-color .18s ease, color .18s ease, background .18s ease, transform .18s ease; }
|
|
|
|
.config-toolbar button, .config-restore-panel button { min-height: 26px; padding: 0 8px; border: 1px solid var(--ui-border); border-radius: 7px; background: var(--ui-input); color: var(--ui-muted); font: 700 10px/1 ui-sans-serif, system-ui, sans-serif; cursor: pointer; transition: border-color .18s ease, color .18s ease, background .18s ease, transform .18s ease; }
|
|
|
|
.config-toolbar button:hover, .config-toolbar button:focus-visible, .config-restore-panel button:hover, .config-restore-panel button:focus-visible { outline: 0; border-color: var(--ui-accent); color: var(--ui-accent); transform: translateY(-1px); }
|
|
|
|
.config-toolbar button:hover, .config-toolbar button:focus-visible, .config-restore-panel button:hover, .config-restore-panel button:focus-visible { outline: 0; border-color: var(--ui-accent); color: var(--ui-accent); transform: translateY(-1px); }
|
|
|
|
.config-layout-state { min-width: 0; color: var(--ui-muted); font-size: 10px; }
|
|
|
|
.config-layout-state { min-width: 0; color: var(--ui-muted); font-size: 10px; }
|
|
|
|
.config-restore-panel { display: grid; gap: 6px; padding: 7px 10px; border-bottom: 1px solid var(--ui-border); background: color-mix(in srgb, var(--ui-input) 45%, transparent); }
|
|
|
|
.config-restore-panel { display: grid; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--ui-border); background: color-mix(in srgb, var(--ui-input) 45%, transparent); }
|
|
|
|
.config-restore-title { color: var(--ui-muted); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
|
|
|
.config-restore-title { color: var(--ui-muted); font-size: 10px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
|
|
|
.config-restore-list { display: flex; flex-wrap: wrap; gap: 5px; }
|
|
|
|
.config-structure-create { display: flex; align-items: end; flex-wrap: wrap; gap: 6px; }
|
|
|
|
.config-restore-item { display: inline-flex; align-items: center; gap: 6px; padding: 3px 5px 3px 8px; border: 1px solid var(--ui-border); border-radius: 7px; color: var(--ui-text); font-size: 11px; }
|
|
|
|
.config-structure-create label { display: grid; gap: 3px; min-width: 150px; color: var(--ui-muted); font-size: 10px; }
|
|
|
|
|
|
|
|
.config-structure-create label:first-child { min-width: 190px; }
|
|
|
|
|
|
|
|
.config-structure-create input, .config-structure-create select { height: 27px; min-width: 0; padding: 0 7px; border: 1px solid var(--ui-border); border-radius: 7px; background: var(--ui-input); color: var(--ui-text); font: inherit; }
|
|
|
|
|
|
|
|
.config-structure-create input:focus, .config-structure-create select:focus { outline: 0; border-color: var(--ui-accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-accent) 10%, transparent); }
|
|
|
|
|
|
|
|
.config-structure-hint { color: var(--ui-muted); font-size: 10px; }
|
|
|
|
|
|
|
|
.config-restore-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 5px; }
|
|
|
|
|
|
|
|
.config-restore-item { display: flex; align-items: center; gap: 6px; min-width: 0; padding: 4px 5px 4px 8px; border: 1px solid var(--ui-border); border-radius: 7px; color: var(--ui-text); font-size: 11px; }
|
|
|
|
|
|
|
|
.config-restore-item.is-hidden { opacity: .72; }
|
|
|
|
|
|
|
|
.config-restore-item .config-item-title { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
|
|
|
.config-restore-item .config-item-state { color: var(--ui-muted); font-size: 9px; text-transform: uppercase; }
|
|
|
|
|
|
|
|
.config-item-actions { display: inline-flex; gap: 3px; }
|
|
|
|
.config-restore-panel[hidden], .config-trash[hidden] { display: none !important; }
|
|
|
|
.config-restore-panel[hidden], .config-trash[hidden] { display: none !important; }
|
|
|
|
.config-canvas { position: relative; display: block; min-height: 760px; padding: 8px 6px 18px; }
|
|
|
|
.config-canvas { position: relative; display: block; min-height: 760px; padding: 8px 6px 18px; overflow: auto; overscroll-behavior: contain; }
|
|
|
|
.config-canvas > .config-block { position: absolute; margin: 0; min-width: 240px; min-height: 110px; border: 1px solid var(--ui-border) !important; border-radius: 10px; background: var(--ui-panel) !important; box-shadow: 0 6px 18px rgba(0, 0, 0, .1); overflow: auto; transition: border-color .2s ease, box-shadow .2s ease, opacity .2s ease; }
|
|
|
|
.config-canvas > .config-block { position: absolute; box-sizing: border-box; margin: 0; min-width: 240px; min-height: 110px; border: 1px solid var(--ui-border) !important; border-radius: 10px; background: var(--ui-panel) !important; box-shadow: 0 6px 18px rgba(0, 0, 0, .1); overflow: auto; transition: border-color .2s ease, box-shadow .2s ease, opacity .2s ease; }
|
|
|
|
.config-canvas > .config-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)) !important; }
|
|
|
|
.config-canvas > .config-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)) !important; }
|
|
|
|
.config-canvas > .config-block.is-dragging, .config-canvas > .config-block.is-resizing { z-index: 5; border-color: var(--ui-accent) !important; box-shadow: 0 10px 28px color-mix(in srgb, var(--ui-accent) 16%, transparent); transition: none; }
|
|
|
|
.config-canvas > .config-block.is-dragging, .config-canvas > .config-block.is-resizing { z-index: 5; border-color: var(--ui-accent) !important; box-shadow: 0 10px 28px color-mix(in srgb, var(--ui-accent) 16%, transparent); transition: none; }
|
|
|
|
|
|
|
|
.config-canvas > .config-custom-block { border-style: dashed !important; }
|
|
|
|
.config-block > summary { position: sticky; top: 0; z-index: 2; background: var(--ui-panel); }
|
|
|
|
.config-block > summary { position: sticky; top: 0; z-index: 2; background: var(--ui-panel); }
|
|
|
|
.block-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.block-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
.config-block > summary { cursor: grab; touch-action: none; }
|
|
|
|
.config-block > summary { cursor: grab; touch-action: none; }
|
|
|
|
@ -1305,8 +1316,10 @@ HTML = """<!doctype html>
|
|
|
|
@media (max-width: 760px) {
|
|
|
|
@media (max-width: 760px) {
|
|
|
|
.config-toolbar { align-items: stretch; }
|
|
|
|
.config-toolbar { align-items: stretch; }
|
|
|
|
.config-toolbar-title, .config-layout-state { flex-basis: 100%; }
|
|
|
|
.config-toolbar-title, .config-layout-state { flex-basis: 100%; }
|
|
|
|
.config-canvas { min-height: 0 !important; display: grid; gap: 8px; padding: 8px 0; }
|
|
|
|
.config-canvas { min-height: 900px; padding: 8px 4px 24px; }
|
|
|
|
.config-canvas > .config-block { position: relative !important; inset: auto !important; width: auto !important; height: auto !important; min-width: 0; max-height: none; }
|
|
|
|
.config-canvas > .config-block { min-width: 220px; max-width: calc(100% - 8px); }
|
|
|
|
|
|
|
|
.config-structure-create { align-items: stretch; }
|
|
|
|
|
|
|
|
.config-structure-create label, .config-structure-create label:first-child { min-width: min(100%, 220px); flex: 1 1 180px; }
|
|
|
|
.config-trash { bottom: 10px; }
|
|
|
|
.config-trash { bottom: 10px; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@keyframes form-field-in { from { opacity: .35; transform: translateY(3px); } to { opacity: 1; transform: none; } }
|
|
|
|
@keyframes form-field-in { from { opacity: .35; transform: translateY(3px); } to { opacity: 1; transform: none; } }
|
|
|
|
@ -1402,11 +1415,26 @@ HTML = """<!doctype html>
|
|
|
|
<div class="config-toolbar" role="toolbar" aria-label="Управление блоками конфигурации">
|
|
|
|
<div class="config-toolbar" role="toolbar" aria-label="Управление блоками конфигурации">
|
|
|
|
<span class="config-toolbar-title">Блоки конфигурации</span>
|
|
|
|
<span class="config-toolbar-title">Блоки конфигурации</span>
|
|
|
|
<button id="configResetLayout" type="button">сбросить раскладку</button>
|
|
|
|
<button id="configResetLayout" type="button">сбросить раскладку</button>
|
|
|
|
<button id="configRestoreToggle" type="button" aria-expanded="false">скрытые блоки</button>
|
|
|
|
<button id="configRestoreToggle" type="button" aria-expanded="false">структура</button>
|
|
|
|
<span id="configLayoutState" class="config-layout-state">перетаскивайте блоки за ⠿</span>
|
|
|
|
<span id="configLayoutState" class="config-layout-state">перетаскивайте блоки за ⠿</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="configRestorePanel" class="config-restore-panel" hidden>
|
|
|
|
<div id="configRestorePanel" class="config-restore-panel" hidden>
|
|
|
|
<span class="config-restore-title">Скрытые и удалённые блоки</span>
|
|
|
|
<span class="config-restore-title">Структура canvas</span>
|
|
|
|
|
|
|
|
<div class="config-structure-create">
|
|
|
|
|
|
|
|
<label>Шаблон
|
|
|
|
|
|
|
|
<select id="configTemplateSelect">
|
|
|
|
|
|
|
|
<option value="source">Источник</option>
|
|
|
|
|
|
|
|
<option value="processing">Обработка</option>
|
|
|
|
|
|
|
|
<option value="archive">Архив</option>
|
|
|
|
|
|
|
|
<option value="advanced">Выход ошибки и калибровка</option>
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label>Название нового блока
|
|
|
|
|
|
|
|
<input id="configBlockTitle" type="text" maxlength="48" placeholder="Мой блок">
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button id="configCreateBlock" type="button">создать блок</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<span class="config-structure-hint">Перетаскивайте заголовок. Граница меняет размер. Клик поднимает блок. Перетащите в корзину для удаления.</span>
|
|
|
|
<div id="configRestoreList" class="config-restore-list"></div>
|
|
|
|
<div id="configRestoreList" class="config-restore-list"></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<form id="controlForm" class="control-form config-canvas">
|
|
|
|
<form id="controlForm" class="control-form config-canvas">
|
|
|
|
@ -1843,8 +1871,8 @@ HTML = """<!doctype html>
|
|
|
|
let configCanvas = null;
|
|
|
|
let configCanvas = null;
|
|
|
|
let configBlocks = [];
|
|
|
|
let configBlocks = [];
|
|
|
|
let configLayout = {};
|
|
|
|
let configLayout = {};
|
|
|
|
let configManualLayout = false;
|
|
|
|
|
|
|
|
let restorePanelOpen = false;
|
|
|
|
let restorePanelOpen = false;
|
|
|
|
|
|
|
|
let configZIndex = 1;
|
|
|
|
function readConfigLayout() {
|
|
|
|
function readConfigLayout() {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const saved = JSON.parse(localStorage.getItem('fpv-config-layout-v1') || '{}');
|
|
|
|
const saved = JSON.parse(localStorage.getItem('fpv-config-layout-v1') || '{}');
|
|
|
|
@ -1854,19 +1882,28 @@ HTML = """<!doctype html>
|
|
|
|
function saveConfigLayout() {
|
|
|
|
function saveConfigLayout() {
|
|
|
|
try { localStorage.setItem('fpv-config-layout-v1', JSON.stringify(configLayout)); } catch (_error) {}
|
|
|
|
try { localStorage.setItem('fpv-config-layout-v1', JSON.stringify(configLayout)); } catch (_error) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function configMinWidth() {
|
|
|
|
|
|
|
|
return window.innerWidth <= 760 ? 220 : 240;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function configCanvasWidth() {
|
|
|
|
|
|
|
|
return Math.max(configMinWidth(), (configCanvas?.clientWidth || 0) - 12);
|
|
|
|
|
|
|
|
}
|
|
|
|
function configTitle(block) {
|
|
|
|
function configTitle(block) {
|
|
|
|
return block.querySelector(':scope > summary .block-title')?.textContent || block.dataset.section || 'Блок';
|
|
|
|
return block.querySelector(':scope > summary .block-title')?.textContent || block.dataset.section || 'Блок';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function configBlockState(block) {
|
|
|
|
function configBlockState(block) {
|
|
|
|
return block.dataset.blockState || '';
|
|
|
|
return block.dataset.blockState || '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function configTemplateKey(block) {
|
|
|
|
|
|
|
|
return block.dataset.custom === 'true' ? (block.dataset.template || 'source') : block.dataset.section;
|
|
|
|
|
|
|
|
}
|
|
|
|
function updateConfigCanvasHeight() {
|
|
|
|
function updateConfigCanvasHeight() {
|
|
|
|
if (!configCanvas) return;
|
|
|
|
if (!configCanvas) return;
|
|
|
|
const bottom = configBlocks.reduce((value, block) => {
|
|
|
|
const bottom = configBlocks.reduce((value, block) => {
|
|
|
|
if (configBlockState(block)) return value;
|
|
|
|
if (configBlockState(block)) return value;
|
|
|
|
return Math.max(value, (block.offsetTop || 0) + block.offsetHeight);
|
|
|
|
return Math.max(value, (block.offsetTop || 0) + block.offsetHeight);
|
|
|
|
}, 0);
|
|
|
|
}, 0);
|
|
|
|
configCanvas.style.minHeight = `${Math.max(760, bottom + 26)}px`;
|
|
|
|
configCanvas.style.minHeight = `${Math.max(window.innerWidth <= 760 ? 900 : 760, bottom + 28)}px`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function captureConfigLayout(block) {
|
|
|
|
function captureConfigLayout(block) {
|
|
|
|
const key = block.dataset.section;
|
|
|
|
const key = block.dataset.section;
|
|
|
|
@ -1876,45 +1913,67 @@ HTML = """<!doctype html>
|
|
|
|
entry.top = Math.round(parseFloat(block.style.top) || block.offsetTop || 0);
|
|
|
|
entry.top = Math.round(parseFloat(block.style.top) || block.offsetTop || 0);
|
|
|
|
entry.width = Math.round(block.offsetWidth);
|
|
|
|
entry.width = Math.round(block.offsetWidth);
|
|
|
|
if (block.style.height) entry.height = Math.round(block.offsetHeight);
|
|
|
|
if (block.style.height) entry.height = Math.round(block.offsetHeight);
|
|
|
|
|
|
|
|
else delete entry.height;
|
|
|
|
if (block.style.zIndex) entry.z = Number(block.style.zIndex);
|
|
|
|
if (block.style.zIndex) entry.z = Number(block.style.zIndex);
|
|
|
|
configLayout[key] = entry;
|
|
|
|
configLayout[key] = entry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function placeConfigBlock(block, left, top, width) {
|
|
|
|
|
|
|
|
if (!block) return;
|
|
|
|
|
|
|
|
const canvasWidth = configCanvasWidth();
|
|
|
|
|
|
|
|
const minWidth = configMinWidth();
|
|
|
|
|
|
|
|
const safeLeft = Math.max(0, Math.round(Math.min(left, Math.max(0, canvasWidth - minWidth))));
|
|
|
|
|
|
|
|
const safeWidth = Math.max(minWidth, Math.min(canvasWidth - safeLeft, Math.round(width)));
|
|
|
|
|
|
|
|
block.style.left = `${safeLeft}px`;
|
|
|
|
|
|
|
|
block.style.top = `${Math.max(0, Math.round(top))}px`;
|
|
|
|
|
|
|
|
block.style.width = `${safeWidth}px`;
|
|
|
|
|
|
|
|
}
|
|
|
|
function applyStoredConfigLayout() {
|
|
|
|
function applyStoredConfigLayout() {
|
|
|
|
if (!configCanvas) return;
|
|
|
|
if (!configCanvas) return;
|
|
|
|
const width = Math.max(300, configCanvas.clientWidth - 12);
|
|
|
|
const width = configCanvasWidth();
|
|
|
|
const gap = 12;
|
|
|
|
const gap = 14;
|
|
|
|
const source = configBlocks.find(block => block.dataset.section === 'source' && !configBlockState(block));
|
|
|
|
const visible = block => block && !configBlockState(block);
|
|
|
|
const processing = configBlocks.find(block => block.dataset.section === 'processing' && !configBlockState(block));
|
|
|
|
const source = configBlocks.find(block => block.dataset.section === 'source' && visible(block));
|
|
|
|
const archive = configBlocks.find(block => block.dataset.section === 'archive' && !configBlockState(block));
|
|
|
|
const processing = configBlocks.find(block => block.dataset.section === 'processing' && visible(block));
|
|
|
|
const advanced = configBlocks.find(block => block.dataset.section === 'advanced' && !configBlockState(block));
|
|
|
|
const archive = configBlocks.find(block => block.dataset.section === 'archive' && visible(block));
|
|
|
|
const place = (block, left, top, blockWidth) => {
|
|
|
|
const advanced = configBlocks.find(block => block.dataset.section === 'advanced' && visible(block));
|
|
|
|
|
|
|
|
const defaults = new Set();
|
|
|
|
|
|
|
|
const placeDefault = (block, left, top, blockWidth) => {
|
|
|
|
if (!block) return;
|
|
|
|
if (!block) return;
|
|
|
|
block.style.left = `${Math.max(0, Math.round(left))}px`;
|
|
|
|
const entry = configLayout[block.dataset.section] || {};
|
|
|
|
block.style.top = `${Math.max(0, Math.round(top))}px`;
|
|
|
|
if (!Number.isFinite(Number(entry.left)) || !Number.isFinite(Number(entry.top))) placeConfigBlock(block, left, top, blockWidth);
|
|
|
|
block.style.width = `${Math.max(240, Math.round(blockWidth))}px`;
|
|
|
|
defaults.add(block);
|
|
|
|
block.style.height = '';
|
|
|
|
|
|
|
|
};
|
|
|
|
};
|
|
|
|
let y = 0;
|
|
|
|
let y = 8;
|
|
|
|
place(source, 0, y, width);
|
|
|
|
placeDefault(source, 0, y, width);
|
|
|
|
if (source) y += source.offsetHeight + gap;
|
|
|
|
if (source) y += source.offsetHeight + gap;
|
|
|
|
const pair = [processing, archive].filter(Boolean);
|
|
|
|
const pair = [processing, archive].filter(Boolean);
|
|
|
|
if (pair.length === 2) {
|
|
|
|
if (pair.length === 2) {
|
|
|
|
const half = Math.max(240, (width - gap) / 2);
|
|
|
|
const half = Math.max(configMinWidth(), (width - gap) / 2);
|
|
|
|
place(processing, 0, y, half);
|
|
|
|
placeDefault(processing, 0, y, half);
|
|
|
|
place(archive, half + gap, y, half);
|
|
|
|
placeDefault(archive, half + gap, y, half);
|
|
|
|
y += Math.max(processing.offsetHeight, archive.offsetHeight) + gap;
|
|
|
|
y += Math.max(processing.offsetHeight, archive.offsetHeight) + gap;
|
|
|
|
} else if (pair.length === 1) {
|
|
|
|
} else if (pair.length === 1) {
|
|
|
|
place(pair[0], 0, y, width);
|
|
|
|
placeDefault(pair[0], 0, y, width);
|
|
|
|
y += pair[0].offsetHeight + gap;
|
|
|
|
y += pair[0].offsetHeight + gap;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
place(advanced, 0, y, width);
|
|
|
|
placeDefault(advanced, 0, y, width);
|
|
|
|
|
|
|
|
if (advanced) y += advanced.offsetHeight + gap;
|
|
|
|
|
|
|
|
configBlocks.filter(block => visible(block) && !defaults.has(block)).forEach(block => {
|
|
|
|
|
|
|
|
const entry = configLayout[block.dataset.section] || {};
|
|
|
|
|
|
|
|
if (!Number.isFinite(Number(entry.left)) || !Number.isFinite(Number(entry.top))) {
|
|
|
|
|
|
|
|
placeConfigBlock(block, 12, y, Math.min(width, Math.max(configMinWidth(), width * .46)));
|
|
|
|
|
|
|
|
y += block.offsetHeight + gap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
const entry = configLayout[block.dataset.section];
|
|
|
|
const entry = configLayout[block.dataset.section];
|
|
|
|
if (!entry || configBlockState(block)) return;
|
|
|
|
if (!entry || configBlockState(block)) return;
|
|
|
|
const left = Number.isFinite(Number(entry.left)) ? Math.max(0, Math.min(width - 240, Number(entry.left))) : 0;
|
|
|
|
const minWidth = configMinWidth();
|
|
|
|
if (Number.isFinite(Number(entry.left))) block.style.left = `${left}px`;
|
|
|
|
const storedLeft = Number(entry.left);
|
|
|
|
|
|
|
|
const left = Number.isFinite(storedLeft) ? Math.max(0, Math.min(width - minWidth, storedLeft)) : null;
|
|
|
|
|
|
|
|
if (left != null) block.style.left = `${left}px`;
|
|
|
|
if (Number.isFinite(Number(entry.top))) block.style.top = `${Math.max(0, Number(entry.top))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.top))) block.style.top = `${Math.max(0, Number(entry.top))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.width))) block.style.width = `${Math.max(240, Math.min(width - left, Number(entry.width)))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.width))) block.style.width = `${Math.max(minWidth, Math.min(width - (left || 0), Number(entry.width)))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.height))) block.style.height = `${Math.max(110, Number(entry.height))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.height))) block.style.height = `${Math.max(110, Number(entry.height))}px`;
|
|
|
|
if (Number.isFinite(Number(entry.z))) { block.style.zIndex = String(Math.max(1, Number(entry.z))); configZIndex = Math.max(configZIndex, Number(entry.z)); }
|
|
|
|
if (Number.isFinite(Number(entry.z))) { block.style.zIndex = String(Math.max(1, Number(entry.z))); configZIndex = Math.max(configZIndex, Number(entry.z)); }
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -1945,26 +2004,98 @@ HTML = """<!doctype html>
|
|
|
|
if (!panel || !list || !toggle) return;
|
|
|
|
if (!panel || !list || !toggle) return;
|
|
|
|
const hidden = configBlocks.filter(block => configBlockState(block));
|
|
|
|
const hidden = configBlocks.filter(block => configBlockState(block));
|
|
|
|
list.innerHTML = '';
|
|
|
|
list.innerHTML = '';
|
|
|
|
hidden.forEach(block => {
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
const item = document.createElement('span');
|
|
|
|
const state = configBlockState(block);
|
|
|
|
item.className = 'config-restore-item';
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
|
|
|
item.className = `config-restore-item${state ? ' is-hidden' : ''}`;
|
|
|
|
const label = document.createElement('span');
|
|
|
|
const label = document.createElement('span');
|
|
|
|
|
|
|
|
label.className = 'config-item-title';
|
|
|
|
label.textContent = configTitle(block);
|
|
|
|
label.textContent = configTitle(block);
|
|
|
|
const restore = document.createElement('button');
|
|
|
|
const stateNode = document.createElement('span');
|
|
|
|
restore.type = 'button';
|
|
|
|
stateNode.className = 'config-item-state';
|
|
|
|
restore.textContent = configBlockState(block) === 'deleted' ? 'вернуть' : 'показать';
|
|
|
|
stateNode.textContent = state === 'deleted' ? 'удалён' : state === 'hidden' ? 'скрыт' : block.dataset.custom === 'true' ? 'свой' : 'активен';
|
|
|
|
restore.setAttribute('aria-label', `${restore.textContent} блок ${configTitle(block)}`);
|
|
|
|
const actions = document.createElement('span');
|
|
|
|
restore.addEventListener('click', () => setConfigBlockState(block, ''));
|
|
|
|
actions.className = 'config-item-actions';
|
|
|
|
item.append(label, restore);
|
|
|
|
const visibility = document.createElement('button');
|
|
|
|
|
|
|
|
visibility.type = 'button';
|
|
|
|
|
|
|
|
visibility.textContent = state ? 'вернуть' : 'скрыть';
|
|
|
|
|
|
|
|
visibility.setAttribute('aria-label', `${visibility.textContent} блок ${configTitle(block)}`);
|
|
|
|
|
|
|
|
visibility.addEventListener('click', () => setConfigBlockState(block, state ? '' : 'hidden'));
|
|
|
|
|
|
|
|
const duplicate = document.createElement('button');
|
|
|
|
|
|
|
|
duplicate.type = 'button';
|
|
|
|
|
|
|
|
duplicate.textContent = 'дубль';
|
|
|
|
|
|
|
|
duplicate.setAttribute('aria-label', `создать копию блока ${configTitle(block)}`);
|
|
|
|
|
|
|
|
duplicate.addEventListener('click', () => duplicateConfigBlock(block));
|
|
|
|
|
|
|
|
actions.append(visibility, duplicate);
|
|
|
|
|
|
|
|
if (block.dataset.custom === 'true' && !state) {
|
|
|
|
|
|
|
|
const remove = document.createElement('button');
|
|
|
|
|
|
|
|
remove.type = 'button';
|
|
|
|
|
|
|
|
remove.textContent = 'удалить';
|
|
|
|
|
|
|
|
remove.addEventListener('click', () => setConfigBlockState(block, 'deleted'));
|
|
|
|
|
|
|
|
actions.appendChild(remove);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
item.append(label, stateNode, actions);
|
|
|
|
list.appendChild(item);
|
|
|
|
list.appendChild(item);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
toggle.disabled = !hidden.length;
|
|
|
|
toggle.disabled = false;
|
|
|
|
toggle.setAttribute('aria-expanded', String(restorePanelOpen && !!hidden.length));
|
|
|
|
toggle.textContent = hidden.length ? `структура · ${hidden.length} скрыто` : 'структура';
|
|
|
|
panel.hidden = !restorePanelOpen || !hidden.length;
|
|
|
|
toggle.setAttribute('aria-expanded', String(restorePanelOpen));
|
|
|
|
|
|
|
|
panel.hidden = !restorePanelOpen;
|
|
|
|
const visible = configBlocks.length - hidden.length;
|
|
|
|
const visible = configBlocks.length - hidden.length;
|
|
|
|
setText('configLayoutState', `${visible} блоков · ${hidden.length} скрыто`);
|
|
|
|
setText('configLayoutState', `${visible} блоков · ${hidden.length} скрыто`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
let configZIndex = 1;
|
|
|
|
function nextCustomConfigKey() {
|
|
|
|
|
|
|
|
let index = 1;
|
|
|
|
|
|
|
|
while (configBlocks.some(block => block.dataset.section === `custom-${index}`)) index += 1;
|
|
|
|
|
|
|
|
return `custom-${index}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function createCustomConfigBlock(templateKey, title, existingKey = '') {
|
|
|
|
|
|
|
|
const template = configBlocks.find(block => !block.dataset.custom && block.dataset.section === templateKey);
|
|
|
|
|
|
|
|
if (!template || !configCanvas) return null;
|
|
|
|
|
|
|
|
const key = existingKey || nextCustomConfigKey();
|
|
|
|
|
|
|
|
const clone = template.cloneNode(true);
|
|
|
|
|
|
|
|
clone.dataset.section = key;
|
|
|
|
|
|
|
|
clone.dataset.custom = 'true';
|
|
|
|
|
|
|
|
clone.dataset.template = templateKey;
|
|
|
|
|
|
|
|
clone.classList.add('config-block', 'config-custom-block');
|
|
|
|
|
|
|
|
clone.removeAttribute('id');
|
|
|
|
|
|
|
|
clone.querySelectorAll('[id]').forEach(node => node.removeAttribute('id'));
|
|
|
|
|
|
|
|
clone.querySelectorAll('[for]').forEach(node => node.removeAttribute('for'));
|
|
|
|
|
|
|
|
const summary = clone.querySelector(':scope > summary');
|
|
|
|
|
|
|
|
if (summary) {
|
|
|
|
|
|
|
|
summary.textContent = '';
|
|
|
|
|
|
|
|
const titleNode = document.createElement('span');
|
|
|
|
|
|
|
|
titleNode.className = 'block-title';
|
|
|
|
|
|
|
|
titleNode.textContent = title || `Новый блок ${key.replace('custom-', '')}`;
|
|
|
|
|
|
|
|
summary.appendChild(titleNode);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
configCanvas.appendChild(clone);
|
|
|
|
|
|
|
|
configBlocks.push(clone);
|
|
|
|
|
|
|
|
const entry = configLayout[key] || {};
|
|
|
|
|
|
|
|
configLayout[key] = {...entry, custom: true, template: templateKey, title: titleNodeText(clone)};
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function titleNodeText(block) {
|
|
|
|
|
|
|
|
return block.querySelector(':scope > summary .block-title')?.textContent?.trim() || 'Новый блок';
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function positionNewConfigBlock(block) {
|
|
|
|
|
|
|
|
const bottom = configBlocks.filter(item => item !== block && !configBlockState(item)).reduce((value, item) => Math.max(value, (item.offsetTop || 0) + item.offsetHeight), 0);
|
|
|
|
|
|
|
|
const width = configCanvasWidth();
|
|
|
|
|
|
|
|
block.style.left = '12px';
|
|
|
|
|
|
|
|
block.style.top = `${Math.max(12, bottom + 18)}px`;
|
|
|
|
|
|
|
|
block.style.width = `${Math.min(width, Math.max(configMinWidth(), Math.round(width * .46)))}px`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
function duplicateConfigBlock(block) {
|
|
|
|
|
|
|
|
const clone = createCustomConfigBlock(configTemplateKey(block), `${configTitle(block)} · копия`);
|
|
|
|
|
|
|
|
if (!clone) return;
|
|
|
|
|
|
|
|
makeConfigBlockInteractive(clone);
|
|
|
|
|
|
|
|
positionNewConfigBlock(clone);
|
|
|
|
|
|
|
|
bringConfigBlockFront(clone);
|
|
|
|
|
|
|
|
captureConfigLayout(clone);
|
|
|
|
|
|
|
|
saveConfigLayout();
|
|
|
|
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
|
|
|
|
setText('configLayoutState', `создан блок «${configTitle(clone)}»`);
|
|
|
|
|
|
|
|
}
|
|
|
|
function bringConfigBlockFront(block) {
|
|
|
|
function bringConfigBlockFront(block) {
|
|
|
|
if (!block || configBlockState(block)) return;
|
|
|
|
if (!block || configBlockState(block)) return;
|
|
|
|
block.style.zIndex = String(++configZIndex);
|
|
|
|
block.style.zIndex = String(++configZIndex);
|
|
|
|
@ -1973,7 +2104,7 @@ HTML = """<!doctype html>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function configResizeEdge(event, block) {
|
|
|
|
function configResizeEdge(event, block) {
|
|
|
|
const rect = block.getBoundingClientRect();
|
|
|
|
const rect = block.getBoundingClientRect();
|
|
|
|
const edge = 8;
|
|
|
|
const edge = 9;
|
|
|
|
const left = event.clientX - rect.left <= edge;
|
|
|
|
const left = event.clientX - rect.left <= edge;
|
|
|
|
const right = rect.right - event.clientX <= edge;
|
|
|
|
const right = rect.right - event.clientX <= edge;
|
|
|
|
const top = event.clientY - rect.top <= edge;
|
|
|
|
const top = event.clientY - rect.top <= edge;
|
|
|
|
@ -1982,7 +2113,7 @@ HTML = """<!doctype html>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function trashDrop(event) {
|
|
|
|
function trashDrop(event) {
|
|
|
|
const trash = $('configTrash');
|
|
|
|
const trash = $('configTrash');
|
|
|
|
if (!trash || trash.hidden) return false;
|
|
|
|
if (!trash || trash.hidden || !event) return false;
|
|
|
|
const rect = trash.getBoundingClientRect();
|
|
|
|
const rect = trash.getBoundingClientRect();
|
|
|
|
return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
|
|
|
|
return event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2009,7 +2140,8 @@ HTML = """<!doctype html>
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function makeConfigBlockInteractive(block) {
|
|
|
|
function makeConfigBlockInteractive(block) {
|
|
|
|
const summary = block.firstElementChild;
|
|
|
|
const summary = block.firstElementChild;
|
|
|
|
if (!summary || summary.tagName !== 'SUMMARY') return;
|
|
|
|
if (!summary || summary.tagName !== 'SUMMARY' || block._configInteractive) return;
|
|
|
|
|
|
|
|
block._configInteractive = true;
|
|
|
|
const title = summary.textContent.trim();
|
|
|
|
const title = summary.textContent.trim();
|
|
|
|
summary.textContent = '';
|
|
|
|
summary.textContent = '';
|
|
|
|
const titleNode = document.createElement('span');
|
|
|
|
const titleNode = document.createElement('span');
|
|
|
|
@ -2026,9 +2158,8 @@ HTML = """<!doctype html>
|
|
|
|
});
|
|
|
|
});
|
|
|
|
block.addEventListener('pointerdown', event => {
|
|
|
|
block.addEventListener('pointerdown', event => {
|
|
|
|
bringConfigBlockFront(block);
|
|
|
|
bringConfigBlockFront(block);
|
|
|
|
if (window.innerWidth <= 760 || event.target.closest('summary')) return;
|
|
|
|
|
|
|
|
const edge = configResizeEdge(event, block);
|
|
|
|
const edge = configResizeEdge(event, block);
|
|
|
|
if (!edge) return;
|
|
|
|
if (!edge || (event.target.closest('summary') && !edge.includes('n'))) return;
|
|
|
|
event.preventDefault();
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
event.stopPropagation();
|
|
|
|
const startX = event.clientX;
|
|
|
|
const startX = event.clientX;
|
|
|
|
@ -2042,19 +2173,21 @@ HTML = """<!doctype html>
|
|
|
|
const move = moveEvent => {
|
|
|
|
const move = moveEvent => {
|
|
|
|
const dx = moveEvent.clientX - startX;
|
|
|
|
const dx = moveEvent.clientX - startX;
|
|
|
|
const dy = moveEvent.clientY - startY;
|
|
|
|
const dy = moveEvent.clientY - startY;
|
|
|
|
|
|
|
|
const minWidth = configMinWidth();
|
|
|
|
|
|
|
|
const minHeight = 110;
|
|
|
|
|
|
|
|
const canvasWidth = configCanvasWidth();
|
|
|
|
let left = startLeft;
|
|
|
|
let left = startLeft;
|
|
|
|
let top = startTop;
|
|
|
|
let top = startTop;
|
|
|
|
let width = startWidth;
|
|
|
|
let width = startWidth;
|
|
|
|
let height = startHeight;
|
|
|
|
let height = startHeight;
|
|
|
|
if (edge.includes('w')) { left = Math.max(0, Math.min(startLeft + dx, startLeft + startWidth - 240)); width = startWidth - (left - startLeft); }
|
|
|
|
if (edge.includes('w')) { left = Math.max(0, Math.min(startLeft + dx, startLeft + startWidth - minWidth)); width = startWidth - (left - startLeft); }
|
|
|
|
if (edge.includes('e')) width = Math.max(240, Math.min(configCanvas.clientWidth - left - 6, startWidth + dx));
|
|
|
|
if (edge.includes('e')) width = Math.max(minWidth, Math.min(canvasWidth - left, startWidth + dx));
|
|
|
|
if (edge.includes('n')) { top = Math.max(0, Math.min(startTop + dy, startTop + startHeight - 110)); height = startHeight - (top - startTop); }
|
|
|
|
if (edge.includes('n')) { top = Math.max(0, Math.min(startTop + dy, startTop + startHeight - minHeight)); height = startHeight - (top - startTop); }
|
|
|
|
if (edge.includes('s')) height = Math.max(110, startHeight + dy);
|
|
|
|
if (edge.includes('s')) height = Math.max(minHeight, startHeight + dy);
|
|
|
|
block.style.left = `${Math.round(left)}px`;
|
|
|
|
block.style.left = `${Math.round(left)}px`;
|
|
|
|
block.style.top = `${Math.round(top)}px`;
|
|
|
|
block.style.top = `${Math.round(top)}px`;
|
|
|
|
block.style.width = `${Math.round(width)}px`;
|
|
|
|
block.style.width = `${Math.round(width)}px`;
|
|
|
|
block.style.height = `${Math.round(height)}px`;
|
|
|
|
block.style.height = `${Math.round(height)}px`;
|
|
|
|
configManualLayout = true;
|
|
|
|
|
|
|
|
updateConfigCanvasHeight();
|
|
|
|
updateConfigCanvasHeight();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const end = () => {
|
|
|
|
const end = () => {
|
|
|
|
@ -2065,17 +2198,22 @@ HTML = """<!doctype html>
|
|
|
|
block.classList.remove('is-resizing');
|
|
|
|
block.classList.remove('is-resizing');
|
|
|
|
captureConfigLayout(block);
|
|
|
|
captureConfigLayout(block);
|
|
|
|
saveConfigLayout();
|
|
|
|
saveConfigLayout();
|
|
|
|
|
|
|
|
block._suppressSummaryClick = true;
|
|
|
|
|
|
|
|
window.setTimeout(() => { block._suppressSummaryClick = false; }, 0);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
block.addEventListener('pointermove', move);
|
|
|
|
block.addEventListener('pointermove', move);
|
|
|
|
block.addEventListener('pointerup', end, {once: true});
|
|
|
|
block.addEventListener('pointerup', end, {once: true});
|
|
|
|
block.addEventListener('pointercancel', end, {once: true});
|
|
|
|
block.addEventListener('pointercancel', end, {once: true});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
block.addEventListener('pointermove', event => {
|
|
|
|
block.addEventListener('pointermove', event => {
|
|
|
|
if (!block.classList.contains('is-resizing') && window.innerWidth > 760) block.style.cursor = configResizeEdge(event, block) ? configResizeEdge(event, block) + '-resize' : '';
|
|
|
|
if (!block.classList.contains('is-resizing')) {
|
|
|
|
|
|
|
|
const edge = configResizeEdge(event, block);
|
|
|
|
|
|
|
|
block.style.cursor = edge ? `${edge}-resize` : '';
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
block.addEventListener('pointerleave', () => { if (!block.classList.contains('is-resizing')) block.style.cursor = ''; });
|
|
|
|
block.addEventListener('pointerleave', () => { if (!block.classList.contains('is-resizing')) block.style.cursor = ''; });
|
|
|
|
summary.addEventListener('pointerdown', event => {
|
|
|
|
summary.addEventListener('pointerdown', event => {
|
|
|
|
if (event.button !== 0 || !configCanvas || window.innerWidth <= 760) return;
|
|
|
|
if (event.button !== 0 || !configCanvas) return;
|
|
|
|
const startX = event.clientX;
|
|
|
|
const startX = event.clientX;
|
|
|
|
const startY = event.clientY;
|
|
|
|
const startY = event.clientY;
|
|
|
|
const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0;
|
|
|
|
const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0;
|
|
|
|
@ -2092,10 +2230,9 @@ HTML = """<!doctype html>
|
|
|
|
if (trash) { trash.hidden = false; trash.classList.remove('is-over'); }
|
|
|
|
if (trash) { trash.hidden = false; trash.classList.remove('is-over'); }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
event.preventDefault();
|
|
|
|
const maxLeft = Math.max(0, configCanvas.clientWidth - block.offsetWidth - 6);
|
|
|
|
const maxLeft = Math.max(0, configCanvasWidth() - block.offsetWidth);
|
|
|
|
block.style.left = `${Math.max(0, Math.min(maxLeft, Math.round(startLeft + moveEvent.clientX - startX)))}px`;
|
|
|
|
block.style.left = `${Math.max(0, Math.min(maxLeft, Math.round(startLeft + moveEvent.clientX - startX)))}px`;
|
|
|
|
block.style.top = `${Math.max(0, Math.round(startTop + moveEvent.clientY - startY))}px`;
|
|
|
|
block.style.top = `${Math.max(0, Math.round(startTop + moveEvent.clientY - startY))}px`;
|
|
|
|
configManualLayout = true;
|
|
|
|
|
|
|
|
setTrashHover(moveEvent);
|
|
|
|
setTrashHover(moveEvent);
|
|
|
|
updateConfigCanvasHeight();
|
|
|
|
updateConfigCanvasHeight();
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -2114,19 +2251,23 @@ HTML = """<!doctype html>
|
|
|
|
block.addEventListener('toggle', updateConfigCanvasHeight);
|
|
|
|
block.addEventListener('toggle', updateConfigCanvasHeight);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function resetConfigLayout() {
|
|
|
|
function resetConfigLayout() {
|
|
|
|
configLayout = {};
|
|
|
|
const custom = {};
|
|
|
|
configManualLayout = false;
|
|
|
|
|
|
|
|
configZIndex = 1;
|
|
|
|
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
|
|
|
|
if (block.dataset.custom === 'true') custom[block.dataset.section] = {custom: true, template: configTemplateKey(block), title: configTitle(block)};
|
|
|
|
block.style.left = '';
|
|
|
|
block.style.left = '';
|
|
|
|
block.style.top = '';
|
|
|
|
block.style.top = '';
|
|
|
|
block.style.width = '';
|
|
|
|
block.style.width = '';
|
|
|
|
block.style.height = '';
|
|
|
|
block.style.height = '';
|
|
|
|
block.style.zIndex = '';
|
|
|
|
block.style.zIndex = '';
|
|
|
|
block.open = true;
|
|
|
|
block.open = true;
|
|
|
|
setConfigBlockState(block, '');
|
|
|
|
block.dataset.blockState = '';
|
|
|
|
|
|
|
|
block.classList.remove('config-block-hidden', 'config-block-deleted');
|
|
|
|
|
|
|
|
block.removeAttribute('aria-hidden');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
try { localStorage.removeItem('fpv-config-layout-v1'); localStorage.removeItem('fpv-open-sections'); } catch (_error) {}
|
|
|
|
configLayout = custom;
|
|
|
|
|
|
|
|
configZIndex = 1;
|
|
|
|
|
|
|
|
try { localStorage.removeItem('fpv-open-sections'); } catch (_error) {}
|
|
|
|
|
|
|
|
saveConfigLayout();
|
|
|
|
applyStoredConfigLayout();
|
|
|
|
applyStoredConfigLayout();
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
setText('configLayoutState', 'раскладка сброшена');
|
|
|
|
setText('configLayoutState', 'раскладка сброшена');
|
|
|
|
@ -2134,13 +2275,21 @@ HTML = """<!doctype html>
|
|
|
|
function initConfigCanvas() {
|
|
|
|
function initConfigCanvas() {
|
|
|
|
configCanvas = $('controlForm');
|
|
|
|
configCanvas = $('controlForm');
|
|
|
|
if (!configCanvas) return;
|
|
|
|
if (!configCanvas) return;
|
|
|
|
configBlocks = Array.from(configCanvas.children).filter(node => node.matches('details[data-section]'));
|
|
|
|
|
|
|
|
configLayout = readConfigLayout();
|
|
|
|
configLayout = readConfigLayout();
|
|
|
|
|
|
|
|
configBlocks = Array.from(configCanvas.children).filter(node => node.matches('details[data-section]'));
|
|
|
|
|
|
|
|
Object.entries(configLayout).forEach(([key, entry]) => {
|
|
|
|
|
|
|
|
if (!entry || entry.custom !== true || configBlocks.some(block => block.dataset.section === key)) return;
|
|
|
|
|
|
|
|
createCustomConfigBlock(entry.template || 'source', entry.title || 'Новый блок', key);
|
|
|
|
|
|
|
|
});
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
configBlocks.forEach(block => {
|
|
|
|
block.classList.add('config-block');
|
|
|
|
block.classList.add('config-block');
|
|
|
|
makeConfigBlockInteractive(block);
|
|
|
|
makeConfigBlockInteractive(block);
|
|
|
|
const state = configLayout[block.dataset.section]?.state;
|
|
|
|
const state = configLayout[block.dataset.section]?.state;
|
|
|
|
if (state) setConfigBlockState(block, state);
|
|
|
|
if (state) {
|
|
|
|
|
|
|
|
block.dataset.blockState = state;
|
|
|
|
|
|
|
|
block.classList.add(state === 'deleted' ? 'config-block-deleted' : 'config-block-hidden');
|
|
|
|
|
|
|
|
block.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
applyStoredConfigLayout();
|
|
|
|
applyStoredConfigLayout();
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
@ -2149,14 +2298,23 @@ HTML = """<!doctype html>
|
|
|
|
restorePanelOpen = !restorePanelOpen;
|
|
|
|
restorePanelOpen = !restorePanelOpen;
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
$('configCreateBlock')?.addEventListener('click', () => {
|
|
|
|
if (!configManualLayout) applyStoredConfigLayout();
|
|
|
|
const templateKey = $('configTemplateSelect')?.value || 'source';
|
|
|
|
else updateConfigCanvasHeight();
|
|
|
|
const title = String($('configBlockTitle')?.value || '').trim().slice(0, 48) || 'Новый блок';
|
|
|
|
|
|
|
|
const block = createCustomConfigBlock(templateKey, title);
|
|
|
|
|
|
|
|
if (!block) return;
|
|
|
|
|
|
|
|
makeConfigBlockInteractive(block);
|
|
|
|
|
|
|
|
positionNewConfigBlock(block);
|
|
|
|
|
|
|
|
bringConfigBlockFront(block);
|
|
|
|
|
|
|
|
captureConfigLayout(block);
|
|
|
|
|
|
|
|
saveConfigLayout();
|
|
|
|
|
|
|
|
if ($('configBlockTitle')) $('configBlockTitle').value = '';
|
|
|
|
|
|
|
|
renderConfigRestorePanel();
|
|
|
|
|
|
|
|
setText('configLayoutState', `создан блок «${configTitle(block)}»`);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
window.addEventListener('resize', applyStoredConfigLayout);
|
|
|
|
if (window.ResizeObserver) {
|
|
|
|
if (window.ResizeObserver) {
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
|
|
const observer = new ResizeObserver(updateConfigCanvasHeight);
|
|
|
|
updateConfigCanvasHeight();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
configBlocks.forEach(block => observer.observe(block));
|
|
|
|
configBlocks.forEach(block => observer.observe(block));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -2920,10 +3078,7 @@ HTML = """<!doctype html>
|
|
|
|
$('archiveTab').hidden = name !== 'archive';
|
|
|
|
$('archiveTab').hidden = name !== 'archive';
|
|
|
|
$('settingsTab').hidden = name !== 'settings';
|
|
|
|
$('settingsTab').hidden = name !== 'settings';
|
|
|
|
$('modelTab').hidden = name !== 'model';
|
|
|
|
$('modelTab').hidden = name !== 'model';
|
|
|
|
if (name === 'settings' && typeof applyStoredConfigLayout === 'function') requestAnimationFrame(() => {
|
|
|
|
if (name === 'settings' && typeof applyStoredConfigLayout === 'function') requestAnimationFrame(applyStoredConfigLayout);
|
|
|
|
if (!configManualLayout) applyStoredConfigLayout();
|
|
|
|
|
|
|
|
else updateConfigCanvasHeight();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
if (name === 'archive') loadArchive();
|
|
|
|
if (name === 'archive') loadArchive();
|
|
|
|
if (updateHash && location.hash !== `#${name}`) history.replaceState(null, '', `#${name}`);
|
|
|
|
if (updateHash && location.hash !== `#${name}`) history.replaceState(null, '', `#${name}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|