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

Разметка страниц

Где ляжет разрыв — единственный метод SDK, который спрашивает и ничего не меняет; саму печать делает хост, и это граница, а не пробел.

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

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

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

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="page-breaks" 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>compute_page_break_layout</code> — единственная операция семьи, которая ничего не
меняет, а СПРАШИВАЕТ: книга после вопроса та же, ответ — только геометрия. Бумагу,
ориентацию, масштаб и поля называет вызывающий; где именно режется лист и сколько выйдет
страниц — считает тот, кто стоит за швом.
<b>САМУ ПЕЧАТЬ ДЕЛАЕТ ХОСТ</b>: своего окна печати мы не рисуем и PDF не собираем — это не
пробел, а граница. У Electron она своя, у встраивающего в WebView своя.
</div>
</header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-print-panel>
<div class="flex flex-wrap items-end gap-2">
<!-- Подписи — Excel для macOS, окно «Параметры страницы», вкладка
«Страница» (те же строки, что у нашего окна). -->
<label class="form-control">
<span class="label-text text-xs">Размер бумаги:</span>
<select data-print-paper class="select select-sm select-bordered">
<option value="1">Письмо US</option>
<option value="9" selected>A4</option>
<option value="5">Legal</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-xs">Ориентация</span>
<select data-print-orientation class="select select-sm select-bordered">
<option value="portrait" selected>книжная</option>
<option value="landscape">альбомная</option>
</select>
</label>
<label class="form-control">
<span class="label-text text-xs">Установить:</span>
<input data-print-scale type="number" min="10" max="400" value="100" class="input input-sm input-bordered w-24" />
</label>
<button data-print-ask class="btn btn-sm btn-primary">Спросить разметку</button>
</div>
<div data-print-answer class="text-xs opacity-60">ещё не спрашивали</div>
<div data-print-breaks 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 { DEFAULT_PAGE_SETUP } from "@f1/sheets";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* РАЗМЕТКА СТРАНИЦ ЧЕРЕЗ SDK — ЕДИНСТВЕННЫЙ МЕТОД, КОТОРЫЙ СПРАШИВАЕТ.
*
* `getPageBreaksAsync()` книгу не меняет: ответ — только геометрия. Это то
* место, где соблазн посчитать самому больше всего: ширины столбцов и высоты
* строк лежат прямо перед глазами. Считать их здесь нельзя — второй расчёт
* разойдётся с печатью ровно на ширину одного столбца, и разойдётся МОЛЧА
* (инвариант №1).
*
* ПЕЧАТЬ ЖЕ ДЕЛАЕТ ХОЗЯИН ОКНА. Своего диалога печати продукт не рисует и PDF не
* собирает: напечатать лист может только тот, кто умеет нарисовать его
* постранично. Кнопки «Печать», которая ничего не печатает, здесь нет —
* нарисованная, она была бы ровно тем fake-green, который запрещён инвариантом
* №3. Страница показывает то, что ПРАВДА есть: числа ядра, из которых печать и
* собирается.
*
* УМОЛЧАНИЯ НЕ НАШИ: не назвали бумагу — уходит `DEFAULT_PAGE_SETUP` из
* `@f1/sheets` (письмо, книжная, 100 %, поля 0,7/0,75″ — те же, что у оболочки
* f1). Придумать здесь свои числа значило бы печатать не так, как продукт.
*/
const { locale } = hostAsks();
const paper = document.querySelector<HTMLSelectElement>("[data-print-paper]")!;
const orientation = document.querySelector<HTMLSelectElement>("[data-print-orientation]")!;
const scale = document.querySelector<HTMLInputElement>("[data-print-scale]")!;
const answer = document.querySelector<HTMLElement>("[data-print-answer]")!;
const breaks = document.querySelector<HTMLElement>("[data-print-breaks]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
function ask(): void {
void sheet
.getPageBreaksAsync({
pageSetup: {
...DEFAULT_PAGE_SETUP,
paperSize: Number(paper.value),
orientationLandscape: orientation.value === "landscape",
scale: Number(scale.value),
},
/* «Разрывов руками не ставили» — это ПУСТОЙ список, а не отсутствие поля:
так требует контракт, и молчание он читал бы иначе. */
manualRowBreaks: [],
manualColBreaks: [],
})
.then((layout) => {
if (!layout.ok) {
answer.className = "text-xs text-error";
answer.setAttribute("data-print-refusal", layout.code ?? "refused");
answer.textContent = `Разметка не получена: ${layout.reason}${layout.code ? ` (${layout.code})` : ""}`;
return;
}
answer.removeAttribute("data-print-refusal");
answer.className = "text-xs opacity-70";
answer.setAttribute("data-print-pages", String(layout.pageCount));
answer.textContent =
`Посчитано ${EXECUTOR}: страниц ${layout.pageCount} ` +
`(${layout.pagesAcross} в ширину × ${layout.pagesDown} в высоту) ` +
`на занятой области до строки ${layout.usedRowEnd + 1} и столбца ${layout.usedColEnd + 1}.`;
/* Разрыв «перед строкой N» — и вид разрыва тоже слово ядра: `automatic`
поставило оно само, `manual` — человек. Смешать их значит показать
пользователю его собственный разрыв там, где его нет. */
breaks.textContent =
`Разрывы строк: ${layout.rowBreaks.map((line) => `перед ${line.position + 1} (${line.kind})`).join(", ") || "нет"}. ` +
`Разрывы столбцов: ${layout.colBreaks.map((line) => `перед ${line.position + 1} (${line.kind})`).join(", ") || "нет"}.`;
});
}
document.querySelector("[data-print-ask]")!.addEventListener("click", ask);
/* Первый вопрос задаётся сам — но только ПОСЛЕ загрузки: докуда считать, знает
ядро (`geometry` его ответа), и без этого честный ответ невозможен. */
void core.f1.loadAsync().then(ask);
import.meta.hot?.dispose(() => core.destroy());

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

src/book.ts
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/**
* КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА.
*
* Книга нарочно шире и длиннее одной страницы: на листе в две ячейки разговор о
* разрывах не состоится. Занятую область объявляет ФИКСТУРА (`usedRange`), а не
* размер загруженного окна: окно — это сколько мы попросили показать, и выдать
* его за размер книги значило бы напечатать экран вместо документа.
*
* ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ: умолчание — заглушка,
* `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется.
*/
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", {
sheets: ["Лист1"],
usedRange: { rowEnd: 199, colEnd: 25 },
cellAt: (row, col) => (col === 0 ? `строка ${row + 1}` : `${(row + 1) * (col + 1)}`),
});
export const transport = createTransport(setting);
/**
* КТО СЧИТАЕТ РАЗМЕТКУ — и почему это написано рядом с транспортом. Ширины
* столбцов, высоты строк, поля и масштаб знает ядро; второй расчёт в TypeScript
* разошёлся бы с печатью ровно на ширину столбца. Подпись обязана называть
* исполнителя правдиво и на заглушке, и на живом ядре.
*/
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-page-breaks",
"private": true,
"type": "module",
"description": "Разметка страниц: где ляжет разрыв — считает ядро, а печать делает хост.",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"dependencies": {
"@f1/preset-sheets-core": "*",
"@f1/sheets": "*",
"@f1/contract": "*",
"@f1/i18n": "*"
},
"devDependencies": {
"vite": "^7.0.0"
}
}

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

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