nuvra 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # nuvra
2
2
 
3
3
  Word-style document editor for Vue 3. A real page view with paper sizes and margins, a lighter web view for
4
- forms, tables, images, lists, find & replace, comments, HTML source mode, printing, export to HTML and Word files
5
- (`.docx`) you can open again.
4
+ forms, tables, images, lists, find & replace, comments, HTML source mode, printing, PDF download, export to HTML and Word
5
+ files (`.docx`) you can open again.
6
6
 
7
7
  **[Documentation and live demo →](https://nuvra-docs.vercel.app)**
8
8
 
9
9
  - No editor framework underneath: its own small editing engine with a sanitizing schema.
10
10
  - No UI framework either: native HTML controls, built-in SVG icons and plain CSS variables. Vue is the only
11
11
  dependency.
12
- - Pagination in the page view (A4, A5, Letter, …, portrait or landscape).
12
+ - Pagination in the page view (A4, A5, Letter, …, portrait or landscape), with section breaks for landscape pages inside a
13
+ portrait document.
13
14
  - Tables with merge / split, images with resize and alignment, task lists, links, colors, fonts.
14
15
  - Undo / redo, keyboard shortcuts that work with non-Latin keyboard layouts, Markdown-like input rules.
15
16
  - Office tools: format painter, letter case, paragraph spacing, formatting marks, a right-click menu and
@@ -18,10 +19,12 @@ forms, tables, images, lists, find & replace, comments, HTML source mode, printi
18
19
  the editor shows them, and the Word export uses Word's own header, footer and page fields.
19
20
  - A ruler above the page for the margins and for the first line, left and right indents of a paragraph.
20
21
  - A page watermark such as DRAFT or COPY, drawn behind the text of every page and carried into print, HTML and Word.
21
- - Real `.docx` export and import, multilevel (1.1.1) numbering, a table of contents and a navigation pane.
22
+ - Real `.docx` export and import, PDF download without the print dialog, multilevel (1.1.1) numbering, a table of
23
+ contents and a navigation pane with headings and page thumbnails.
22
24
  - Footnotes at the bottom of their page, carried into print and Word.
23
25
  - Comments with replies, tracked changes that round-trip with Word, a comparison of two versions, and templates
24
26
  filled in as a form.
27
+ - Hooks for editing together: other people's carets and selections, your selection as character positions.
25
28
  - A `/` command menu and a toolbar slot for your own commands and buttons.
26
29
 
27
30
  ## Installation
@@ -103,6 +106,7 @@ const uploadImage: DocumentImageUploadHandler = async file => {
103
106
  | `author` | `string` | `''` | Name written on new comments, replies and tracked changes. |
104
107
  | `autofocus` | `boolean` | `false` | Places the caret at the end of the document once ready. |
105
108
  | `canvasPadding` | `number \| string` | `50` | Gray space around the page or web sheet. |
109
+ | `collaborators` | `Collaborator[]` | `[]` | Other people editing the document; their carets and selections are drawn. |
106
110
  | `defaultViewMode` | `'page' \| 'web'` | `'page'` | View shown first. |
107
111
  | `disabled` | `boolean` | `false` | Read-only document, disabled controls. |
108
112
  | `height` | `number \| string` | `760` | Height of the editor, or `'auto'` to grow between `minHeight`/`maxHeight`. |
@@ -119,18 +123,20 @@ const uploadImage: DocumentImageUploadHandler = async file => {
119
123
  | `variables` | `TemplateVariable[]` | `[]` | Template variables the user can insert. |
120
124
 
121
125
  `Editor` accepts the same props except `v-model:page`, `v-model:comments`, `v-model:trackChanges`, `author`,
122
- `defaultViewMode`, `height`, `ruler`, `slashCommands` and `title`.
126
+ `collaborators`, `defaultViewMode`, `height`, `ruler`, `slashCommands` and `title`.
123
127
 
124
128
  Numbers are pixels; strings are used as CSS lengths (`'100%'`, `'50vh'`).
125
129
 
126
130
  ### Events
127
131
 
128
- | Event | Payload | Description |
129
- | ------------- | ---------------- | ------------------------------------------------- |
130
- | `focus` | — | The document received focus. |
131
- | `blur` | — | The document lost focus; the model is up to date. |
132
- | `uploadError` | `error: unknown` | An image was rejected or its upload failed. |
133
- | `importError` | `error: unknown` | A Word file could not be read. |
132
+ | Event | Payload | Description |
133
+ | ----------------- | ----------------------------------- | -------------------------------------------------------------- |
134
+ | `focus` | — | The document received focus. |
135
+ | `blur` | — | The document lost focus; the model is up to date. |
136
+ | `uploadError` | `error: unknown` | An image was rejected or its upload failed. |
137
+ | `importError` | `error: unknown` | A Word file could not be read. |
138
+ | `exportError` | `error: unknown` | The PDF could not be drawn; no file is downloaded. |
139
+ | `selectionChange` | `selection: SelectionOffsets \| null` | The caret or selection moved; `null` when it left the document. |
134
140
 
135
141
  ### Exposed methods (`DocumentEditor` ref)
136
142
 
@@ -144,6 +150,7 @@ Numbers are pixels; strings are used as CSS lengths (`'100%'`, `'50vh'`).
144
150
  | `print()` | Opens the browser print dialog. |
145
151
  | `exportHtml()` | Downloads the document as an HTML page. |
146
152
  | `exportWord()` | Downloads the document as a Word file (`.docx`). |
153
+ | `exportPdf()` | Downloads the document as a PDF drawn from its pages. |
147
154
  | `engine` | The editing engine (`DocumentEngine`), for advanced integrations. |
148
155
 
149
156
  ### Keyboard shortcuts
@@ -210,8 +217,25 @@ const submit = () => {
210
217
  footers with page fields, the watermark, lists, tables with merged cells, images, footnotes and tracked changes as
211
218
  Word revisions. "Open Word file (.docx)" (or `importWord(file)`) loads one into the editor, footnotes and revisions
212
219
  included; a file that cannot be read emits `importError`. `buildDocx` and `readDocx` do the same in your own code. A
213
- document with several sections gets the page setup of its last section, and the content of a different first page
214
- header is not imported.
220
+ document with several sections takes the page setup of its first section, and every further section starts with a
221
+ section break; the content of a different first page header is not imported.
222
+
223
+ ### PDF download
224
+
225
+ "Download as PDF" in the "More" menu, or `exportPdf()`, saves a PDF without the print dialog. Every sheet is drawn into a
226
+ picture (SVG `foreignObject` → canvas → JPEG) and written into the PDF, so it looks like the printout, but its text
227
+ cannot be selected or searched. Images from servers without CORS are left out, and in the web view the editor switches to
228
+ the page view for the moment of the export. A PDF that cannot be drawn emits `exportError`; printing with "Save as PDF"
229
+ still gives a PDF with real text.
230
+
231
+ ### Pages in different orientations
232
+
233
+ "Section break: landscape pages" and "Section break: portrait pages" in the insert menu and the `/` menu (or
234
+ `engine.insertSectionBreak('landscape')`) turn the pages after the break; paper size and margins stay, and the break
235
+ starts a new page. It is saved as `<div data-type="section-break" data-orientation="landscape"></div>`. The page view
236
+ draws sheets of different sizes and gives the blocks of a turned section their sheet's text width; printing uses a named
237
+ `@page` for turned sheets, the PDF has turned pages, and the Word export writes every section as a Word section with its
238
+ own orientation.
215
239
 
216
240
  ### Footnotes
217
241
 
@@ -225,7 +249,8 @@ From code: `engine.insertFootnote(text)`, `setFootnoteText(element, text)`, `rem
225
249
  For long documents the toolbar offers multilevel numbering (1., 1.1., 1.1.1., saved as `<ol data-numbering="legal">`),
226
250
  a table of contents of the headings with page numbers (saved as `<table data-type="toc">`, refreshed with
227
251
  `updateTableOfContents()`), and a navigation pane. `PageSettings` has `differentFirstPage` to hide the header, footer
228
- and page number on the first page, and `firstPageNumber` for the number printed on it.
252
+ and page number on the first page, and `firstPageNumber` for the number printed on it. The navigation pane lists the
253
+ headings or shows page thumbnails; clicking one scrolls to it.
229
254
 
230
255
  ## Comments, tracked changes and comparison
231
256
 
@@ -253,12 +278,13 @@ const save = () =>
253
278
  </template>
254
279
  ```
255
280
 
256
- Users select text and press "Add comment" (`Ctrl/⌘+Alt+M`); the "Comments" panel replies, resolves, reopens and
257
- deletes them, and flags comments whose text was deleted.
281
+ The comment and change tools are in the **Review** menu of the toolbar. Users select text and choose "Add comment"
282
+ (`Ctrl/⌘+Alt+M`); the "Comments" panel replies, resolves, reopens and deletes them, and flags comments whose text was
283
+ deleted.
258
284
 
259
285
  ### Tracked changes
260
286
 
261
- Bind `v-model:trackChanges` (or press "Track changes") to record typing, deleting, cut and paste as tracked changes
287
+ Bind `v-model:trackChanges` (or choose "Track changes" in the Review menu) to record typing, deleting, cut and paste as tracked changes
262
288
  by `author`. They are part of the HTML, `<ins data-change="id" data-author="…" data-time="…">` and `<del …>`, shown
263
289
  and printed green-underlined and red-struck. The "Changes" panel accepts or rejects them one by one or all at once;
264
290
  from code use `engine.getChanges()`, `engine.resolveChanges(accept, id?)` and `engine.selectChange(id)`. Formatting,
@@ -277,9 +303,39 @@ shown whole. `compareDocuments(before, after)` returns the same HTML and counts
277
303
  <DocumentCompare :before="previousVersion" :after="html" :height="600" />
278
304
  ```
279
305
 
306
+ ## Editing together
307
+
308
+ The editor has hooks for several people on one document; the transport (WebSocket, WebRTC, …) is up to your app.
309
+ `selectionChange` reports your caret or selection as character positions, `collaborators` draws other people's carets
310
+ (with names) and selections, and a new `v-model` value from outside keeps your caret at the same character position.
311
+ The engine also offers `getSelectionOffsets()`, `getOffsetRects(offsets)` and `setContent(html, { keepSelection })`;
312
+ `collaboratorColor` gives the colour a collaborator is drawn in.
313
+
314
+ ```vue
315
+ <script setup lang="ts">
316
+ import { ref } from 'vue';
317
+ import { type Collaborator, DocumentEditor, type SelectionOffsets } from 'nuvra';
318
+
319
+ const html = ref('');
320
+ const collaborators = ref<Collaborator[]>([]); // filled from your socket messages
321
+
322
+ const sendSelection = (selection: SelectionOffsets | null) =>
323
+ socket.send(JSON.stringify({ type: 'selection', id: me.id, name: me.name, selection }));
324
+ </script>
325
+
326
+ <template>
327
+ <DocumentEditor v-model="html" :collaborators="collaborators" @selection-change="sendSelection" />
328
+ </template>
329
+ ```
330
+
331
+ This is not a CRDT: the document travels as a whole, so when two people type at the same time the document sent last
332
+ wins, and because positions are character offsets, a remote edit before your caret shifts it. A library such as Yjs
333
+ can carry the document and the selections (awareness), but edits are still not merged character by character. See the
334
+ [guide](https://nuvra-docs.vercel.app/docs/collaboration).
335
+
280
336
  ## Commands and toolbar buttons
281
337
 
282
- Typing `/` at the start of a line or after a space opens a command menu: headings, lists, table, page break, footnote,
338
+ Typing `/` at the start of a line or after a space opens a command menu: headings, lists, table, page and section breaks, footnote,
283
339
  table of contents, dates, signature blocks and your variables. `slashCommands` adds your own commands at the top, and the
284
340
  `toolbar` slot adds your own buttons:
285
341
 
@@ -1,3 +1,4 @@
1
+ import type { Collaborator, SelectionOffsets } from '../core/collaboration';
1
2
  import { type DocumentComment } from '../core/comments';
2
3
  import { DocumentEngine } from '../core/engine/engine';
3
4
  import { type EditorLocaleInput } from '../core/labels';
@@ -15,6 +16,8 @@ interface Props {
15
16
  autofocus?: boolean;
16
17
  /** Gray space around the page or web sheet that separates the document from the editor frame. */
17
18
  canvasPadding?: CssSize;
19
+ /** Other people editing the document; their carets and selections are drawn over it. */
20
+ collaborators?: readonly Collaborator[];
18
21
  /** Name written as the author of new comments, replies and tracked changes. */
19
22
  author?: string;
20
23
  /** View shown first; form fields use the lighter web view. */
@@ -76,6 +79,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
76
79
  exportHtml: () => Promise<void>;
77
80
  /** Downloads the document as a Word-compatible file. */
78
81
  exportWord: () => Promise<void>;
82
+ /** Downloads the document as a PDF drawn from its pages. */
83
+ exportPdf: () => Promise<void>;
79
84
  /** Moves keyboard focus into the document. */
80
85
  focus: () => void | undefined;
81
86
  /** Returns the document HTML, including edits not yet written to the model. */
@@ -94,6 +99,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
94
99
  "update:page": (value: PageSettings) => any;
95
100
  uploadError: (error: unknown) => any;
96
101
  importError: (error: unknown) => any;
102
+ exportError: (error: unknown) => any;
103
+ selectionChange: (selection: SelectionOffsets | null) => any;
97
104
  "update:modelValue": (value: string) => any;
98
105
  "update:trackChanges": (value: boolean) => any;
99
106
  "update:comments": (value: DocumentComment[] | undefined) => any;
@@ -103,6 +110,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
103
110
  "onUpdate:page"?: ((value: PageSettings) => any) | undefined;
104
111
  onUploadError?: ((error: unknown) => any) | undefined;
105
112
  onImportError?: ((error: unknown) => any) | undefined;
113
+ onExportError?: ((error: unknown) => any) | undefined;
114
+ onSelectionChange?: ((selection: SelectionOffsets | null) => any) | undefined;
106
115
  "onUpdate:modelValue"?: ((value: string) => any) | undefined;
107
116
  "onUpdate:trackChanges"?: ((value: boolean) => any) | undefined;
108
117
  "onUpdate:comments"?: ((value: DocumentComment[] | undefined) => any) | undefined;
@@ -114,6 +123,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
114
123
  placeholder: string;
115
124
  disabled: boolean;
116
125
  minHeight: CssSize;
126
+ collaborators: readonly Collaborator[];
117
127
  maxHeight: CssSize;
118
128
  maxLength: number;
119
129
  variables: readonly TemplateVariable[];
@@ -2,6 +2,7 @@ import type { ParagraphIndents } from '../core/engine/blocks';
2
2
  import type { DocumentEngine } from '../core/engine/engine';
3
3
  import { type SheetFootnote } from '../core/footnotes';
4
4
  import { type DocumentViewMode, type PageMargins, type PageMetrics, type PageSettings } from '../core/page';
5
+ import { type SheetGeometry } from '../core/pagination';
5
6
  interface Props {
6
7
  /** Grow with the content between the editor's min and max height instead of filling a fixed height. */
7
8
  autoHeight: boolean;
@@ -21,6 +22,8 @@ interface Props {
21
22
  metrics: PageMetrics;
22
23
  /** Number of sheets drawn behind the content in page view. */
23
24
  pageCount: number;
25
+ /** Position, size and orientation of every sheet, as laid out by pagination; empty until the first layout. */
26
+ sheets: readonly SheetGeometry[];
24
27
  /** Whether the ruler is drawn above the sheet; it is only ever shown in the page view. */
25
28
  rulerVisible: boolean;
26
29
  /** Paginated sheets or a single web sheet. */
@@ -0,0 +1,17 @@
1
+ import { type Collaborator } from '../core/collaboration';
2
+ import type { DocumentEngine } from '../core/engine/engine';
3
+ interface Props {
4
+ /** Engine that measures positions in the document. */
5
+ engine: DocumentEngine;
6
+ /** People whose carets are drawn. */
7
+ collaborators: readonly Collaborator[];
8
+ /** Element the marks are positioned in; it must be positioned itself. */
9
+ container: HTMLElement;
10
+ /** Scrolling canvas; marks outside it are hidden. */
11
+ scrollTarget: HTMLElement;
12
+ /** Changes whenever the layout may move text (zoom, view mode, page setup), so the marks are measured again. */
13
+ layoutKey: string;
14
+ }
15
+ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
16
+ declare const _default: typeof __VLS_export;
17
+ export default _default;
@@ -1,16 +1,36 @@
1
- import type { OutlineHeading } from '../core/outline';
1
+ import type { OutlineHeading, OutlineView } from '../core/outline';
2
+ import type { PageMetrics } from '../core/page';
3
+ import type { SheetGeometry } from '../core/pagination';
2
4
  interface Props {
3
5
  /** Headings of the document with the page each starts on, `null` outside the page view. */
4
6
  headings: ReadonlyArray<OutlineHeading & {
5
7
  page: number | null;
6
8
  }>;
9
+ /** Clean HTML of every sheet, shown as page thumbnails; empty outside the page view. */
10
+ pages: readonly string[];
11
+ /** Page geometry the thumbnails are scaled from. */
12
+ metrics: PageMetrics;
13
+ /** Size of every sheet; a sheet turned by a section break gets a turned thumbnail. */
14
+ sheets: readonly SheetGeometry[];
15
+ /** Number printed on the first sheet. */
16
+ firstPageNumber: number;
7
17
  }
8
- declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
+ type __VLS_Props = Props;
19
+ type __VLS_ModelProps = {
20
+ /** Which list is shown; the editor reads it to lay out thumbnails only while they are visible. */
21
+ 'view'?: OutlineView;
22
+ };
23
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
24
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
25
  select: (heading: HTMLElement) => any;
10
26
  close: () => any;
11
- }, string, import("vue").PublicProps, Readonly<Props> & Readonly<{
27
+ selectPage: (index: number) => any;
28
+ "update:view": (value: OutlineView) => any;
29
+ }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
12
30
  onSelect?: ((heading: HTMLElement) => any) | undefined;
13
31
  onClose?: (() => any) | undefined;
32
+ onSelectPage?: ((index: number) => any) | undefined;
33
+ "onUpdate:view"?: ((value: OutlineView) => any) | undefined;
14
34
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
15
35
  declare const _default: typeof __VLS_export;
16
36
  export default _default;
@@ -37,9 +37,9 @@ interface Props {
37
37
  /** Template variables offered by the variable menu; the menu is hidden without any. */
38
38
  variables: ReadonlyArray<TemplateVariable>;
39
39
  }
40
- declare var __VLS_604: {};
40
+ declare var __VLS_626: {};
41
41
  type __VLS_Slots = {} & {
42
- default?: (props: typeof __VLS_604) => any;
42
+ default?: (props: typeof __VLS_626) => any;
43
43
  };
44
44
  declare const __VLS_base: import("vue").DefineComponent<Props, {
45
45
  /** Opens the link popover. */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Hooks for editing a document together. The editor reports where the caret is and draws the carets and selections
3
+ * of other people; sending the document and the selections between them (WebSocket, WebRTC, a CRDT such as Yjs) is
4
+ * left to the application.
5
+ */
6
+ /** A selection as character positions through the document; equal positions are a caret. */
7
+ export interface SelectionOffsets {
8
+ /** Where selecting started. */
9
+ anchor: number;
10
+ /** Where the caret is. */
11
+ focus: number;
12
+ }
13
+ /** Someone else editing the same document. */
14
+ export interface Collaborator {
15
+ /** Stable id of the person or connection. */
16
+ id: string;
17
+ /** Name shown next to the caret. */
18
+ name: string;
19
+ /** CSS colour of the caret and the selection; one is picked from the id when it is left out. */
20
+ color?: string;
21
+ /** Where the person's caret or selection is, or `null` while they are not in the document. */
22
+ selection: SelectionOffsets | null;
23
+ }
24
+ /** The colour of a collaborator: their own, or a stable one derived from the id. */
25
+ export declare const collaboratorColor: (collaborator: Pick<Collaborator, "id" | "color">) => string;
@@ -13,8 +13,15 @@ export declare const FOOTNOTE_ATTRIBUTE = "data-footnote";
13
13
  export declare const FOOTNOTE_SELECTOR = "sup[data-footnote]";
14
14
  /** Selector for every inline atom: elements inside a line that behave as one character and hold no editable text. */
15
15
  export declare const INLINE_ATOM_SELECTOR = "span[data-variable], sup[data-footnote]";
16
- /** Selector for blocks without editable text inside: rules, images and page breaks. */
17
- export declare const ATOM_SELECTOR = "hr, img, div[data-type=\"page-break\"]";
16
+ /** Selector for blocks without editable text inside: rules, images, page breaks and section breaks. */
17
+ export declare const ATOM_SELECTOR = "hr, img, div[data-type=\"page-break\"], div[data-type=\"section-break\"]";
18
+ /** Whether a `<div>` is one of the break blocks the document model keeps: a page break or a section break. */
19
+ export declare const isBreakBlock: (node: Node | null | undefined) => boolean;
20
+ /**
21
+ * Creates a section break: the sheets after it are turned to `orientation`. It also starts a new sheet, so it is drawn
22
+ * and printed like a page break.
23
+ */
24
+ export declare const createSectionBreak: (orientation: "portrait" | "landscape") => HTMLDivElement;
18
25
  /** Selector for blocks that hold editable inline text. */
19
26
  export declare const TEXT_BLOCK_SELECTOR = "p, h1, h2, h3, h4, h5, h6, pre";
20
27
  /** Marks the extra `<br>` that keeps an empty or break-terminated line visible; it is never serialised. */
@@ -6,6 +6,7 @@ import { EditorHistory } from './history';
6
6
  import { type ListKind } from './lists';
7
7
  import { type MarkName, type StyleName, type TextCase } from './marks';
8
8
  import { SearchController, type SearchState } from './search';
9
+ import { type TextBookmark } from './selection';
9
10
  import { type TableCell } from './tables';
10
11
  /** A tracked insertion or deletion; the parts of one edit are joined. */
11
12
  export interface TrackedChange {
@@ -162,10 +163,23 @@ export declare class DocumentEngine {
162
163
  * @param options.addToHistory Record the replacement as an undo step instead of clearing the history.
163
164
  * @param options.emitUpdate Emit `update` so the host saves the new content.
164
165
  */
165
- setContent(html: string, { addToHistory, emitUpdate }?: {
166
+ setContent(html: string, { addToHistory, emitUpdate, keepSelection }?: {
166
167
  addToHistory?: boolean | undefined;
167
168
  emitUpdate?: boolean | undefined;
169
+ keepSelection?: boolean | undefined;
168
170
  }): void;
171
+ /**
172
+ * The selection as character positions through the document: `anchor` where selecting started, `focus` where the
173
+ * caret is. Positions survive changes to the markup, so they can be sent to other people editing the document.
174
+ */
175
+ getSelectionOffsets(): TextBookmark | null;
176
+ /**
177
+ * Viewport rectangles of the text between two character positions, for drawing someone else's selection; a
178
+ * collapsed position gives the rectangle of its caret line with no width.
179
+ */
180
+ getOffsetRects(offsets: TextBookmark): DOMRect[];
181
+ /** Number of character positions in the document, the largest offset a selection can have. */
182
+ private characterPositions;
169
183
  /** Switches between editing and read-only mode. */
170
184
  setEditable(editable: boolean): void;
171
185
  /** Word and character counts of the document text. */
@@ -395,6 +409,8 @@ export declare class DocumentEngine {
395
409
  private insertBlock;
396
410
  /** Inserts a horizontal rule. */
397
411
  insertHorizontalRule(): void;
412
+ /** Inserts a section break: the sheets after it are turned to `orientation`, for example for a wide table. */
413
+ insertSectionBreak(orientation: 'portrait' | 'landscape'): void;
398
414
  /** Inserts a manual page break. */
399
415
  insertPageBreak(): void;
400
416
  /**
@@ -2,6 +2,8 @@
2
2
  export declare const VARIABLE_NAME: RegExp;
3
3
  /** Attribute holding the label a variable chip shows in the editor; editor-only. */
4
4
  export declare const VARIABLE_LABEL_ATTRIBUTE = "data-label";
5
+ /** Attribute pagination writes on blocks of a section turned against the document's orientation; editor-only. */
6
+ export declare const ROTATED_ATTRIBUTE = "data-doc-rotated";
5
7
  /** Attribute pagination writes on blocks it moved to the next sheet; editor-only. */
6
8
  export declare const GAP_ATTRIBUTE = "data-doc-gap";
7
9
  /** Class marking table cells in a drag selection; editor-only. */
@@ -9,6 +9,8 @@ interface DocumentSnapshot {
9
9
  * with real page numbers and breaks the pages exactly where the editor shows them.
10
10
  */
11
11
  pages?: string[];
12
+ /** For every sheet of `pages`, whether a section break turned it against the document's orientation. */
13
+ rotated?: boolean[];
12
14
  /** Footnotes of every sheet of `pages`; without pages, the notes of all of them are printed after the document. */
13
15
  footnotes?: SheetFootnote[][];
14
16
  /** Document title, used for the print title and file names. */
@@ -16,6 +18,11 @@ interface DocumentSnapshot {
16
18
  /** Page size, orientation, margins and the running texts. */
17
19
  page: PageSettings;
18
20
  }
21
+ /** Paper width and height in millimetres, in the chosen orientation. */
22
+ export declare const paperSize: ({ size, orientation }: PageSettings) => {
23
+ width: number;
24
+ height: number;
25
+ };
19
26
  /** Turns a document title into a safe file name without extension. */
20
27
  export declare const toFileName: (title: string) => string;
21
28
  /**
@@ -24,7 +31,7 @@ export declare const toFileName: (title: string) => string;
24
31
  * the pages break where the editor shows them; without them the document flows and the browser repeats one fixed
25
32
  * header and footer on every page.
26
33
  */
27
- export declare const buildPrintableHtml: ({ html, pages, footnotes, title, page }: DocumentSnapshot) => string;
34
+ export declare const buildPrintableHtml: ({ html, pages, rotated, footnotes, title, page }: DocumentSnapshot) => string;
28
35
  /** Offers text or binary content as a file download. */
29
36
  export declare const downloadFile: (content: string | Blob, fileName: string, type: string) => void;
30
37
  /** Prints through a detached iframe so the application chrome never ends up on paper. Waits for images first. */
@@ -59,6 +59,7 @@ export declare const uz: {
59
59
  readonly 'editor.enterFullscreen': "To‘liq ekran";
60
60
  readonly 'editor.exitFullscreen': "To‘liq ekrandan chiqish";
61
61
  readonly 'editor.exportHtml': "HTML sifatida yuklab olish";
62
+ readonly 'editor.exportPdf': "PDF sifatida yuklab olish";
62
63
  readonly 'editor.exportWord': "Word (.docx) sifatida yuklab olish";
63
64
  readonly 'editor.fitWidth': "Kenglikka moslash";
64
65
  readonly 'editor.fontFamily': "Shrift";
@@ -112,6 +113,10 @@ export declare const uz: {
112
113
  readonly 'editor.numbering.legal': "Ko‘p darajali raqamlash (1.1, 1.2)";
113
114
  readonly 'editor.outline': "Navigatsiya paneli";
114
115
  readonly 'editor.outline.empty': "Hujjatda sarlavhalar yo‘q. Sarlavha uslubini qo‘llang, ular shu yerda ko‘rinadi.";
116
+ readonly 'editor.outline.headings': "Sarlavhalar";
117
+ readonly 'editor.outline.noPages': "Sahifalar faqat sahifa ko‘rinishida chiqadi.";
118
+ readonly 'editor.outline.page': "{page}-sahifa";
119
+ readonly 'editor.outline.pages': "Sahifalar";
115
120
  readonly 'editor.openLink': "Havolani ochish";
116
121
  readonly 'editor.or': "yoki havola orqali";
117
122
  readonly 'editor.orderedList': "Raqamli ro‘yxat";
@@ -147,6 +152,7 @@ export declare const uz: {
147
152
  readonly 'editor.replace.one': "Almashtirish";
148
153
  readonly 'editor.replace.placeholder': "Almashtirish";
149
154
  readonly 'editor.replace.toggle': "Almashtirishni ko‘rsatish";
155
+ readonly 'editor.review': "Taqriz: izohlar va o‘zgarishlar";
150
156
  readonly 'editor.ruler': "Chizg‘ich";
151
157
  readonly 'editor.ruler.firstLine': "Birinchi qator chekinishi";
152
158
  readonly 'editor.ruler.indentLeft': "Chap chekinish";
@@ -160,6 +166,8 @@ export declare const uz: {
160
166
  readonly 'editor.search.title': "Qidirish va almashtirish";
161
167
  readonly 'editor.search.wholeWord': "Butun so‘z";
162
168
  readonly 'editor.selectAll': "Barchasini tanlash";
169
+ readonly 'editor.sectionBreak.landscape': "Bo‘lim uzilishi: albom sahifalar";
170
+ readonly 'editor.sectionBreak.portrait': "Bo‘lim uzilishi: kitob sahifalar";
163
171
  readonly 'editor.signature': "Imzo bloki";
164
172
  readonly 'editor.signature.agreed': "KELISHILDI grifi";
165
173
  readonly 'editor.signature.approval': "TASDIQLAYMAN grifi";
@@ -12,6 +12,8 @@ export interface OutlineHeading {
12
12
  /** The heading element in the editable document. */
13
13
  element: HTMLElement;
14
14
  }
15
+ /** What the navigation panel lists: the headings, or thumbnails of the pages. */
16
+ export type OutlineView = 'headings' | 'pages';
15
17
  /** A line of the table of contents. */
16
18
  export interface TableOfContentsEntry {
17
19
  /** Heading level, 1 to 6; deeper levels are indented. */
@@ -1,5 +1,18 @@
1
1
  import { type SheetFootnote } from './footnotes';
2
- import type { PageMetrics } from './page';
2
+ import type { PageMetrics, PageOrientation } from './page';
3
+ /** Where a sheet is drawn and which way it is turned. */
4
+ export interface SheetGeometry {
5
+ /** Distance of the sheet from the top of the first sheet, in pixels. */
6
+ top: number;
7
+ /** Sheet width in pixels. */
8
+ width: number;
9
+ /** Sheet height in pixels. */
10
+ height: number;
11
+ /** Orientation of the sheet. */
12
+ orientation: PageOrientation;
13
+ /** Whether the sheet is turned against the document's own orientation, by a section break. */
14
+ rotated: boolean;
15
+ }
3
16
  /** Handle returned by {@link createPagination}. */
4
17
  export interface PaginationController {
5
18
  /** Sets the page geometry and re-runs the layout; pass `null` to switch pagination off (web view). */
@@ -15,15 +28,21 @@ interface PaginationOptions {
15
28
  onPageCount: (count: number) => void;
16
29
  /** Receives the footnotes of every sheet whenever they change; the web view is one sheet holding all of them. */
17
30
  onFootnotes?: (sheets: SheetFootnote[][]) => void;
31
+ /** Receives the position, size and orientation of every sheet whenever they change; empty in the web view. */
32
+ onSheets?: (sheets: SheetGeometry[]) => void;
18
33
  /** Whether an IME composition is in progress, during which blocks must not move. */
19
34
  isComposing: () => boolean;
20
35
  }
36
+ /** Sheets of a document without section breaks, all alike. */
37
+ export declare const uniformSheets: (metrics: PageMetrics, count: number) => SheetGeometry[];
38
+ /** Index of the sheet a vertical position falls on, counted from the top of the first sheet. */
39
+ export declare const sheetIndexAt: (sheets: readonly SheetGeometry[], offset: number) => number;
21
40
  /**
22
41
  * Splits the laid-out document into the clean HTML of every sheet, for printing and export. Pagination has already
23
42
  * moved every block onto its page, so a block belongs to the page its top falls on; blocks taller than a page stay
24
43
  * whole, exactly as they are shown on screen. Footnote references keep their document-wide numbers.
25
44
  */
26
- export declare const splitIntoPages: (root: HTMLElement, metrics: PageMetrics, pageCount: number) => string[];
45
+ export declare const splitIntoPages: (root: HTMLElement, metrics: PageMetrics, pageCount: number, geometry?: readonly SheetGeometry[]) => string[];
27
46
  /**
28
47
  * Keeps the document laid out on sheets. Layout passes are batched to one per animation frame and re-run when the
29
48
  * document resizes, images or fonts load, or the host schedules one after an edit.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * PDF download without the print dialog and without dependencies. Every sheet of the printable document is drawn into
3
+ * a canvas through an SVG `foreignObject`, and the pictures are written into a PDF, one per page. The PDF looks like
4
+ * the printout, but its text cannot be selected or searched.
5
+ */
6
+ /** A rendered page: a JPEG picture of the sheet and the paper size it is printed on. */
7
+ export interface PdfPage {
8
+ /** JPEG data of the page picture. */
9
+ jpeg: Uint8Array;
10
+ /** Picture width in pixels. */
11
+ width: number;
12
+ /** Picture height in pixels. */
13
+ height: number;
14
+ /** Paper width in PDF points (1/72 inch). */
15
+ widthPt: number;
16
+ /** Paper height in PDF points. */
17
+ heightPt: number;
18
+ }
19
+ /** Options of {@link renderPdf}. */
20
+ export interface PdfOptions {
21
+ /** Pixels per CSS pixel of the page pictures; higher is sharper and larger. Defaults to 2. */
22
+ scale?: number;
23
+ /** JPEG quality between 0 and 1. Defaults to 0.92. */
24
+ quality?: number;
25
+ }
26
+ /** MIME type of PDF files. */
27
+ export declare const PDF_MIME = "application/pdf";
28
+ /**
29
+ * Writes JPEG pictures into a PDF document, one picture filling each page.
30
+ *
31
+ * @returns the PDF file content.
32
+ */
33
+ export declare const buildPdf: (pages: readonly PdfPage[]) => Uint8Array;
34
+ /**
35
+ * Turns the printable HTML of a paginated document (one `section.doc-sheet` per page) into a PDF.
36
+ *
37
+ * @param printableHtml Standalone HTML as built for printing from the page view.
38
+ * @param paper Paper width and height in millimetres, in the chosen orientation.
39
+ */
40
+ export declare const renderPdf: (printableHtml: string, paper: {
41
+ width: number;
42
+ height: number;
43
+ }, options?: PdfOptions) => Promise<Blob>;
@@ -1,3 +1,3 @@
1
1
  /** Uploads an image and resolves with its public URL. Without a handler, images are embedded as data URLs. */
2
2
  export type DocumentImageUploadHandler = (file: File) => Promise<string>;
3
- export type DocumentMenuAction = 'source' | 'print' | 'exportHtml' | 'exportWord' | 'importWord' | 'formattingMarks' | 'ruler' | 'outline' | 'tableOfContents' | 'comments' | 'addComment' | 'footnote' | 'trackChanges' | 'changes' | 'fullscreen';
3
+ export type DocumentMenuAction = 'source' | 'print' | 'exportHtml' | 'exportWord' | 'exportPdf' | 'importWord' | 'formattingMarks' | 'ruler' | 'outline' | 'tableOfContents' | 'comments' | 'addComment' | 'footnote' | 'trackChanges' | 'changes' | 'fullscreen';