Перейти к содержимому
FastOffice

Структура листа

Восемь действий — одна операция: оболочка называет диапазон, ось и действие, а уровни и что спрятать считает ядро.

восемь действий — одна операция: оболочка называет диапазон, ось и действие, а уровни и что спрятать считает ядро
открыть в новой вкладке ↗Работающий пример. Ниже — его код целиком, файл за файлом.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.

Страница, которую открывает браузер.

index.html
<!doctype html>
<!--
Структура листа — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе
целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.
-->
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Структура листа — @f1</title>
</head>
<body>
<div data-example="outline" class="flex h-screen w-screen flex-col bg-base-100 text-base-content">
<header class="border-b border-base-300 px-4 py-2">
<div class="text-sm font-semibold">Структура листа</div>
<div class="text-xs opacity-70">
Вкладка «Данные», группа «Структура». Восемь действий — ОДНА операция контракта
<code>apply_outline_subtotal</code>: оболочка называет диапазон, ось и действие, а какой у
дорожки уровень, что спрятать при сворачивании и сколько уровней всего — считает тот, кто
стоит за швом, и присылает состоянием. Ни одного уровня на этой странице не вычисляется.
</div>
</header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-outline-panel>
<div class="flex flex-wrap items-end gap-2">
<label class="form-control">
<span class="label-text text-xs">Диапазон</span>
<input data-outline-range value="A2:C4" class="input input-sm input-bordered w-28" />
</label>
<!-- Подписи — Excel для macOS, вкладка «Данные», группа «Структура»
(те же строки, что у нашей ленты: `data-groups.ts`). -->
<button data-outline-group class="btn btn-sm btn-primary">Группировать</button>
<button data-outline-ungroup class="btn btn-sm">Разгруппировать</button>
<button data-outline-hide class="btn btn-sm">Скрыть детали</button>
<button data-outline-show class="btn btn-sm">Показать детали</button>
<button data-outline-auto class="btn btn-sm">Создать структуру</button>
<button data-outline-clear class="btn btn-sm">Удалить структуру</button>
</div>
<div data-outline-answer class="text-xs opacity-60">ещё не спрашивали</div>
<div data-outline-state class="text-xs"></div>
</section>
<div id="app" class="flex min-h-0 flex-1 flex-col"></div>
</div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

Точка входа: здесь лист создаётся и сюда же приходят события.

