Перейти к содержимому
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="annotations" 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>review_annotations_lifecycle</code> и поле <code>intent</code>, которым они
различаются. Текст, номер записи, автора, нормализованный адрес и время правки называет
тот, кто стоит за швом: оболочка не сочиняет ничего и примечаний не хранит. До первого
вопроса список пуст — это «не спрашивали», а не «их нет».
</div>
</header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-notes-panel>
<div class="flex flex-wrap items-end gap-2">
<label class="form-control">
<span class="label-text text-xs">Ячейка</span>
<input data-notes-cell value="b3" class="input input-sm input-bordered w-24" />
</label>
<label class="form-control grow">
<span class="label-text text-xs">Текст примечания</span>
<input
data-notes-text
value="Курс уточнить у поставщика"
class="input input-sm input-bordered w-full"
/>
</label>
<!-- Подписи кнопок — Excel для macOS, вкладка «Рецензирование», группа
«Примечания» (те же строки, что у нашей ленты: `review-groups.ts`). -->
<button data-notes-new class="btn btn-sm btn-primary">Создать примечание</button>
<button data-notes-delete class="btn btn-sm">Удалить</button>
<button data-notes-show-hide class="btn btn-sm">Показать или скрыть примечание</button>
<button data-notes-show-all class="btn btn-sm">Показать все примечания</button>
</div>
<!-- Расписка о последнем ответе: чей он и что в нём. До первого вопроса
здесь честное «ещё не спрашивали», а не пустой список, притворившийся
ответом. -->
<div data-notes-answer class="text-xs opacity-60">ещё не спрашивали</div>
<div data-notes-list class="flex flex-col gap-1 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 { AnnotationsResult, SheetAnnotation } from "@f1/facade";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* ПРИМЕЧАНИЯ ЧЕРЕЗ SDK — ШЕСТЬ ПУНКТОВ EXCEL И ОДНА ОПЕРАЦИЯ КОНТРАКТА.
*
* Показывает то, что при первом знакомстве обычно понимают неправильно: у
* примечаний НЕТ шести методов контракта. Есть одна операция
* (`review_annotations_lifecycle`) и намерение внутри неё, а разными их делают
* подписи Excel: у операции шесть действий — перечислить, создать или
* изменить, удалить, ответить, отменить, повторить, — и под каждой кнопкой
* этой страницы лежит один и тот же путь, меняется только намерение.
*
* ЧЕГО СТРАНИЦА НЕ ДЕЛАЕТ. Она не хранит примечаний и не сочиняет их полей:
* номер записи, автора, время правки и НОРМАЛИЗОВАННЫЙ адрес («b3» → «B3»)
* присылает тот, кто стоит за швом. Список ниже — это последний его ответ, а не
* наша память: инвариант №1 звучит именно так, а не «мы стараемся».
*
* ЖЁЛТУЮ КОРОБКУ У ЯЧЕЙКИ РИСУЕТ ЛИСТ, и в этом примере её на листе не
* появляется: слой примечаний принадлежит сетке, а его срез ещё не сделан.
* Обещать её здесь картинкой было бы fake-green; страница показывает ровно то,
* что уже есть, — ответ ядра целиком.
*/
const { locale } = hostAsks();
const cell = document.querySelector<HTMLInputElement>("[data-notes-cell]")!;
const text = document.querySelector<HTMLInputElement>("[data-notes-text]")!;
const answer = document.querySelector<HTMLElement>("[data-notes-answer]")!;
const list = document.querySelector<HTMLElement>("[data-notes-list]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });
const sheet = core.f1.getActiveWorkbook().getActiveSheet()!;
let current: SheetAnnotation | null = null;
/**
* ОДНА РАСПИСКА НА ВСЕ ШЕСТЬ КНОПОК: кто ответил, что он сделал и что теперь
* знает. Отказ — тоже ответ, и он показывается словами и кодом ЯДРА.
*/
function show(said: AnnotationsResult, what: string): void {
if (!said.ok) {
answer.className = "text-xs text-error";
answer.setAttribute("data-notes-refusal", said.code ?? "refused");
answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`;
return;
}
answer.removeAttribute("data-notes-refusal");
answer.className = "text-xs opacity-70";
answer.setAttribute("data-notes-status", said.status);
answer.textContent = `${what} — сделано ${EXECUTOR}; ответ «${said.status}», записей ${said.annotations.length}.`;
current = said.annotations.at(-1) ?? null;
list.replaceChildren(
...said.annotations.map((note) => {
const row = document.createElement("div");
row.setAttribute("data-notes-item", note.cellRef);
row.className = "rounded border border-base-300 px-2 py-1";
/* Поля перечислены поимённо и без домысла: показывается то, что прислало
ядро, — включая вид записи и видимость, у которой смысл есть только у
заметки (у обсуждения её нет вовсе). */
row.textContent =
`${note.cellRef} · ${note.author} · «${note.text}» · ${note.kind}` +
`${note.visible === undefined ? "" : note.visible ? " · видна" : " · скрыта"}`;
return row;
}),
);
}
document.querySelector("[data-notes-new]")!.addEventListener("click", () => {
void sheet
.upsertAnnotationAsync({
cellRef: cell.value,
text: text.value,
/* Автора называет ХОЗЯИН страницы: личность приходит от него, а не
выдумывается в TypeScript (то же правило, что у присутствия). */
author: "Мира",
visible: true,
})
.then((said) => show(said, "Создать примечание"));
});
document.querySelector("[data-notes-delete]")!.addEventListener("click", () => {
if (!current) {
answer.className = "text-xs opacity-70";
answer.textContent = "Удалить нечего: сначала создайте примечание.";
return;
}
void sheet.deleteAnnotationAsync(current.annotationId).then((said) => show(said, "Удалить"));
});
document.querySelector("[data-notes-show-hide]")!.addEventListener("click", () => {
if (!current) {
answer.className = "text-xs opacity-70";
answer.textContent = "Показывать нечего: сначала создайте примечание.";
return;
}
void sheet
.setNoteVisibilityAsync(current.annotationId, current.visible !== true)
.then((said) => show(said, "Показать или скрыть примечание"));
});
document.querySelector("[data-notes-show-all]")!.addEventListener("click", () => {
void sheet.showAllNotesAsync(true).then((said) => show(said, "Показать все примечания"));
});
/* ПЕРВЫЙ ВОПРОС ЗАДАЁТСЯ САМ, и он именно ВОПРОС: `list` ничего не меняет.
Пустой ответ на него — честное «примечаний нет», а не «мы не спрашивали». */
void core.f1.loadAsync().then(() => sheet.listAnnotationsAsync().then((said) => show(said, "Список примечаний")));
import.meta.hot?.dispose(() => core.destroy());

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

src/book.ts
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
const TABLE: readonly (readonly string[])[] = [
["Поставщик", "Курс", "Сумма"],
["Северный", "92,40", "18 480"],
["Восточный", "91,10", "9 110"],
["Южный", "93,75", "28 125"],
];
/**
* КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА.
*
* Транспорт контракта — единственное место, где лист разговаривает с ядром
* (инвариант №1). В продукте здесь стоит `wasm` или мост хоста, и меняется
* РОВНО эта строка.
*
* ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ, А НЕ ВПАЯНА. Умолчание — заглушка;
* `?core=http://127.0.0.1:5507` уводит на живое ядро через мост `@f1/core-host`.
* Ни одна строка примера от этого не меняется: примечания живут на операции
* `review_annotations_lifecycle`, и её исполняет тот, кто стоит за швом.
*/
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", {
sheets: ["Лист1"],
usedRange: { rowEnd: 3, 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-annotations",
"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"
}
}

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

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