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

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!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="protected-ranges" 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>protected_ranges_permissions</code>: перечислить, создать, изменить, удалить, снять замок на сеанс и вернуть его. Пароль уходит ЗА ШОВ и обратно не приходит — возвращается только СПОСОБ защиты. Своего списка закрытых мест у оболочки нет: он принадлежит листу. </div> </header>
<section class="m-4 flex flex-col gap-3 rounded-card border border-base-300 p-4" data-ranges-panel> <!-- Подписи полей — Excel для macOS, окно «Разрешить изменение диапазонов» (те же строки ресурсов, что у нашего окна: `strman_0014.strings`, раздел «Новый диапазон»). --> <div class="flex flex-wrap items-end gap-2"> <label class="form-control"> <span class="label-text text-xs">Название диапазона:</span> <input data-ranges-name value="Оклады" class="input input-sm input-bordered w-40" /> </label> <label class="form-control"> <span class="label-text text-xs">Диапазон ячеек:</span> <input data-ranges-sqref value="C2:C4" class="input input-sm input-bordered w-32" /> </label> <label class="form-control"> <span class="label-text text-xs">Пароль диапазона:</span> <input data-ranges-password type="password" value="секрет" class="input input-sm input-bordered w-32" /> </label> <button data-ranges-create class="btn btn-sm btn-primary">Создать</button> <button data-ranges-unlock class="btn btn-sm">Снять замок</button> <button data-ranges-lock class="btn btn-sm">Вернуть замок</button> <button data-ranges-delete class="btn btn-sm">Удалить</button> </div>
<div data-ranges-answer class="text-xs opacity-60">ещё не спрашивали</div> <div data-ranges-list class="flex flex-col gap-1 text-xs"></div>
<p class="text-xs opacity-60"> Подсветки закрытых мест на самом ЛИСТЕ здесь нет: она принадлежит сетке, и её срез ещё не сделан. Нарисовать её страницей значило бы показать несделанное сделанным. </p> </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>Точка входа: здесь лист создаётся и сюда же приходят события.
import { createSheetsCore } from "@f1/preset-sheets-core";import type { ProtectedRange, ProtectedRangesResult } from "@f1/facade";import { EXECUTOR, transport } from "./book.js";import { hostAsks } from "./host.js";import "./styles.css";
/** * ПРАВА НА ДИАПАЗОН ЧЕРЕЗ SDK — ШЕСТЬ НАМЕРЕНИЙ ОДНОЙ ОПЕРАЦИИ. * * Ответ на чужую страницу «Hide Permission Background Shadow»: у нас закрытые * места объявляются и разбираются по-настоящему, а вот подсветки их фона на * листе нет — она принадлежит сетке, и это сказано на самой странице, а не * замолчано. * * ЧТО ЗДЕСЬ ГЛАВНОЕ И ЧЕГО НЕ ВИДНО В ЧУЖИХ ПРИМЕРАХ: * * · ПАРОЛЬ УХОДИТ ЗА ШОВ И НЕ ВОЗВРАЩАЕТСЯ. Обратно приходит `accessKind` — * способ защиты, — и `unlocked`. Держать пароль в оболочке значило бы завести * вторую правду о защите ровно там, где она опаснее всего; * · НОМЕР СОСТОЯНИЯ БЕРЁТСЯ ИЗ ОТВЕТА и уходит ВНУТРИ намерения: правка по * устаревшему списку отвергается ядром, и это защита от «двое правят одно». * Фасад ведёт этот номер сам — здесь его нет ни в одной строке; * · ОТКАЗ — ЗНАЧЕНИЕ, а не исключение: имя занято, пароль не тот, листа нет. * Код ядра показывается человеку рядом со словами. */const { locale } = hostAsks();
const name = document.querySelector<HTMLInputElement>("[data-ranges-name]")!;const sqref = document.querySelector<HTMLInputElement>("[data-ranges-sqref]")!;const password = document.querySelector<HTMLInputElement>("[data-ranges-password]")!;const answer = document.querySelector<HTMLElement>("[data-ranges-answer]")!;const list = document.querySelector<HTMLElement>("[data-ranges-list]")!;
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale });const workbook = core.f1.getActiveWorkbook();const sheetKey = workbook.getActiveSheet()!.getName();
let current: ProtectedRange | null = null;
const ACCESS: Readonly<Record<string, string>> = { open: "Изменение диапазона без пароля", password: "Защищён паролем", domain_permission: "Права домена", irm: "Управление правами (IRM)",};
function show(said: ProtectedRangesResult, what: string): void { if (!said.ok) { answer.className = "text-xs text-error"; answer.setAttribute("data-ranges-refusal", said.code ?? "refused"); answer.textContent = `${what}: отказ — ${said.reason}${said.code ? ` (${said.code})` : ""}`; return; } answer.removeAttribute("data-ranges-refusal"); answer.className = "text-xs opacity-70"; answer.setAttribute("data-ranges-status", said.status); answer.textContent = `${what} — сделано ${EXECUTOR}; ответ «${said.status}», диапазонов ${said.entries.length}, ` + `открыто на сеанс ${said.unlockedCount}, номер состояния ${said.revision}.`;
current = said.entries.at(-1) ?? null; list.replaceChildren( ...said.entries.map((entry) => { const row = document.createElement("div"); row.setAttribute("data-ranges-item", entry.name); row.className = "rounded border border-base-300 px-2 py-1"; row.textContent = `${entry.name} · ${entry.sqref} · ${ACCESS[entry.accessKind] ?? entry.accessKind}` + `${entry.unlocked ? " · замок снят на сеанс" : " · заперт"}`; return row; }), ); /* ПУСТОЙ СПИСОК — УТВЕРЖДЕНИЕ ЯДРА, а не молчание страницы. */ if (!said.entries.length) { const empty = document.createElement("div"); empty.setAttribute("data-ranges-empty", ""); empty.className = "opacity-60"; empty.textContent = "На этом листе нет диапазонов, изменение которых разрешено паролем."; list.replaceChildren(empty); }}
document.querySelector("[data-ranges-create]")!.addEventListener("click", () => { void workbook .createProtectedRangeAsync(sheetKey, { name: name.value, /* Ссылка уходит КАК ЕЁ НАПИСАЛ человек: разбирает её ядро, не мы. */ sqref: sqref.value, ...(password.value ? { password: password.value } : {}), }) .then((said) => show(said, "Создать"));});
document.querySelector("[data-ranges-unlock]")!.addEventListener("click", () => { if (!current) return; void workbook .unlockProtectedRangeAsync(sheetKey, current.id, password.value) .then((said) => show(said, "Снять замок"));});
document.querySelector("[data-ranges-lock]")!.addEventListener("click", () => { if (!current) return; void workbook.lockProtectedRangeAsync(sheetKey, current.id).then((said) => show(said, "Вернуть замок"));});
document.querySelector("[data-ranges-delete]")!.addEventListener("click", () => { if (!current) return; void workbook.deleteProtectedRangeAsync(sheetKey, current.id).then((said) => show(said, "Удалить"));});
/* ПЕРВЫЙ ВОПРОС — ИМЕННО ВОПРОС: `list` книгу не меняет и не запирается даже в режиме чтения (иначе окно, объясняющее отказ, само отказывало бы). */void core.f1 .loadAsync() .then(() => workbook.getProtectedRangesAsync(sheetKey).then((said) => show(said, "Список диапазонов")));
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createTransport, transportExecutorName, transportSettingFromSearch } from "@f1/contract";
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * В книге есть колонка, которую и закрывают в жизни: зарплаты. Права на * диапазон при этом принадлежат ЛИСТУ, то есть ядру, — в оболочке второго * списка нет и быть не может. * * ТРАНСПОРТ ВЫБИРАЕТСЯ НАСТРОЙКОЙ: умолчание — заглушка, * `?core=http://127.0.0.1:5507` уводит на живое ядро. Код примера не меняется. */const TABLE: readonly (readonly string[])[] = [ ["Сотрудник", "Отдел", "Оклад"], ["Иванова", "Продажи", "180 000"], ["Петров", "Логистика", "145 000"], ["Сидорова", "Финансы", "210 000"],];
const setting = transportSettingFromSearch(globalThis.location?.search ?? "", { sheets: ["Лист1"], usedRange: { rowEnd: 3, colEnd: 2 }, cellAt: (row, col) => TABLE[row]?.[col],});
export const transport = createTransport(setting);
/** * КТО РЕШАЕТ, ЧТО ЗАКРЫТО, — и почему это написано рядом с транспортом. Пароль * остаётся у ядра: обратно приходит только СПОСОБ защиты. Подпись под ответом * обязана называть исполнителя правдиво и на заглушке, и на живом ядре. */export const EXECUTOR = transportExecutorName(setting);Сторона хозяина страницы: что приложение отвечает листу и что от него получает.
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 };}Оформление страницы вокруг листа.
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ. Собственное оформление лист приносит в теневые корни сам, но утилиты раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл замера у сетки без размеров). У потребителя вне репозитория это `@fastoffices/formula-kit` из его node_modules. */@import "@fastoffices/formula-kit"; /* см. примечание выше */html,body { height: 100%; margin: 0;}Из каких пакетов собран пример.
{ "name": "f1-example-protected-ranges", "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" }}- Остальные примеры — Примеры F1.
- Из чего собирается лист — Архитектура.
- Что можно вызвать сегодня — справочник Compute Contract.