@inkbar/core 0.9.0 → 0.11.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 +129 -2
- package/dist/dom.d.mts +600 -5
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +4423 -1581
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{sign-FuwcsNkA.d.mts → sign-BTbtPHbM.d.mts} +50 -6
- package/dist/sign-BTbtPHbM.d.mts.map +1 -0
- package/dist/{sign-DIhqyOV6.mjs → sign-DZVDJhqT.mjs} +57 -5
- package/dist/sign-DZVDJhqT.mjs.map +1 -0
- package/package.json +3 -3
- package/dist/sign-DIhqyOV6.mjs.map +0 -1
- package/dist/sign-FuwcsNkA.d.mts.map +0 -1
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ viewer.open(lockedFile, { password }) // this password applies to this load only
|
|
|
111
111
|
viewer.destroy()
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
-
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `annotations`, `fields`, `onReady` and `onError`. Pass `annotations: { mode: 'controlled', initial, onChange }` to own the session annotation list: `onChange` hears each proposed list, and you push it back with `viewer.document.get()?.annotations.setControlledValue(next)`. `initial` applies to every document the viewer opens, so a host that keeps its own list pushes it with `setControlledValue` in `onReady` for each new document. Undo and redo are inert in controlled mode. Set `threads` to `false` to leave out the thread popover. `viewer.status`, `viewer.document`, `viewer.engine`, `viewer.viewport` and `viewer.tools` are readables; the last two change when the engine rebuilds. `viewer.activated` is a readable too: the id of the annotation the user last clicked or activated with Enter, or `null`. `viewer.setActivated(id)` activates one from your code without calling `onActivate`; it ignores an id the document doesn't have, and `setActivated(null)` clears it. A password you pass to `open` replaces the `createViewer` one for that load. The controls are `createToolButton`, `createUndo`, `createRedo`, `createZoomIn`, `createZoomOut`, `createZoomSelect`, `createPageInput`, `createPageCount`, `createPrevPage`, `createNextPage` and `createCommandButton`. Each one disables itself while no document is ready and lives as long as its viewer. Buttons show an icon from [`@inkbar/icons`](https://github.com/inkbar/inkbar/tree/main/packages/icons). Replace their children with your own icon and the accessible name stays; `createIcon` draws any `@inkbar/icons` icon for your own buttons. For a tool the viewer doesn't have, `createToolButton` and the highlight, shape and signature menus log `InkError('UnknownTool')`. The button renders disabled and the menus render nothing. `createToolbar` gives one Tab stop to all its buttons, with arrow keys between them, and `createToolbarSeparator` divides groups of controls in it. `createCommandButton(viewer, id)` places a plugin command: a `div.ink-command` holding a button named by the command's label (disabled with the command; while it runs, it keeps focus with `aria-busy` and `aria-disabled`, and a press does nothing) and a `span.ink-command-status[role="status"]` with its status text; for an id no plugin registered it logs an `InkError('UnknownCommand')` to the console, naming the id and the ones that are registered, and returns the same markup disabled and hidden (`aria-hidden`, no label). Pass `plugins: [...]` to `createViewer` to register plugins; their tools join the viewer's, and `createToolButton` takes a plugin tool's own label and icon. `viewer.commands.get(id)` returns a command. An input, select or textarea you add keeps its arrow keys and its own `tabindex`: it is a Tab stop unless you give it `tabindex="-1"`.
|
|
114
|
+
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `annotations`, `fields`, `onReady` and `onError`. Pass `annotations: { mode: 'controlled', initial, onChange }` to own the session annotation list: `onChange` hears each proposed list, and you push it back with `viewer.document.get()?.annotations.setControlledValue(next)`. `initial` applies to every document the viewer opens, so a host that keeps its own list pushes it with `setControlledValue` in `onReady` for each new document. Undo and redo are inert in controlled mode. Set `threads` to `false` to leave out the thread popover. `viewer.status`, `viewer.document`, `viewer.engine`, `viewer.viewport` and `viewer.tools` are readables; the last two change when the engine rebuilds. `viewer.activated` is a readable too: the id of the annotation the user last clicked or activated with Enter, or `null`. `viewer.setActivated(id)` activates one from your code without calling `onActivate`; it ignores an id the document doesn't have, and `setActivated(null)` clears it. A password you pass to `open` replaces the `createViewer` one for that load. `viewer.download({ filename })` and `viewer.print()` download and print the open PDF, and Ctrl or Cmd+S and +P inside the root do the same; they do nothing while no document is open. The download is named by `doc.fileName`: the opened `File`'s name, else the URL's last segment, else `document.pdf`. Printing loads the file into a hidden frame and opens the browser's print dialog for it; on iOS the PDF opens in a new tab instead. Both use `doc.getData()`, which today is the file as it was opened, without this session's annotations. For a host of your own, `downloadDocument(doc, opts)`, `printDocument(doc)` and `attachFileKeys(el, () => doc)` do the same for any document. The controls are `createToolButton`, `createUndo`, `createRedo`, `createDownloadButton`, `createPrintButton`, `createZoomIn`, `createZoomOut`, `createZoomSelect`, `createPageInput`, `createPageCount`, `createPrevPage`, `createNextPage` and `createCommandButton`. Each one disables itself while no document is ready and lives as long as its viewer. Buttons show an icon from [`@inkbar/icons`](https://github.com/inkbar/inkbar/tree/main/packages/icons). Replace their children with your own icon and the accessible name stays; `createIcon` draws any `@inkbar/icons` icon for your own buttons. For a tool the viewer doesn't have, `createToolButton` and the highlight, shape and signature menus log `InkError('UnknownTool')`. The button renders disabled and the menus render nothing. `createToolbar` gives one Tab stop to all its buttons, with arrow keys between them, and `createToolbarSeparator` divides groups of controls in it. `createCommandButton(viewer, id)` places a plugin command: a `div.ink-command` holding a button named by the command's label (disabled with the command; while it runs, it keeps focus with `aria-busy` and `aria-disabled`, and a press does nothing) and a `span.ink-command-status[role="status"]` with its status text; for an id no plugin registered it logs an `InkError('UnknownCommand')` to the console, naming the id and the ones that are registered, and returns the same markup disabled and hidden (`aria-hidden`, no label). Pass `plugins: [...]` to `createViewer` to register plugins; their tools join the viewer's, and `createToolButton` takes a plugin tool's own label and icon. `viewer.commands.get(id)` returns a command. An input, select or textarea you add keeps its arrow keys and its own `tabindex`: it is a Tab stop unless you give it `tabindex="-1"`.
|
|
115
115
|
|
|
116
116
|
`createViewer` reads the theme's colours from the element you pass, so that element needs to be in the page when you call it.
|
|
117
117
|
|
|
@@ -139,7 +139,8 @@ Options:
|
|
|
139
139
|
- `container` and `document`, both required.
|
|
140
140
|
- `tools`: the creation tools to register (`allTools()` or the ones you import from `@inkbar/core/dom`). `select-text` and `select` are always present.
|
|
141
141
|
- `zoom` (a number, `'fit-width'` or `'fit-page'`; default `'fit-width'`), `page`, `mode`, `rotation` and `tool` set the starting view. Change them later through `engine.viewport` and `engine.tools`.
|
|
142
|
-
- `gap`, `buffer` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
|
|
142
|
+
- `gap`, `buffer`, `pinchZoom` and `customAnnotations` are fixed for a build. Change them with `engine.rebuild`.
|
|
143
|
+
- `pinchZoom` (default `true`): a two-finger pinch over the container, a trackpad pinch or ctrl+wheel zooms the document. One mouse wheel notch with ctrl held zooms by 10%. Two fingers also scroll the document. Set it to `false` and the browser handles pinches itself, zooming the whole page.
|
|
143
144
|
- `toolOptions` holds the tools' options, such as the shape kind. Pass one from `createToolOptions()` and keep it to carry the options over to the next document; `createViewer` keeps one for its whole life. The tool style (colours, line style, text style) belongs to the document: each new document starts from the theme's defaults.
|
|
144
145
|
- `signatures` is where saved signatures live (see [Signatures](#signatures)); `signatureFonts` lists the handwriting fonts a typed signature offers.
|
|
145
146
|
- `decorateAnnotation(annotation)` returns your own `data`, `classes` and `description` for an annotation's element, or `undefined`. `{ data: { 'my-flag': 'on' } }` writes `data-my-flag="on"`, and `description` becomes the element's `aria-description`. Inkbar re-applies them whenever the annotations change. Call `engine.redecorate()` when they depend on anything else. Core's own attributes and `ink-` classes always win.
|
|
@@ -195,6 +196,8 @@ Most creation tools are one-shot: `shape`, `freetext`, `note`, `redact-area` and
|
|
|
195
196
|
|
|
196
197
|
A host tool says which it is with `kind: 'one-shot' | 'sticky'` (no `kind` is sticky). A one-shot tool adds and selects what it makes, then calls `ctx.made(ids)`; `ctx.done()` clears the selection and switches to `select-text` itself. Under a sticky tool, `ctx.made` does nothing. The controller's `active` readable reports every switch, including one a one-shot tool makes.
|
|
197
198
|
|
|
199
|
+
On a touch screen, `draw`, `sketch`, `shape`, `redact-area` and `freetext` take one-finger drags: one finger draws and two fingers scroll and zoom. A finger tap with `draw` or `sketch` draws nothing, while a pen tap or a mouse click makes a dot. A pen that lands while a palm rests on the screen takes over and draws. A one-shot tool gives one-finger scrolling back once it has made something. A host tool opts in with `claimsTouch: true`; while it is active, one finger no longer scrolls the document. For this the controller sets `touch-action` on the pages element (`[data-ink-pages]`) and removes it when the tool changes, so a value of your own there is lost.
|
|
200
|
+
|
|
198
201
|
| Where focus is | Key | Effect |
|
|
199
202
|
| ---------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
200
203
|
| Viewport, `highlight` tool | `Enter` | Turns the text selection into a highlight |
|
|
@@ -303,10 +306,25 @@ A command's `run` gets `doc`, the document open when it started, and a `signal`
|
|
|
303
306
|
|
|
304
307
|
Marks redraw when their records change, undo and redo included, and keep their elements, so focus and an open card survive a zoom. Chain `.scrollToFirst()` onto `plugin.marks(…)` to scroll the viewer to new marks: when records start to get a mark, it scrolls to the first in reading order, unless that one is in view already. It moves no focus, and the records a document opens with, undo and redo never scroll. Each carries `data-record` and `data-plugin`; `style` (custom properties included), `className` and `attrs` add your own look. A card has a `title`, optional `label`, `meta`, `swatch` and `body`, and `actions`. Actions must be synchronous: a write after an `await` falls outside the action's undo step. The first action is the primary one and takes focus. After an action that removes the mark, focus moves to the next of this `marks()` call's marks, else the previous one, else the viewport. A mark with a card has `aria-haspopup="dialog"`, and `aria-expanded` is true while its card is open. Escape or a press outside closes the card and returns focus to the mark. `@inkbar/theme` styles `.ink-card`, `.ink-card-title`, `.ink-card-meta`, `.ink-card-swatch`, `.ink-card-body` and `.ink-card-actions`, and the card's `className` adds your own class.
|
|
305
308
|
|
|
309
|
+
Your marks' card also gives you a list. `createRecordList(viewer, { records: 'review.suggestions' })` (`InkRecordList` in the framework packages) lists the records that have a mark, each as the card the mark opens, with the same actions in the same undo step. See [Record lists](#record-lists).
|
|
310
|
+
|
|
306
311
|
`suggestions.annotates('redact', { from, onRemoved })` links records to the annotations made from them. Its `add(record)` adds an annotation from `from(record)`, stores the link at `custom.<plugin>.<records name>` (here `custom.review.suggestions`) and fills the type's default colour when `from` gives none. `of(record)` returns the linked annotations. `onRemoved(record, annotation)` and `onChanged(record, annotation)` run inside the user's undo step, never on undo or redo. Call it in `setup`. The link travels with the annotation through a Gloss export and import.
|
|
307
312
|
|
|
308
313
|
`quadsFromRect(rect)` and `rectFromQuads(quads)` from `@inkbar/core` convert between a box and the quads that text markup takes, and `doc.pageText(page)` resolves to a page's text joined the way the page reads: nothing between touching items, a space at line ends and at wide gaps.
|
|
309
314
|
|
|
315
|
+
### Reading records from your app
|
|
316
|
+
|
|
317
|
+
`viewer.records(key)` gives your app the records a plugin keeps, under the key `<plugin>.<name>` (`review.suggestions`). It is the plugin's own handle less `annotates`, and it follows the open document: `list` is a readable that emits as records change, and `[]` while nothing is open. The same key returns the same handle. Writes are undo steps like the plugin's own, and `batch(fn)` makes several one step. An unknown key returns an empty handle that ignores writes, and logs a warning once.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const suggestions = viewer.records<Suggestion>('review.suggestions')
|
|
321
|
+
suggestions.list.subscribe((items) => render(items))
|
|
322
|
+
suggestions.batch(() => {
|
|
323
|
+
suggestions.update('s1', { status: 'accepted' })
|
|
324
|
+
suggestions.remove('s2')
|
|
325
|
+
})
|
|
326
|
+
```
|
|
327
|
+
|
|
310
328
|
### Going further
|
|
311
329
|
|
|
312
330
|
The `plugin` that `setup` receives is also the plugin's context, so everything below works in the new form. The object form, `definePlugin({ id, setup(ctx) { … } })`, still works and returns the plugin itself: pass `plugins: [plugin]`.
|
|
@@ -322,6 +340,21 @@ A command added with `ctx.commands.add` is disabled until a document is ready, u
|
|
|
322
340
|
- Decorations from `ctx.decorate` re-apply when the annotations change. Call `ctx.redecorate()` when one reads state of your own that changed. Marks from `plugin.marks` take `refresh()` for the same job.
|
|
323
341
|
- The framework packages run `setup` once the viewer has mounted in the browser, never while rendering on the server. Until then the plugin's commands are disabled, and anything `setup` creates, such as a store, doesn't exist yet.
|
|
324
342
|
|
|
343
|
+
### Presses
|
|
344
|
+
|
|
345
|
+
`ctx.onPress(fn)` and `ctx.onLongPress(fn)` tell a plugin about a press on a page that nothing in Inkbar acted on. Each returns a function that stops listening, and listeners are removed when the plugin is torn down.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
ctx.onPress(({ page, point, pointerType }) => console.log(page, point, pointerType))
|
|
349
|
+
ctx.onLongPress(({ page, point }) => openMenu(page, point))
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
The payload is `{ page, point, pointerType }`. `page` is a zero-based index, `point` is in user space on that page (points, origin bottom-left, before `/Rotate`), and `pointerType` is `'mouse'`, `'touch'` or `'pen'`.
|
|
353
|
+
|
|
354
|
+
A press is a pointer going down and up on a page without moving more than a few pixels. A mouse click is a press. A touch or pen held still for about half a second is a long press instead: it fires while the finger is still down, and the release reports nothing more. A mouse never long-presses. A touch that becomes part of a pinch before a long press fires is neither, and so is one that starts a scroll.
|
|
355
|
+
|
|
356
|
+
Both fire only under the text and rectangle select tools; any other tool owns every press. They stay silent when something else took the press: an annotation, a resize handle, a link, a form field, a plugin mark or a text editor. A press that commits an open text editor or closes a popover reports nothing either. Inkbar leaves the browser's own long-press behaviour alone, so text selection and context menus still work.
|
|
357
|
+
|
|
325
358
|
## Search
|
|
326
359
|
|
|
327
360
|
`doc.search(query, options?)` finds text across the document. The query is a string or a `RegExp`. It returns `SearchResults`: iterate it with `for await` to get hits as pages are scanned, or call `all()` for the lot.
|
|
@@ -360,6 +393,100 @@ Ctrl/Cmd+F opens the bar when one is mounted and the key comes from inside the v
|
|
|
360
393
|
|
|
361
394
|
Hits are drawn as a layer over the page that multiplies with it, so the text shows through. `@inkbar/theme` styles them with `--ink-search-hit` and, for the current hit, `--ink-search-current`.
|
|
362
395
|
|
|
396
|
+
## Sidebars
|
|
397
|
+
|
|
398
|
+
`createSidebar` builds a sidebar of panels, each showing an element you give it. It needs no document or viewer: place it beside the viewer with your own flex or grid layout.
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { createSidebar } from '@inkbar/core/dom'
|
|
402
|
+
import { Square } from '@inkbar/icons'
|
|
403
|
+
|
|
404
|
+
const { element, controller } = createSidebar({
|
|
405
|
+
layout: 'rail',
|
|
406
|
+
side: 'left',
|
|
407
|
+
panels: [
|
|
408
|
+
{ id: 'pages', label: 'Pages', icon: Square },
|
|
409
|
+
{ id: 'notes', label: 'Notes', badge: 3 },
|
|
410
|
+
],
|
|
411
|
+
content: { pages: pagesElement, notes: notesElement },
|
|
412
|
+
onOpenChange: (open) => console.log(open),
|
|
413
|
+
})
|
|
414
|
+
shell.prepend(element)
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Each panel has an `id`, a `label`, an optional `icon` (an `IconNode` from `@inkbar/icons`) and an optional `badge`, a number or short string hidden when empty or 0. In the rail, the open panel shows its label as a title above its content. Pass `visible: false`, or call `controller.update({ visible })`, to hide the whole sidebar and keep its open panel, content and scroll for when it shows again: a results sidebar shown only while a search has hits, for instance.
|
|
418
|
+
|
|
419
|
+
`layout` is one of:
|
|
420
|
+
|
|
421
|
+
- `rail` (default): a vertical strip of icon buttons on the inner edge, beside the document, with the label as a tooltip. One panel is open at a time; activating the open panel's button closes it, leaving only the rail.
|
|
422
|
+
- `tabs`: labelled tabs across the top with one panel open. Pass `open: null` to collapse the sidebar, from a toolbar button of your own for instance.
|
|
423
|
+
- `stack`: every panel as a section with a header button, one under another, any number expanded.
|
|
424
|
+
|
|
425
|
+
`side` (`left` by default) decides which edge the rail and border sit on. `open` is the open panel id (`string | null`) for rail and tabs, and the expanded ids (`string[]`) for stack. Without it the sidebar starts with the first panel open and keeps its own state. With it the sidebar is controlled: a click reports the new value through `onOpenChange` and nothing changes until you call `controller.update({ open })`. When `update({ panels })` removes the open panel, rail and tabs open the first one left and stack drops it; the new value goes to `onOpenChange` either way.
|
|
426
|
+
|
|
427
|
+
Closed panels stay in the document with `hidden`, so their content keeps its state and scroll position. Rail and tabs follow the WAI-ARIA tabs pattern: one Tab stop, arrow keys (Up and Down on the rail, Left and Right for tabs), Home and End to move, Enter or Space to open. Stack headers follow the accordion pattern: each is a Tab stop with `aria-expanded`, and Up and Down move between them.
|
|
428
|
+
|
|
429
|
+
To render your own markup, use `createSidebarController(options)`, which holds the same state and behaviour. Spread `rootAttrs()` on the root, `tablistAttrs()` on the element holding the buttons, `triggerAttrs(id)` on each button and `panelAttrs(id)` on each panel, then call `bindTriggers(element)` once on an element containing the buttons. It handles clicks and keys and keeps the Tab stop right as buttons are re-rendered. `sidebarBadge(badge)` gives the text a badge shows, or `null` when it is hidden. Pass `idPrefix` when the markup is rendered on the server, so the element ids match on the client. A binding whose panels register one by one passes `pending: true` until they are all in, then `update({ pending: false })`: meanwhile a controlled `open` id not registered yet is kept instead of falling back, and the root reports the open state it will have. An uncontrolled sidebar created with no panels opens the first one to arrive.
|
|
430
|
+
|
|
431
|
+
`@inkbar/theme` sizes the sidebar with `--ink-sidebar-width` (280px) and the rail with `--ink-sidebar-rail-width` (48px). Override either per sidebar with CSS.
|
|
432
|
+
|
|
433
|
+
## Thumbnails
|
|
434
|
+
|
|
435
|
+
`createThumbnail(viewer, { page })` draws one page of the open document as a small image, with its annotations and plugin marks on top. Put it anywhere: a sidebar panel, a dialog, a card.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
import { createThumbnail } from '@inkbar/core/dom'
|
|
439
|
+
|
|
440
|
+
panel.append(createThumbnail(viewer, { page: 3, width: 120 }))
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
`page` is 0-based. Give `width` or `height` in CSS pixels and the other follows the page's shape and its own /Rotate (the viewer's rotation does not apply); `width` wins when you give both, and with neither the width is 120px. `annotations: false` and `marks: false` leave those layers out. `label` replaces the accessible name, "Page 4" for page 3.
|
|
444
|
+
|
|
445
|
+
A thumbnail is an image to assistive technology (`role="img"`), with no focus and no click behaviour of its own: wrap it in a button or link for that. It renders once it comes within 200px of the visible area and shows a placeholder of the right size before then, and while no document is open. Form fields show the values saved in the PDF. Annotations follow edits, undo and redo, and marks are drawn with the plugin's class and style but take no presses.
|
|
446
|
+
|
|
447
|
+
To paint into your own element, call `createThumbnailPainter(element, { document, pluginHost }, options)` with readables of the document and, for marks, the plugin host. It adds the `ink-thumbnail` class, its role, name, size and layers, and leaves the element's own classes and children alone. `update(options)` applies changed options and `destroy()` removes everything it added. `thumbnailSize(pageInfo, { width, height })` gives the size and zoom a thumbnail of that page gets.
|
|
448
|
+
|
|
449
|
+
### A list of pages
|
|
450
|
+
|
|
451
|
+
`createThumbnails(viewer, options)` lists the pages as thumbnails, each a button that scrolls the viewer to its page. It fills its parent's height and scrolls, so put it in a sidebar panel or any box with a height.
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
import { createThumbnails } from '@inkbar/core/dom'
|
|
455
|
+
|
|
456
|
+
panel.append(createThumbnails(viewer))
|
|
457
|
+
panel.append(createThumbnails(viewer, { pages: [0, 3, 7], width: 96 }))
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
`pages` lists those pages in that order (0-based); without it every page is listed. The thumbnails fill the list's width less a 16px gutter each side and follow it as it resizes; `width` fixes them at that many pixels instead. `annotations` and `marks` work as they do for one thumbnail. The current page has `aria-current="page"` and scrolls into view in the list when it changes, unless the list was scrolled by hand in the last second. The list is one Tab stop: arrow keys move between pages, Home and End go to the first and last, Enter or Space opens one.
|
|
461
|
+
|
|
462
|
+
Only the items near the visible part of the list are in the document, so a 500-page file stays cheap. When the list scrolls away from the focused item, focus moves to the first visible one without scrolling back.
|
|
463
|
+
|
|
464
|
+
To render your own items, use `createThumbnailsController({ document, pluginHost, viewport }, options)`. Its `state` gives the `pages` listed, the `current` page (-1 with no viewport), the `window` of indexes into `pages` to mount the `offsets` of each item from the top of the list (the last is the total height) and the `width` to draw each thumbnail at. Render a spacer of the total height and each item in the window at its offset, as a button with `data-ink-thumbnail-item` set to its page and a `createThumbnailPainter` at that `width` inside, wrapped in an element with `itemAttrs(index)` spread on it: its `listitem` role, and `aria-setsize` and `aria-posinset` for its place among all the pages, as only the window is mounted. An item's height is its thumbnail's plus 38px for the 16px gutter above it, a 6px gap and its 16px number, as the theme draws it. Call `attach(scrollElement)` once the list is in the document: it follows scrolling, handles clicks and keys, and sets each item's `tabindex`. `activate(page)` scrolls the viewer to a page, and `update(options)` takes new options.
|
|
465
|
+
|
|
466
|
+
## Record lists
|
|
467
|
+
|
|
468
|
+
`createRecordList(viewer, { records })` lists a plugin's records as cards, the same cards their marks open, with the same actions. It fills its parent's height and scrolls, so put it in a sidebar panel or any box with a height.
|
|
469
|
+
|
|
470
|
+
```ts
|
|
471
|
+
import { createRecordList, createSearchResults } from '@inkbar/core/dom'
|
|
472
|
+
|
|
473
|
+
panel.append(createRecordList(viewer, { records: 'review.suggestions' }))
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Each record takes its card from the first `plugin.marks` call over those records whose `where` it passes, and the list shows only records that pass one. Records that no `marks` call covers are all listed, titled with their id, with no actions. Pass `where`, `card` or `sort` to choose for yourself; actions on a card you pass run in one undo step too. The default order is by page, then top to bottom and left to right by the mark's rect.
|
|
477
|
+
|
|
478
|
+
The list and the marks share one selection per records key, so every list over the same records agrees with the marks. Clicking a card, or Enter on it, selects the record, gives its mark `data-selected` and scrolls the viewer to it. Focus stays in the list and no popover opens. Activating a mark selects its card and scrolls every list over those records to it, inside the list, without moving the page; a list in a closed panel scrolls when it shows. Escape in the list clears the selection, and so does removing the record or undoing it away. A `where` of yours that is stricter than the marks' hides a selected record from your list but leaves it selected, for the marks and other lists. Choosing a record that no `marks` call covers scrolls to it without selecting it.
|
|
479
|
+
|
|
480
|
+
The list is one Tab stop: arrow keys move between cards, Home and End go to the first and last. When the focused card goes, after an action, an undo or any other change, focus moves to the next card, else the previous one, else the list. A polite live region says "3 items" when an action or your app adds or removes cards, again when the count repeats, but not on undo or redo. With nothing listed it shows "Nothing here yet."
|
|
481
|
+
|
|
482
|
+
`createSearchResults(viewer)` is the same list over the viewer's search: one card per hit, titled with its page ("Page 2"), with its place on that page ("Match 3") and the text around it. Choosing a card makes its hit current, as next and previous in the find bar do, and the find bar's current hit is the selected card. It has its own query field, labelled "Find in document", above the cards and the find bar's match count in a footer below them. Both share the viewer's search state, so they never disagree. Pass `{ field: false }` to leave the field out when a find bar is on the page. A hit's id is `<page>:<n>`, its place among that page's hits, so it stays the same while the scan adds hits from other pages. Escape in the results does not clear the current hit. If you render your own field, `handleResultsFieldKey(event, search)` gives it the find bar's Enter and Shift+Enter. `isBlankQuery(query)` says whether a query counts as no query at all, as the search treats it: the list shows its empty text only then.
|
|
483
|
+
|
|
484
|
+
To render your own cards, build a source with `recordsSource({ pluginHost, viewport }, key, options)` or `searchSource(search)` and pass it to `createRecordListController(source)`. Its `rows` give each item, its card and whether it is selected, and `count` and `announcement` give the count and the live-region text. Render each row as an element with `data-ink-record` set to the item's id, call `attach(element)` on the element holding them, and call `act(id, label)` from each action button. `attach` handles clicks and keys, sets each row's `tabindex` and scrolls the selected row into view. `choose(id)` selects and reveals an item from code. A source of your own needs `items` (each with an `id`, a 0-based `page` and an optional `rect`), `card`, `selected`, `select`, `reveal`, `run` and `replaying`.
|
|
485
|
+
|
|
486
|
+
`@inkbar/theme` styles the list with `.ink-records`, `.ink-record-list`, `.ink-record` and `.ink-record-list-empty`, and the selected card with `aria-current="true"`.
|
|
487
|
+
|
|
488
|
+
`createThumbnail`, `createThumbnails`, `createRecordList` and `createSearchResults` follow the viewer and stop when it is destroyed, not when you remove their element. For a short-lived use, a dialog or a popover for instance, call `createThumbnailPainter(element, deps, options)` or `createRecordListController(source)` and its `destroy()` when you close it.
|
|
489
|
+
|
|
363
490
|
## Forms
|
|
364
491
|
|
|
365
492
|
Inkbar fills the AcroForm fields in a PDF and runs the form's scripts. `doc.form` reads and sets the values:
|