// web-test dom shared v1.4 — embedded JS function constants // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills /** * Shared function strings embedded into page.evaluate() generators. * Не экспортируются наружу через dom.mjs facade — внутренняя кухня. */ /** Find visible #modalSurface. 1C may leave multiple #modalSurface in DOM (duplicate id), * e.g. when a second form (drill-down) creates its own alongside a stale one from the first * form. getElementById returns the FIRST in document order, which may be hidden. Scan all. */ export const HAS_VISIBLE_MODAL_FN = `function hasVisibleModal() { const all = document.querySelectorAll('#modalSurface'); for (const el of all) { if (el.offsetWidth > 0) return true; } return false; }`; /** * Click point INSIDE a grid row's first visible text cell — NOT the row-line centre. * * A wide multi-column row's centre `x = line.x + line.width/2` lands far beyond the * form's horizontal viewport (the `.gridLine` spans ALL columns, frozen + scrollable), * so `mouse.click` at that X falls on an overlay outside the visible grid and the row * is never hit — the click silently does nothing. Seen on narrow modal selection forms * with many columns (множественный выбор) and the `not_selectable` bug on selection forms. * * Picks the first visible non-checkbox cell that HAS text (so center-clicking never * toggles a checkbox/picture mark), skips the first column on tree grids (it holds the * expand toggle), and clamps X near the left edge (`min(width/2, 60)`) so a wide first * column still lands in the viewport. * * @param line a `.gridLine` element * @param body the grid's `.gridBody` (for tree detection); may be null * @returns `{ x, y }` rounded, or `null` when the row has no usable cell. */ export const ROW_CLICK_POINT_FN = `function rowClickPoint(line, body) { const isTree = !!(body && body.querySelector('.gridBoxTree')); let cells = [...line.children] .filter(b => b.offsetWidth > 0) .map(b => ({ r: b.getBoundingClientRect(), checkbox: !!b.querySelector('.checkbox'), hasText: !!b.querySelector('.gridBoxText') })); if (isTree && cells.length > 1) cells = cells.slice(1); const pick = cells.find(c => !c.checkbox && c.hasText) || cells.find(c => !c.checkbox) || cells[0]; if (!pick) return null; return { x: Math.round(pick.r.x + Math.min(pick.r.width / 2, 60)), y: Math.round(pick.r.y + pick.r.height / 2) }; }`; /** * Single source of truth for column derivation on HEADERLESS grids (no `.gridHead`). * 1C still puts `colindex` on body cells, so anchoring works without a header. * Returns ordered descriptors consumed identically by readers (readTable, getFormState) * and resolvers (findCellCoords, findGridCell, scanGridRows) so a synthesized name like * "Колонка1" always maps to the same physical cell on both read and write. * * Descriptor: { name, kind:'data'|'checkbox'|'picture', colindex, subTarget:'checkbox'|'title'|'text'|null } * - colindex — anchor: find the cell via line.children box with matching getAttribute('colindex'). * - subTarget — node inside that box: 'checkbox' → .checkbox, 'title' → .gridBoxTitle, * 'text' → .gridBoxText, null → box itself. * * A COMBINED mark-box (one box holding BOTH .checkbox AND non-empty .gridBoxTitle, e.g. the * value-list checkbox mark-lists) is split into TWO logical columns sharing one colindex: * "(checkbox)" (subTarget:checkbox) + "КолонкаN" (subTarget:title). Data columns are numbered * КолонкаN among themselves (checkbox/picture don't consume a number); duplicate * "(checkbox)"/"(picture)" get a " 2", " 3" suffix. */ export const HEADERLESS_GRID_FN = `function synthHeaderlessColumns(grid) { function picInfo(cell) { if (!cell) return null; if (cell.querySelector('.gridListH, .gridListV, [tree="true"], .gridBoxTree')) return null; const dib = cell.querySelector('.gridBoxImg .dIB'); if (!dib) return null; const bg = dib.style.backgroundImage || ''; if (!bg.includes('pictureCollection/picture/')) return null; const m = bg.match(/[?&]gx=(\\d+)/); return { gx: m ? m[1] : '0' }; } const body = grid.querySelector('.gridBody'); if (!body) return []; const line = body.querySelector('.gridLine'); if (!line) return []; const cols = []; let dataN = 0; const uniq = (base) => { if (!cols.some(c => c.name === base)) return base; let n = 2; while (cols.some(c => c.name === base + ' ' + n)) n++; return base + ' ' + n; }; [...line.children].forEach(box => { if (box.offsetWidth === 0) return; const ci = box.getAttribute('colindex'); if (ci == null) return; const chk = box.querySelector('.checkbox'); const titleEl = box.querySelector('.gridBoxTitle'); const textEl = box.querySelector('.gridBoxText'); const titleTxt = ((titleEl ? titleEl.innerText : '') || '').trim(); if (chk && titleTxt) { cols.push({ name: uniq('(checkbox)'), kind: 'checkbox', colindex: ci, subTarget: 'checkbox' }); cols.push({ name: 'Колонка' + (++dataN), kind: 'data', colindex: ci, subTarget: 'title' }); } else if (chk) { cols.push({ name: uniq('(checkbox)'), kind: 'checkbox', colindex: ci, subTarget: 'checkbox' }); } else if (picInfo(box)) { cols.push({ name: uniq('(picture)'), kind: 'picture', colindex: ci, subTarget: null }); } else { cols.push({ name: 'Колонка' + (++dataN), kind: 'data', colindex: ci, subTarget: textEl ? 'text' : (titleEl ? 'title' : null) }); } }); return cols; }`; /** * Single source of truth for columns of a grid WITH a header — the headed twin of * synthHeaderlessColumns above, and for the same reason: a column name must map to the same * physical cell for readers (readTable) and resolvers (click, row search, filter, fill). * * Column identity is `colindex` — 1С's own column id, present on both header boxes and body * cells. Geometry is the FALLBACK, used only for cells that have no header of their own * (sub-rows of a merged header, e.g. «Субконто Дт» over three stacked cells). * * Why colindex first: a wide header (ERP task list, «Исполнитель» spanning x 1085…1515) covers * the narrow headers below it («Срок» 1085…1251, «Выполнена» 1251…1515). Matching a cell by its * center-x alone puts the «Исполнитель» cell (center 1300) into the «Выполнена» group — which * both fakes a merged header (phantom «Выполнена 1/2») and glues foreign values together. * The write path (grid-edit.mjs) already resolves cells by colindex for exactly this reason. * * Column: { name, text, title, ci, x, right, y, h, fixed, kind?, subIdx? } * - ci — anchor; null for expanded sub-columns (their cells carry a different colindex). * - subIdx — set on «Имя 1/2/3» columns expanded from ONE header over several sub-rows; * such a cell is found by its Y order inside the header's x-range. */ export const COLUMN_MODEL_FN = HEADERLESS_GRID_FN + ` function picInfoShared(cell) { if (!cell) return null; if (cell.querySelector('.gridListH, .gridListV, [tree="true"], .gridBoxTree')) return null; const dib = cell.querySelector('.gridBoxImg .dIB'); if (!dib) return null; const bg = dib.style.backgroundImage || ''; if (!bg.includes('pictureCollection/picture/')) return null; const m = bg.match(/[?&]gx=(\\d+)/); return { gx: m ? m[1] : '0' }; } function buildColumnModel(grid) { const head = grid.querySelector('.gridHead'); const body = grid.querySelector('.gridBody'); const empty = { columns: [], byCi: {}, groups: new Map(), subRows: {}, multiRow: {}, headless: !head }; if (!body) return empty; if (!head) { const cols = synthHeaderlessColumns(grid).map(c => ({ name: c.name, text: c.name, title: '', ci: c.colindex, subTarget: c.subTarget, kind: c.kind, x: 0, right: 0, y: 0, h: 0, fixed: false, })); const byCi = {}; cols.forEach(c => { if (c.ci != null && byCi[c.ci] === undefined) byCi[c.ci] = c; }); return { columns: cols, byCi, groups: new Map(), subRows: {}, multiRow: {}, headless: true }; } const headLine = head.querySelector('.gridLine') || head; const lines = [...body.querySelectorAll('.gridLine')]; const cellByCi = (line, ci) => [...line.children].find(b => b.offsetWidth > 0 && b.getAttribute('colindex') === ci); const columns = []; [...headLine.children].forEach(box => { if (box.offsetWidth === 0) return; const ci = box.getAttribute('colindex'); const textEl = box.querySelector('.gridBoxText'); const text = ((textEl || box).innerText || '').trim().replace(/\\n/g, ' '); const title = (box.getAttribute('title') || '').trim(); const r = box.getBoundingClientRect(); const base = { ci, x: r.x, right: r.x + r.width, y: r.y, h: r.height, fixed: box.classList.contains('gridBoxFix') }; if (text) { columns.push(Object.assign(base, { name: text, text: text, title: title })); return; } // Unnamed header — a column only if its cells hold a checkbox or a picture. 1С doesn't // expose the technical name, so it is named by the header tooltip. // Sample SEVERAL rows: a picture bound to a Boolean draws nothing for false, so an empty // first row is not evidence that the column has no pictures at all. let kind = null; for (const line of lines.slice(0, 10)) { const cell = ci != null ? cellByCi(line, ci) : null; if (!cell) continue; if (cell.querySelector('.checkbox')) { kind = 'checkbox'; break; } if (picInfoShared(cell)) { kind = 'picture'; break; } } if (!kind && picInfoShared(box)) kind = 'picture'; if (!kind) return; let name = kind === 'checkbox' ? '(checkbox)' : (title || '(picture)'); if (columns.some(c => c.name === name)) { let n = 2; while (columns.some(c => c.name === name + ' ' + n)) n++; name = name + ' ' + n; } columns.push(Object.assign(base, { name: name, text: '', title: title, kind: kind })); }); const keyOf = c => Math.round(c.x) + ':' + Math.round(c.right); const groups = new Map(); columns.forEach(c => { const k = keyOf(c); if (!groups.has(k)) groups.set(k, []); groups.get(k).push(c); }); for (const hdrs of groups.values()) hdrs.sort((a, b) => a.y - b.y); const byCi = {}; columns.forEach(c => { if (c.ci != null && byCi[c.ci] === undefined) byCi[c.ci] = c; }); // Sub-rows per x-group, measured on the first data line. A cell belongs to the group of its // OWN header whenever colindex says so; only header-less cells are placed geometrically. const subRows = {}; if (lines[0]) { [...lines[0].children].forEach(box => { if (box.offsetWidth === 0) return; const ci = box.getAttribute('colindex'); const own = ci != null ? byCi[ci] : null; let key = null; const r = box.getBoundingClientRect(); if (own) key = keyOf(own); else { const cx = r.x + r.width / 2; for (const [k, hdrs] of groups) { if (cx >= hdrs[0].x && cx < hdrs[0].right) { key = k; break; } } } if (key == null) return; (subRows[key] = subRows[key] || []).push({ y: r.y }); }); Object.keys(subRows).forEach(k => subRows[k].sort((a, b) => a.y - b.y)); } // Stacked headers (2+ over several sub-rows) → match by Y order. // ONE header over several sub-rows → merged header: expand into «Имя 1..N». const multiRow = {}; for (const [k, hdrs] of groups) { const subs = subRows[k]; if (!subs || subs.length <= 1) continue; if (hdrs.length >= 2) { multiRow[k] = hdrs; continue; } const base = hdrs[0]; const at = columns.indexOf(base); columns.splice(at, 1); if (base.ci != null && byCi[base.ci] === base) delete byCi[base.ci]; const expanded = []; for (let si = 0; si < subs.length; si++) { const col = Object.assign({}, base, { name: base.name + ' ' + (si + 1), ci: null, y: base.y + si, h: base.h / subs.length, subIdx: si, }); columns.splice(at + si, 0, col); expanded.push(col); } groups.set(k, expanded); multiRow[k] = expanded; } return { columns: columns, byCi: byCi, groups: groups, subRows: subRows, multiRow: multiRow, headless: false }; } /** Cell → column. colindex first; geometry only for cells without a header of their own. */ function columnForCell(model, box) { const ci = box.getAttribute('colindex'); if (ci != null && model.byCi[ci]) return model.byCi[ci]; const r = box.getBoundingClientRect(); const cx = r.x + r.width / 2; const fixed = box.classList.contains('gridBoxFix'); for (const k of Object.keys(model.multiRow)) { const hdrs = model.multiRow[k]; if (cx < hdrs[0].x || cx >= hdrs[0].right) continue; const subs = model.subRows[k]; if (subs) { const si = subs.findIndex(s => Math.abs(s.y - r.y) < 5); if (si >= 0 && si < hdrs.length) return hdrs[si]; } let best = hdrs[0], bd = Infinity; for (const h of hdrs) { const d = Math.abs(r.y - h.y); if (d < bd) { bd = d; best = h; } } return best; } return model.columns.find(c => cx >= c.x && cx < c.right && c.fixed === fixed) || null; } /** Column → cell inside a given line. Mirror of columnForCell, same precedence. */ function cellForColumn(model, line, col) { const boxes = [...line.children].filter(b => b.offsetWidth > 0); if (col.subIdx != null) { const inGroup = boxes .filter(b => { const r = b.getBoundingClientRect(); const cx = r.x + r.width / 2; return cx >= col.x && cx < col.right && b.classList.contains('gridBoxFix') === col.fixed; }) .sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y); return inGroup[col.subIdx] || null; } if (col.ci != null) { const hit = boxes.find(b => b.getAttribute('colindex') === col.ci); if (hit) return hit; } return boxes .filter(b => b.classList.contains('gridBoxFix') === col.fixed) .find(b => { const r = b.getBoundingClientRect(); const cx = r.x + r.width / 2; return cx >= col.x && cx < col.right; }) || null; } /** Column by user-supplied name: exact → «Группа / Имя» suffix → substring. */ function resolveColumnByName(model, name) { const lo = s => (s || '').toLowerCase().replace(/ё/g, 'е').trim(); const cand = c => [c.name, c.text, c.title].filter(Boolean); const n = lo(name); const suffix = lo(' / ' + name); return model.columns.find(c => cand(c).some(t => lo(t) === n)) || model.columns.find(c => cand(c).some(t => lo(t).endsWith(suffix))) || model.columns.find(c => cand(c).some(t => lo(t).includes(n))) || null; }`; /** Detect active form number. Picks form with most visible elements, skipping form0. * When modalSurface is visible — prefer the highest-numbered form (modal dialog). */ export const DETECT_FORM_FN = HAS_VISIBLE_MODAL_FN + ` function detectForm() { const counts = {}; document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => { if (el.offsetWidth === 0) return; const m = el.id.match(/^form(\\d+)_/); if (m) counts[m[1]] = (counts[m[1]] || 0) + 1; }); const nums = Object.keys(counts).map(Number); if (!nums.length) return null; const candidates = nums.filter(n => n > 0); if (!candidates.length) return nums[0]; // When modal surface is visible, prefer the highest-numbered form (modal dialog) if (hasVisibleModal()) { const maxForm = Math.max(...candidates); if (counts[maxForm] >= 1) return maxForm; } return candidates.reduce((best, n) => counts[n] > counts[best] ? n : best); }`; /** Detect all open forms + modal state. Returns { activeForm, allForms, formCount, modal }. * Works even when the open-windows tab bar is hidden. */ export const DETECT_FORMS_FN = HAS_VISIBLE_MODAL_FN + ` function detectForms() { const counts = {}; document.querySelectorAll('input.editInput[id], textarea[id], a.press[id]').forEach(el => { if (el.offsetWidth === 0) return; const m = el.id.match(/^form(\\d+)_/); if (m) counts[m[1]] = (counts[m[1]] || 0) + 1; }); const nums = Object.keys(counts).map(Number); return { allForms: nums.sort((a, b) => a - b), formCount: nums.length, modal: hasVisibleModal() }; }`; /** Read form state given prefix p. Returns { fields, buttons, tabs, texts, hyperlinks, table, iframes }. */ export const READ_FORM_FN = HEADERLESS_GRID_FN + ` function readForm(p) { const result = {}; const fields = []; const buttons = []; const formTabs = []; const texts = []; const hyperlinks = []; // Normalize non-breaking spaces to regular spaces const nbsp = s => (s || '').replace(/\\u00a0/g, ' '); // Fields (inputs) document.querySelectorAll('input.editInput[id^="' + p + '"]').forEach(el => { if (el.offsetWidth === 0) return; const name = el.id.replace(p, '').replace(/_i\\d+$/, ''); const titleEl = document.getElementById(p + name + '#title_text') || document.getElementById(p + name + '#title_div'); const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ')); const actions = []; if (document.getElementById(p + name + '_DLB')?.offsetWidth > 0) actions.push('select'); if (document.getElementById(p + name + '_OB')?.offsetWidth > 0) actions.push('open'); if (document.getElementById(p + name + '_CLR')?.offsetWidth > 0) actions.push('clear'); if (document.getElementById(p + name + '_CB')?.offsetWidth > 0) actions.push('pick'); const field = { name, value: el.value || '' }; // Multi-value reference fields keep their value in .chipsItem chips, not in input.value if (!field.value) { const labelEl = document.getElementById(p + name); if (labelEl) { const chipTexts = [...labelEl.querySelectorAll('.chipsItem .chipsTitle')] .map(c => nbsp(c.innerText?.trim() || '')) .filter(Boolean); if (chipTexts.length) field.value = chipTexts.join(', '); } } if (label && label !== name) field.label = label; if (el.readOnly) field.readonly = true; if (el.disabled) field.disabled = true; if (el.type && el.type !== 'text') field.type = el.type; if (document.activeElement === el) field.focused = true; if (actions.length) field.actions = actions; if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true; fields.push(field); }); // Textareas document.querySelectorAll('textarea[id^="' + p + '"]').forEach(el => { if (el.offsetWidth === 0) return; const name = el.id.replace(p, '').replace(/_i\\d+$/, ''); const titleEl = document.getElementById(p + name + '#title_text') || document.getElementById(p + name + '#title_div'); const label = nbsp((titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ')); const field = { name, value: el.value || '', type: 'textarea' }; if (label && label !== name) field.label = label; if (el.readOnly) field.readonly = true; if (el.disabled) field.disabled = true; if (document.activeElement === el) field.focused = true; if (el.closest('.inputsBox')?.classList.contains('markIncomplete')) field.required = true; fields.push(field); }); // Checkboxes document.querySelectorAll('[id^="' + p + '"].checkbox').forEach(el => { if (el.offsetWidth === 0) return; const name = el.id.replace(p, ''); const titleEl = document.getElementById(p + name + '#title_text'); const label = nbsp(titleEl?.innerText?.trim() || ''); const field = { name, value: el.classList.contains('checked') || el.classList.contains('checkboxOn') || el.classList.contains('select'), type: 'checkbox' }; if (label && label !== name) field.label = label; if (el.classList.contains('checkboxDisabled')) field.disabled = true; fields.push(field); }); // Radio buttons — base element is option 0, others are #N#radio (N >= 1) const radioGroups = {}; document.querySelectorAll('[id^="' + p + '"].radio').forEach(el => { if (el.offsetWidth === 0) return; const id = el.id.replace(p, ''); const m = id.match(/^(.+?)#(\\d+)#radio$/); if (m) { // Options 1, 2, ... have explicit #N#radio suffix const [, groupName, idx] = m; if (!radioGroups[groupName]) radioGroups[groupName] = []; const labelEl = document.getElementById(p + groupName + '#' + idx + '#radio_text'); const label = nbsp(labelEl?.innerText?.trim() || 'option' + idx); radioGroups[groupName].push({ index: parseInt(idx), label, selected: el.classList.contains('select') }); } else if (!id.includes('#')) { // Base element = option 0 (no #0#radio suffix) if (!radioGroups[id]) radioGroups[id] = []; const labelEl = document.getElementById(p + id + '#0#radio_text'); const label = nbsp(labelEl?.innerText?.trim() || 'option0'); radioGroups[id].unshift({ index: 0, label, selected: el.classList.contains('select') }); } }); for (const [name, options] of Object.entries(radioGroups)) { const titleEl = document.getElementById(p + name + '#title_text'); const label = titleEl?.innerText?.trim() || ''; const selected = options.find(o => o.selected); const field = { name, value: selected?.label || '', type: 'radio', options: options.map(o => o.label) }; if (label && label !== name) field.label = label; if (document.getElementById(p + name)?.classList.contains('radioDisabled')) field.disabled = true; fields.push(field); } // Buttons (a.press) document.querySelectorAll('a.press[id^="' + p + '"]').forEach(el => { if (el.offsetWidth === 0) return; const idName = el.id.replace(p, ''); if (/_(?:DLB|CLR|OB|CB)$/.test(idName)) return; const span = el.querySelector('.submenuText') || el.querySelector('span'); const text = nbsp(span?.textContent?.trim() || el.innerText?.trim() || ''); if (!text && !el.classList.contains('pressCommand')) return; const btn = { name: text || idName }; if (el.classList.contains('pressDefault')) btn.default = true; if (el.classList.contains('pressDisabled')) btn.disabled = true; // Icon-only buttons: expose tooltip from DOM title attribute (1C puts title on parent .framePress) if (!text) { const tip = nbsp(el.title || el.parentElement?.title || ''); if (tip) btn.tooltip = tip; } buttons.push(btn); }); // Frame buttons document.querySelectorAll('[id^="' + p + '"].frameButton, [id^="' + p + '"] .frameButton').forEach(el => { if (el.offsetWidth === 0) return; const text = nbsp(el.innerText?.trim() || ''); const idName = el.id?.replace(p, '') || ''; if (!text && !idName) return; // frameButton disabled uses the same class as a.press buttons (pressDisabled). const btn = { name: text || idName, frame: true }; if (el.classList.contains('pressDisabled')) btn.disabled = true; buttons.push(btn); }); // Tumbler items. Disabled state lives on the group element .frameTumbler // (class tumblerDisabled), not on the individual segments. document.querySelectorAll('[id^="' + p + '"].tumblerItem').forEach(el => { if (el.offsetWidth === 0) return; const text = el.innerText?.trim(); const idName = el.id?.replace(p, '') || ''; const btn = { name: text || idName, tumbler: true }; if (el.closest('.frameTumbler')?.classList.contains('tumblerDisabled')) btn.disabled = true; buttons.push(btn); }); // Tabs — scoped to form by checking ancestor IDs document.querySelectorAll('[data-content]').forEach(el => { if (el.offsetWidth === 0) return; let node = el.parentElement; let inForm = false; while (node) { if (node.id && node.id.startsWith(p)) { inForm = true; break; } node = node.parentElement; } if (!inForm) return; const tab = { name: el.dataset.content }; if (el.classList.contains('select')) tab.active = true; formTabs.push(tab); }); // Static texts and hyperlinks document.querySelectorAll('[id^="' + p + '"].staticText').forEach(el => { if (el.offsetWidth === 0) return; const name = el.id.replace(p, ''); if (name.endsWith('_div') || name.includes('#title')) return; const text = el.innerText?.trim(); if (!text) return; if (el.classList.contains('staticTextHyper')) { hyperlinks.push({ name: text }); } else { const titleEl = document.getElementById(p + name + '#title_text'); const label = titleEl?.innerText?.trim() || ''; const entry = { name, value: text }; if (label) entry.label = label; texts.push(entry); } }); // Tables/grids — collect ALL visible grids const allGrids = [...document.querySelectorAll('[id^="' + p + '"].grid, [id^="' + p + '"] .grid')] .filter(g => g.offsetWidth > 0 && g.offsetHeight > 0); if (allGrids.length > 0) { const tables = allGrids.map(grid => { const name = grid.id ? grid.id.replace(p, '') : ''; const head = grid.querySelector('.gridHead'); const body = grid.querySelector('.gridBody'); const columns = []; if (head) { const headLine = head.querySelector('.gridLine') || head; [...headLine.children].forEach(box => { if (box.offsetWidth === 0) return; const textEl = box.querySelector('.gridBoxText'); const text = (textEl || box).innerText?.trim().replace(/\\n/g, ' ') || ''; if (text) { const r = box.getBoundingClientRect(); columns.push({ text, x: r.x, right: r.x + r.width, y: r.y, h: r.height }); } else { // Unnamed column — check if data cells contain checkboxes const firstLine = body?.querySelector('.gridLine'); if (firstLine) { const visibleHeaders = [...headLine.children].filter(c => c.offsetWidth > 0); const idx = visibleHeaders.indexOf(box); const cells = [...firstLine.children].filter(c => c.offsetWidth > 0); if (cells[idx]?.querySelector('.checkbox')) { columns.push({ text: '(checkbox)', x: 0, right: 0, y: 0, h: 0 }); } } } }); // Expand single merged headers with multiple data sub-rows (e.g. "Субконто Дт" → 1/2/3) const firstLine = body?.querySelector('.gridLine'); if (firstLine && columns.length > 0) { const xGrp = new Map(); columns.forEach(c => { const k = Math.round(c.x) + ':' + Math.round(c.right); if (!xGrp.has(k)) xGrp.set(k, []); xGrp.get(k).push(c); }); for (const [k, hdrs] of xGrp) { if (hdrs.length !== 1) continue; let cnt = 0; [...firstLine.children].forEach(box => { if (box.offsetWidth === 0) return; const r = box.getBoundingClientRect(); const cx = r.x + r.width / 2; if (cx >= hdrs[0].x && cx < hdrs[0].right) cnt++; }); if (cnt > 1) { const base = hdrs[0]; const baseIdx = columns.indexOf(base); columns.splice(baseIdx, 1); for (let si = 0; si < cnt; si++) { columns.splice(baseIdx + si, 0, { text: base.text + ' ' + (si + 1), x: base.x, right: base.right, y: 0, h: 0 }); } } } } } else if (body) { // Headerless grid — synthesize columns by colindex (single source). synthHeaderlessColumns(grid).forEach(c => columns.push({ text: c.name, x: 0, right: 0, y: 0, h: 0 })); } const colNames = columns.map(c => c.text); const rowCount = body ? body.querySelectorAll('.gridLine').length : 0; // Visual label from group title (e.g. "Входящие:" for grid "Входящие") const titleEl = document.getElementById(p + name + '#title_div') || document.getElementById(p + 'Группа' + name + '#title_div'); const label = titleEl ? (titleEl.innerText?.trim().replace(/:\\s*$/, '').replace(/\\u00a0/g, ' ') || null) : null; return { name, columns: colNames, rowCount, ...(label ? { label } : {}) }; }); result.tables = tables; // Backward compat: table = first grid summary const first = tables[0]; result.table = { present: true, columns: first.columns, rowCount: first.rowCount }; } // Active filters (train badges above grid: *СостояниеПросмотра) const filters = []; document.querySelectorAll('[id^="' + p + '"].trainItem').forEach(el => { if (el.offsetWidth === 0) return; const titleEl = el.querySelector('.trainName'); const valueEl = el.querySelector('.trainTitle'); if (!titleEl && !valueEl) return; const field = (titleEl?.innerText?.trim() || '').replace(/\\n/g, ' ').replace(/\\s*:$/, '').trim(); const value = valueEl?.innerText?.trim()?.replace(/\\n/g, ' ') || ''; if (field || value) filters.push({ field, value }); }); // Also check search field value const searchInput = [...document.querySelectorAll('input.editInput[id^="' + p + '"]')] .find(el => el.offsetWidth > 0 && /Строк[аи]Поиска|SearchString/i.test(el.id)); if (searchInput?.value) { filters.push({ type: 'search', value: searchInput.value }); } if (filters.length) result.filters = filters; // Navigation panel (FormNavigationPanel) — lives in parent page{N} container const navigation = []; const formEl = document.querySelector('[id^="' + p + '"]'); if (formEl) { let pageEl = formEl.parentElement; while (pageEl && !(pageEl.id && /^page\\d+$/.test(pageEl.id))) pageEl = pageEl.parentElement; if (pageEl) { pageEl.querySelectorAll('.navigationItem').forEach(el => { if (el.offsetWidth === 0) return; const nameEl = el.querySelector('.navigationItemName'); const text = (nameEl?.innerText?.trim() || '').replace(/\\u00a0/g, ' '); if (!text) return; const nav = { name: text }; if (el.classList.contains('select')) nav.active = true; navigation.push(nav); }); } } // Iframes let iframeCount = 0; document.querySelectorAll('[id^="' + p + '"] iframe, iframe[id^="' + p + '"]').forEach(el => { if (el.offsetWidth > 0 && el.offsetHeight > 0) iframeCount++; }); if (iframeCount) result.iframes = iframeCount; if (fields.length) result.fields = fields; if (buttons.length) result.buttons = buttons; if (formTabs.length) result.tabs = formTabs; if (navigation.length) result.navigation = navigation; if (texts.length) result.texts = texts; if (hyperlinks.length) result.hyperlinks = hyperlinks; // Group DCS report settings into readable format if (result.fields) { const dcsRe = /^(.+Элемент(\\d+))(Использование|Значение|ВидСравнения)$/; const dcsGroups = {}; const dcsNames = new Set(); for (const f of result.fields) { const m = f.name.match(dcsRe); if (!m) continue; if (!dcsGroups[m[1]]) dcsGroups[m[1]] = { _n: parseInt(m[2]) }; dcsGroups[m[1]][m[3]] = f; dcsNames.add(f.name); } const dcsEntries = Object.entries(dcsGroups).sort((a, b) => a[1]._n - b[1]._n); if (dcsEntries.length) { result.reportSettings = dcsEntries.map(([, g]) => { const cb = g['Использование']; const val = g['Значение']; if (!cb && !val) return null; // No checkbox present (class="staticText" instead of .checkbox) — setting is always enabled const label = (val?.label || cb?.label || val?.name || cb?.name || '').replace(/:$/, '').trim(); const s = { name: label, enabled: cb ? !!cb.value : true }; if (val) { s.value = val.value || ''; if (val.actions && val.actions.length) s.actions = val.actions; } return s; }).filter(Boolean); result.fields = result.fields.filter(f => !dcsNames.has(f.name)); if (!result.fields.length) delete result.fields; } } return result; }`;