src/main.ts
import { createSheetsCore } from "@f1/preset-sheets-core";
import type { OutlineAction, OutlineResult } from "@f1/facade";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* СТРУКТУРА ЛИСТА ЧЕРЕЗ SDK — ОДНА ОПЕРАЦИЯ НА ВОСЕМЬ ДЕЙСТВИЙ.
*
* Показывает то, что видно только на живом вызове: у структуры НЕТ восьми
* методов. Есть `FRange.outlineAsync(action, options)` и словарь действий
* контракта (`group`, `ungroup`, `collapse`, `expand`, `show_level`,
* `auto_outline`, `clear_outline`, `read_state`). Восемь кнопок Excel — это
* восемь ЗНАЧЕНИЙ одного поля, и заводить под них восемь дорог значило бы выдать
* наше членение за членение ядра.
*
* ЧЕГО СТРАНИЦА НЕ СЧИТАЕТ: уровней, границ групп и того, какие строки прятать.
* Всё это приезжает состоянием (`state.rowLevels`), и показывается как есть.
*
* ЗАКОННЫЙ ВЫЗОВ МОЖЕТ НИЧЕГО НЕ ИЗМЕНИТЬ, и это не отказ: разгруппировать то,
* что не сгруппировано, — законно. Слово об этом говорит ядро (`didChange`), а
* не наш вывод по длине списка.
*/
const { locale } = hostAsks();
const range = document.querySelector<HTMLInputElement>("[data-outline-range]")!;
const answer = document.querySelector<HTMLElement>("[data-outline-answer]")!;
const state = document.querySelector<HTMLElement>("[data-outline-state]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
function show(said: OutlineResult, what: string): void {
if (!said.ok) {
answer.className = "text-xs text-error";
answer.setAttribute("data-outline-refusal", said.code ?? "refused");
answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`;
return;
}
answer.removeAttribute("data-outline-refusal");
answer.className = "text-xs opacity-70";
answer.setAttribute("data-outline-changed", String(said.didChange));
answer.textContent =
`${what} — сделано ${EXECUTOR}; лист ${said.didChange ? "изменился" : "не изменился (законный вызов, менять было нечего)"}.`;
const rows = said.state.rowLevels;
state.setAttribute("data-outline-max-level", String(said.state.maxRowLevel));
state.textContent = rows.length
? `Уровни строк (счёт ЯДРА, максимум ${said.state.maxRowLevel}): ` +
rows
.map((track) => `${track.index + 1}→${track.level}${track.collapsed ? " свёрнута" : ""}${track.hidden ? " скрыта" : ""}`)
.join(", ")
: "Структуры на листе нет — ни одной дорожки с уровнем.";
}
function ask(action: OutlineAction, what: string): void {
const area = sheet.getRange(range.value);
if (!area) {
answer.className = "text-xs text-error";
answer.textContent = `${what}: «${range.value}» — не адрес диапазона`;
return;
}
void area.outlineAsync(action, { axis: "row" }).then((said) => show(said, what));
}
document.querySelector("[data-outline-group]")!.addEventListener("click", () => ask("group", "Группировать"));
document.querySelector("[data-outline-ungroup]")!.addEventListener("click", () => ask("ungroup", "Разгруппировать"));
document.querySelector("[data-outline-hide]")!.addEventListener("click", () => ask("collapse", "Скрыть детали"));
document.querySelector("[data-outline-show]")!.addEventListener("click", () => ask("expand", "Показать детали"));
document.querySelector("[data-outline-auto]")!.addEventListener("click", () => ask("auto_outline", "Создать структуру"));
document.querySelector("[data-outline-clear]")!.addEventListener("click", () => ask("clear_outline", "Удалить структуру"));
/* ПЕРВЫЙ ВОПРОС — ИМЕННО ВОПРОС: `read_state` ничего не меняет, и пустой ответ
на него честно означает «структуры нет». */
void core.f1.loadAsync().then(() => ask("read_state", "Состояние структуры"));
import.meta.hot?.dispose(() => core.destroy());

Книга примера: данные, с которыми лист открывается.

src/book.ts
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/**
* КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА.
*
* Отчёт по кварталам: три строки данных и строка итога — то, что в Excel и
* группируют. Ни одного уровня структуры здесь нет: уровни считает тот, кто
* стоит за швом, и присылает состоянием (инвариант №1).
*
* ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ, А НЕ ВПАЯНА: умолчание — заглушка,
* `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется.
*/
const TABLE: readonly (readonly string[])[] = [
["Квартал", "Регион", "Выручка"],
["I", "Север", "18 480"],
["I", "Восток", "9 110"],
["I", "Юг", "28 125"],
["Итог I", "", "55 715"],
];
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", {
sheets: ["Лист1"],
usedRange: { rowEnd: 4, colEnd: 2 },
cellAt: (row, col) => TABLE[row]?.[col],
});
export const transport = createTransport(setting);
/**
* КТО СЧИТАЕТ УРОВНИ — и почему это написано рядом с транспортом. Подпись «сделано
* ядром» там, где отвечает заглушка, — fake-green в словах (инвариант №3). Имя
* исполнителя приходит от САМОГО транспорта и остаётся правдой на живом ядре.
*/
export const EXECUTOR = transportExecutorName(setting);

Сторона хозяина страницы: что приложение отвечает листу и что от него получает.

src/host.ts
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/**
* ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык.
*
* У встраивающего это уже есть своё: тему он знает из настроек, язык — из
* профиля пользователя. Здесь и то и другое читается из адреса
* (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было
* дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком,
* недоступна ни ссылке, ни автоматической проверке.
*
* ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не
* лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает
* СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном.
*
* НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром
* страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от
* арабского языка интерфейса не переворачивается. Это не пропуск, а решение
* (`locale.ts`).
*/
export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } {
const params = new URLSearchParams(location.search);
const theme = params.get("theme") === "dark" ? "dark" : "light";
const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme);
/* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */
setLocale(locale);
applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };
}

Оформление страницы вокруг листа.

src/styles.css
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ.
Собственное оформление лист приносит в теневые корни сам, но утилиты
раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без
него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл
замера у сетки без размеров). У потребителя вне репозитория это
`@fastoffices/formula-kit` из его node_modules. */
@import "@fastoffices/formula-kit"; /* см. примечание выше */
html,
body {
height: 100%;
margin: 0;
}

Из каких пакетов собран пример.

package.json
{
"name": "f1-example-outline",
"private": true,
"type": "module",
"description": "Структура листа: восемь действий — одна операция контракта, а уровни считает ядро.",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@f1/preset-sheets-core": "*",
"@f1/facade": "*",
"@f1/contract": "*",
"@f1/i18n": "*"
},
"devDependencies": {
"vite": "^7.0.0"
}
}

Ассистент документации

Ответ собран из документации и может быть неточным — сверяйтесь с источниками.