@inkbar/core 0.5.0 → 0.7.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 +111 -1
- package/dist/dom.d.mts +597 -40
- package/dist/dom.d.mts.map +1 -1
- package/dist/dom.mjs +2154 -436
- package/dist/dom.mjs.map +1 -1
- package/dist/index.d.mts +16 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +181 -25
- package/dist/index.mjs.map +1 -1
- package/dist/{signatures-BwaXi7C8.d.mts → signatures-CalOktkf.d.mts} +148 -13
- package/dist/signatures-CalOktkf.d.mts.map +1 -0
- package/dist/{signatures-CMapTKrC.mjs → signatures-Do_l2IlM.mjs} +882 -56
- package/dist/signatures-Do_l2IlM.mjs.map +1 -0
- package/package.json +4 -3
- package/dist/signatures-BwaXi7C8.d.mts.map +0 -1
- package/dist/signatures-CMapTKrC.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ viewer.open(lockedFile, { password }) // this password applies to this load only
|
|
|
103
103
|
viewer.destroy()
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `onReady` and `onError`. 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` and `
|
|
106
|
+
It takes the [`mountViewer` options](#mounting-a-viewer) except `container` and `document`, plus `source` and `assets` (both required), `password`, `defaults`, `history`, `onReady` and `onError`. 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. `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"`.
|
|
107
107
|
|
|
108
108
|
`createViewer` reads the theme's colours from the element you pass, so that element needs to be in the page when you call it.
|
|
109
109
|
|
|
@@ -228,6 +228,116 @@ With either select tool (`select-text` or `select`), or on what a one-shot tool
|
|
|
228
228
|
|
|
229
229
|
A press on text markup (a highlight, underline, strikeout or squiggly) does nothing at first. A click selects it; a drag selects the text under it, as a drag on the bare text would. Presses on every other annotation select it straight away.
|
|
230
230
|
|
|
231
|
+
## Plugins
|
|
232
|
+
|
|
233
|
+
A plugin adds commands, tools, annotation decorations, page layers and its own records to a viewer. Write it once with `definePlugin` from `@inkbar/core/dom` and it runs in every framework package and in `createViewer`.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import type { RecordBase, Rect } from '@inkbar/core'
|
|
237
|
+
import { definePlugin } from '@inkbar/core/dom'
|
|
238
|
+
|
|
239
|
+
interface Suggestion extends RecordBase {
|
|
240
|
+
page: number // 0-based
|
|
241
|
+
rect: Rect // in PDF user space
|
|
242
|
+
reason: string
|
|
243
|
+
status: 'suggested' | 'dismissed'
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The id is lower-case kebab; everything the plugin adds is named under it.
|
|
247
|
+
export const review = definePlugin('review', (plugin) => {
|
|
248
|
+
// The open document's `review.suggestions` records; empty while none is open.
|
|
249
|
+
const suggestions = plugin.records<Suggestion>('suggestions')
|
|
250
|
+
|
|
251
|
+
// Placed as 'review.suggest'. The returned string becomes the command's status.
|
|
252
|
+
plugin.command('suggest', 'Suggest redactions', async ({ doc, signal }) => {
|
|
253
|
+
const found = await findSuggestions(doc, { signal }) // your own code
|
|
254
|
+
return `${suggestions.in(doc).addMissing(found).length} suggestions`
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// A button over each open suggestion, opening a card styled by `@inkbar/theme`.
|
|
258
|
+
plugin.marks(suggestions, {
|
|
259
|
+
where: (s) => s.status === 'suggested',
|
|
260
|
+
rect: (s) => s.rect,
|
|
261
|
+
label: (s) => `Suggested redaction: ${s.reason}`,
|
|
262
|
+
card: (s) => ({
|
|
263
|
+
title: s.reason,
|
|
264
|
+
actions: { Dismiss: () => suggestions.update(s.id, { status: 'dismissed' }) },
|
|
265
|
+
}),
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
`definePlugin(id, setup)` checks the id and returns a factory. Pass `plugins: [review()]` to the viewer. Nothing a plugin registers shows by itself: place a command with `InkCommand` (`createCommandButton(viewer, 'review.suggest')` without a framework), and a plugin tool with a tool button, as you place the built-in ones.
|
|
271
|
+
|
|
272
|
+
Inkbar keeps no plugin data. The host loads and saves a plugin's records through the factory's `records` option, which `plugin.records(name)` picks up by name: `review({ records: { suggestions: { initial, onChange } } })`. `initial(doc)` returns the records a document opens with; `onChange(list, change)` runs after every change. Options passed to `plugin.records(name, { initial, onChange })` win over the host's.
|
|
273
|
+
|
|
274
|
+
Three rules cover undo and stale results:
|
|
275
|
+
|
|
276
|
+
- Every records call is one undo step, and `plugin.batch` makes several one step.
|
|
277
|
+
- Inside a command, write through `records.in(doc)`. A write through the plain handle after an `await` goes to whatever document is open by then; `in(doc)` writes to the run's document, and nothing once that document closes.
|
|
278
|
+
- Each card action is one undo step.
|
|
279
|
+
|
|
280
|
+
A command's `run` gets `doc`, the document open when it started, and a `signal` that aborts when that document closes or another opens. Returning `undefined` clears the status. A failed run is logged and sets the status to `errorStatus` (default "Something went wrong."); the status clears when the document changes.
|
|
281
|
+
|
|
282
|
+
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.
|
|
283
|
+
|
|
284
|
+
`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.
|
|
285
|
+
|
|
286
|
+
`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.
|
|
287
|
+
|
|
288
|
+
### Going further
|
|
289
|
+
|
|
290
|
+
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]`.
|
|
291
|
+
|
|
292
|
+
A command added with `ctx.commands.add` is disabled until a document is ready, unless it passes its own `enabled` readable, and it is busy, and disabled, until the promise `run` returns settles. A failed run is logged, never thrown. `ctx.store(name)` gives each document its own record store, keyed `<plugin>.<name>`, and its changes undo with annotation edits; `ctx.onAnnotationEdit(fn)` runs inside each user edit's undo step. `ctx.history.batch(fn)` makes everything `fn` changes, annotations and records alike, one undo step. Load records with `initial(doc)` and save them from `onChange`. `ctx.scrollTo(page, rect)` scrolls the viewer to a rect in user space and moves no focus.
|
|
293
|
+
|
|
294
|
+
#### Plugin authors
|
|
295
|
+
|
|
296
|
+
- A layer (`ctx.layer`) lets presses through to the page, and the elements you put straight into it take them back (`pointer-events: auto`), with or without `@inkbar/theme`. That default has no specificity, so a rule of yours such as `pointer-events: none` on a mark wins.
|
|
297
|
+
- A tool you add with `ctx.tools.add` is placed under its namespaced id (`review.stamp`), but its methods run on your own object, so inside them `this.id` is the id you gave it (`stamp`). A class instance keeps its `#private` fields.
|
|
298
|
+
- Presses on a mark you draw in your own layer bubble up to the viewer's root. Stop propagation if listeners there shouldn't see them. `plugin.marks` does this for you.
|
|
299
|
+
- A layer's `destroy` can run after the document's stores have closed, so check `store.get()` for `null` there.
|
|
300
|
+
- 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.
|
|
301
|
+
- 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.
|
|
302
|
+
|
|
303
|
+
## Search
|
|
304
|
+
|
|
305
|
+
`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.
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
for await (const hit of doc.search('invoice')) {
|
|
309
|
+
console.log(hit.page, hit.text, `${hit.before}[${hit.text}]${hit.after}`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const refs = await doc.search(/\b[A-Z]{2}\d{6}[A-D]\b/, { signal }).all()
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Options: `matchCase` (default false; a `RegExp` keeps its own `i` flag), `wholeWord` (a hit must not have a letter, digit or underscore on either side), `pages` (indices to search, in the order given; all pages in order by default) and `signal`, which stops the scan. A string matches the way the page reads: it finds text across line breaks and a hyphenated line end, and ignores accents, ligatures and curly quotes.
|
|
316
|
+
|
|
317
|
+
Each `SearchHit` has:
|
|
318
|
+
|
|
319
|
+
- `page`: 0-based page index.
|
|
320
|
+
- `start`, `end`: offsets into `doc.pageText(page)`.
|
|
321
|
+
- `text`: the page text the hit covers.
|
|
322
|
+
- `quads`, `rect`: where it is, in PDF user space. `quads` has one entry per run of text the hit touches, and can go straight into a text markup annotation. Each quad is cut where the page draws its glyphs and spans the font's full height, so it covers ascenders and descenders.
|
|
323
|
+
- `exact`: `true` when every quad comes from where the page draws its glyphs. `false` when some of the text could not be matched to them (right-to-left or vertical text, or an unusual file) and its quad is cut at estimated widths, which can fall short. Pad those before using them to cover text, as a redaction does.
|
|
324
|
+
- `before`, `after`: up to 40 characters of page text either side.
|
|
325
|
+
|
|
326
|
+
`viewer.search` is the state and actions behind the viewer's find bar: `state` (a readable of `open`, `query`, `matchCase`, `hits`, `current` and `scanning`), `open(query?)`, `setQuery`, `setMatchCase`, `next`, `previous` and `close`. `open()` runs the search and draws the highlights even with no find bar on the page. Plugins get the same object as `ctx.search`.
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
import { createFindBar, createSearchButton } from '@inkbar/core/dom'
|
|
330
|
+
|
|
331
|
+
toolbar.append(createSearchButton(viewer))
|
|
332
|
+
viewer.element.querySelector('[data-ink-viewport-root]')!.append(createFindBar(viewer))
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`createSearchButton(viewer)` opens the bar. `createFindBar(viewer)` is a field with a match count, previous and next, a match-case toggle and close. Put it inside `[data-ink-viewport-root]`; the theme floats it over the top-right of the pages. Enter steps to the next match, `Shift+Enter` to the previous, and `Escape` closes and returns focus to where it was.
|
|
336
|
+
|
|
337
|
+
Ctrl/Cmd+F opens the bar when one is mounted and the key comes from inside the viewer, with the page selection (up to 200 characters) as the query. Without a bar, or from outside the viewer, the browser keeps the key.
|
|
338
|
+
|
|
339
|
+
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`.
|
|
340
|
+
|
|
231
341
|
## Signatures
|
|
232
342
|
|
|
233
343
|
A signature here is a mark on the page, not a cryptographic signature of the file. Draw one, type your name in a handwriting font, or upload a scan, and it is saved as a template. The `signature` tool places the chosen template: a click puts it about 150 pt wide, centred on the press, and `Enter` puts it at the centre of the current page. It is turned against the page's `/Rotate`, so it reads upright on the page; a view rotation turns it with the page. A drawn signature becomes `ink` and the others a `stamp` (an image), both with `subject: 'Signature'`. Both keep their aspect ratio when resized.
|