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

Своё событие

Две точки расширения наружу: onCommand отдаёт любое намерение поверхности (наше и чужое), onGesture — цену закончившегося жеста числами.

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

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

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

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="custom-event" 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>onCommand</code> отдаёт ЛЮБОЕ намерение поверхности — и наше (<code>clipboard.copy</code>),
и чужое (<code>ext.acme.ping</code>): своё событие встраивающий получает так, объявив свой
пункт или сочетание. <code>onGesture</code> отдаёт цену закончившегося жеста числами.
Событие ничего не применяет и ничего не требует — решает хозяин (инвариант №5).
Не подписались — не платите ничем: ни таймера, ни подписки не заводится.
</div>
</header>
<section class="m-4 flex flex-col gap-2 rounded-card border border-base-300 p-4" data-event-panel>
<div class="text-xs opacity-70">
Чтобы журнал заполнился: щёлкните <b>правой кнопкой</b> по ячейке и выберите пункт;
нажмите <b>Ctrl+Alt+H</b> — это ЧУЖОЕ сочетание, объявленное этой страницей; прокрутите
лист колесом — придёт цена жеста.
</div>
<pre data-event-log class="min-h-24 overflow-auto rounded bg-base-200 p-2 font-mono text-xs"></pre>
<div data-event-executor class="text-xs opacity-60"></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 { defineExtension } from "@f1/extend";
import { EXECUTOR, transport } from "./book.js";
import { hostAsks } from "./host.js";
import "./styles.css";
/**
* СВОЁ СОБЫТИЕ — ДВЕ ТОЧКИ РАСШИРЕНИЯ, И НИ ОДНОЙ ВЫДУМАННОЙ.
*
* Чужие SDK на этом месте заводят свою систему событий: подписки, имена,
* жизненный цикл. Здесь её нет и не будет — событий ровно два, и оба уже есть у
* продукта:
*
* · `onCommand` — ШИНА КОМАНД, объявленная наружу. Сюда приходит КАЖДОЕ
* намерение поверхности: наше из реестра и собственное встраивающего с
* приставкой `ext.` Приставку ставим МЫ, поэтому чужая команда не может
* назваться нашей — и реестр отвечает о ней своим словом (`foreign`), а не
* молчанием о нашей;
* · `onGesture` — ЦЕНА ЗАКОНЧИВШЕГОСЯ ЖЕСТА: род, длительность, кадры и
* сколько раз жест попросил новое окно листа. Не подписались — не меряется
* ничего: платит только тот, кто подписался.
*
* ПОЧЕМУ СОБЫТИЕ НИЧЕГО НЕ ПРИМЕНЯЕТ. Компонент поднимает намерение, а решает
* хозяин (инвариант №5) — именно это делает набор переиспользуемым. Журнал ниже
* поэтому только ПИШЕТ: он не отменяет команду и не подтверждает её.
*
* ЧЕГО НЕТ И ЭТО НАЗВАНО: события «ячейка изменилась». Значение принадлежит
* ядру, и узнать о его изменении можно ровно там, где книга отвечает, — у
* фасада (`onChange` корня SDK). Заводить ради этого третью подписку у сборки
* значило бы завести второй источник правды о документе.
*/
const { locale } = hostAsks();
const log = document.querySelector<HTMLElement>("[data-event-log]")!;
const executor = document.querySelector<HTMLElement>("[data-event-executor]")!;
const lines: string[] = [];
function say(line: string): void {
lines.push(line);
log.textContent = lines.slice(-9).join("\n");
log.setAttribute("data-event-count", String(lines.length));
}
const core = createSheetsCore({
mount: document.getElementById("app")!,
transport,
locale,
/* Чужой пункт и чужое сочетание заведены только затем, чтобы было ЧЬЁ
событие показать: своё событие встраивающий получает именно так. */
extend: defineExtension({
owner: "acme",
title: "Acme",
items: [{ menu: "cell-context", id: "ping", label: "Событие в Acme", run: () => say("← его обработчик: ping") }],
shortcuts: [{ keys: "Ctrl+Alt+H", id: "hello", run: () => say("← его обработчик: hello") }],
}),
/* СЮДА ПРИХОДИТ ВСЁ: и наши команды, и его. Различить их можно по приставке,
и это единственное, что страница делает с именем команды. */
onCommand: (command) => say(`${command.startsWith("ext.") ? "чужая" : "наша"} команда · ${command}`),
/* ЖЕСТ ЗАКОНЧИЛСЯ И ВОТ ЕГО ЦЕНА. `cost` приходит `null`, когда кадров было
слишком мало, чтобы о цене говорить: ноль читался бы как «быстро», и это
была бы ложь ровно там, где замера не было. */
onGesture: (gesture) =>
say(
`жест «${gesture.kind}» · ${Math.round(gesture.durationMs)} мс · ` +
`${gesture.cost ? `кадр p50 ${gesture.cost.p50Ms} мс, p95 ${gesture.cost.p95Ms} мс` : "кадров мало для счёта"} · ` +
`окон запрошено ${gesture.windowRequests}`,
),
});
executor.textContent = `Запросы за швом контракта исполняются ${EXECUTOR}: подписки от этого не зависят — они свойство сборки.`;
import.meta.hot?.dispose(() => core.destroy());

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

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

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

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