@inkbar/svelte 0.6.0 → 0.8.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 +13 -0
- package/dist/components/InkFindBar.svelte +213 -0
- package/dist/components/InkFindBar.svelte.d.ts +6 -0
- package/dist/components/InkFindBar.svelte.d.ts.map +1 -0
- package/dist/components/InkFormAlert.svelte +108 -0
- package/dist/components/InkFormAlert.svelte.d.ts +13 -0
- package/dist/components/InkFormAlert.svelte.d.ts.map +1 -0
- package/dist/components/InkRoot.svelte +136 -4
- package/dist/components/InkRoot.svelte.d.ts +22 -2
- package/dist/components/InkRoot.svelte.d.ts.map +1 -1
- package/dist/components/InkSearchButton.svelte +56 -0
- package/dist/components/InkSearchButton.svelte.d.ts +5 -0
- package/dist/components/InkSearchButton.svelte.d.ts.map +1 -0
- package/dist/components/InkSignFieldPopover.svelte +187 -0
- package/dist/components/InkSignFieldPopover.svelte.d.ts +10 -0
- package/dist/components/InkSignFieldPopover.svelte.d.ts.map +1 -0
- package/dist/components/InkSignatureDialog.svelte +11 -3
- package/dist/components/InkSignatureDialog.svelte.d.ts +9 -3
- package/dist/components/InkSignatureDialog.svelte.d.ts.map +1 -1
- package/dist/components/InkSignatureMenu.svelte +10 -99
- package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkViewport.svelte +46 -1
- package/dist/components/InkViewport.svelte.d.ts.map +1 -1
- package/dist/components/SignatureForm.svelte +7 -2
- package/dist/components/SignatureForm.svelte.d.ts +2 -0
- package/dist/components/SignatureForm.svelte.d.ts.map +1 -1
- package/dist/components/SignatureMenuPanel.svelte +127 -0
- package/dist/components/SignatureMenuPanel.svelte.d.ts +13 -0
- package/dist/components/SignatureMenuPanel.svelte.d.ts.map +1 -0
- package/dist/components/search-button.d.ts +10 -0
- package/dist/components/search-button.d.ts.map +1 -0
- package/dist/components/search-button.js +1 -0
- package/dist/context.d.ts +14 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/hooks/useForm.svelte.d.ts +17 -0
- package/dist/hooks/useForm.svelte.d.ts.map +1 -0
- package/dist/hooks/useForm.svelte.js +20 -0
- package/dist/hooks/useSearch.svelte.d.ts +11 -0
- package/dist/hooks/useSearch.svelte.d.ts.map +1 -0
- package/dist/hooks/useSearch.svelte.js +31 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -145,6 +145,12 @@ Inside `InkRoot`, `useHistory()` returns the same object. Its `batch(fn)` runs `
|
|
|
145
145
|
|
|
146
146
|
`key` identifies the document. Reuse the same key when only the signed URL changes and the document is not reloaded.
|
|
147
147
|
|
|
148
|
+
## Forms
|
|
149
|
+
|
|
150
|
+
`InkViewport` fills the PDF's form fields and runs the form's scripts. `useForm()` returns the fields, their values, and `set` and `reset`. Use `bind:fields` to hold the values in your own state. The form never submits, opens a link, sends mail or prints: `onformaction` hands each request to you. `formHighlight={false}` turns off the field tint, and a change applies to the open document in place.
|
|
151
|
+
|
|
152
|
+
The [forms guide](https://inkbar.dev/docs/guides/forms/) covers controlled fields, form actions, scripts, signature fields and saving values as Gloss.
|
|
153
|
+
|
|
148
154
|
## Thread popover
|
|
149
155
|
|
|
150
156
|
`InkThreadPopover` opens the comment thread of the activated note or text markup (a highlight, underline, strikeout or squiggly). A new note or highlight has no text yet, so the form first writes its own `contents` (labelled "Comment"); after that it adds replies. A highlight with a comment shows a small marker at its end. Replies show in the thread only; the page draws just the note or highlight they answer.
|
|
@@ -175,6 +181,12 @@ A plugin from `definePlugin` (in `@inkbar/core/dom`) adds commands, tools, decor
|
|
|
175
181
|
|
|
176
182
|
Plugin stores undo with annotations. In controlled mode with `history`, each undo step also snapshots the plugin stores, so accepting something and undoing it restores both.
|
|
177
183
|
|
|
184
|
+
## Search
|
|
185
|
+
|
|
186
|
+
`InkFindBar` is the find bar and `InkSearchButton` opens it. Place the bar anywhere inside `InkRoot`, once `InkViewport` is there; Ctrl/Cmd+F opens it while it is mounted. `useSearch()` returns the search `state` and its actions (`open`, `setQuery`, `setMatchCase`, `next`, `previous`, `close`) for your own control. See the [`@inkbar/core` README](https://github.com/inkbar/inkbar/tree/main/packages/core#search) for the API.
|
|
187
|
+
|
|
188
|
+
The bar handles Escape itself and stops it there, so an `onkeydown` you pass to `InkFindBar` never sees Escape.
|
|
189
|
+
|
|
178
190
|
## Custom rendering
|
|
179
191
|
|
|
180
192
|
Slots are snippets. `InkViewport` takes `loading`, `empty`, `error`, `annotation` and `pageError`. `InkThreadPopover` takes `children` as a snippet that receives the popover's props. `InkToolButton` takes `children` as a snippet of `{ active, label }`, and `InkCommand` one of `{ label, busy }`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
|
|
@@ -196,6 +208,7 @@ The scroll area takes focus. From there:
|
|
|
196
208
|
- With the `highlight` or `redact-area` tool active, `Enter` turns the text selection into a mark.
|
|
197
209
|
- With the `note` tool active, `Enter` places a note at the centre of the current page and opens it.
|
|
198
210
|
- With the `signature` tool active, `Enter` places the chosen signature at the centre of the current page.
|
|
211
|
+
- `Ctrl` or `Cmd` + `Z` undoes. `Ctrl` or `Cmd` + `Shift` + `Z`, or `Ctrl` + `Y`, redoes. `Ctrl` and `Cmd` both work on every platform. The keys do what `InkUndo` and `InkRedo` do, from anywhere in `InkRoot`, toolbars included, and act only while that button is enabled. In a text field or editable content the keys stay with the element, so a field keeps its own text undo. In a `<select>` they are left to the browser, which does nothing with them.
|
|
199
212
|
|
|
200
213
|
On a focused annotation:
|
|
201
214
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
3
|
+
|
|
4
|
+
export type InkFindBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'>
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
/*
|
|
9
|
+
* The find bar: a field, the match count, previous and next, a match-case toggle and close,
|
|
10
|
+
* a view of the root's search state. Place it anywhere inside `InkRoot`: it portals itself
|
|
11
|
+
* into `[data-ink-viewport-root]`, the positioned ancestor the theme floats it from over the
|
|
12
|
+
* top-right of the pages (as `InkThreadPopover` anchors to the root's overlay rather than
|
|
13
|
+
* where it is written). It renders nothing until `InkViewport` has mounted, and is hidden
|
|
14
|
+
* while the search is closed. While it is mounted it registers with the search controller,
|
|
15
|
+
* which is what lets Ctrl/Cmd+F open it.
|
|
16
|
+
*
|
|
17
|
+
* Enter in the field steps to the next match, Shift+Enter to the previous, Escape closes.
|
|
18
|
+
* Closing returns focus to where it was before the bar opened, else to the document.
|
|
19
|
+
*/
|
|
20
|
+
import { findAnnouncer, findCountText } from '@inkbar/core/dom'
|
|
21
|
+
import { CaseSensitive, ChevronDown, ChevronUp, X } from '@inkbar/icons'
|
|
22
|
+
import { untrack } from 'svelte'
|
|
23
|
+
|
|
24
|
+
import { useInkContext } from '../context.js'
|
|
25
|
+
import { useSearch } from '../hooks/useSearch.svelte.js'
|
|
26
|
+
import { useReadable } from '../use-readable.svelte.js'
|
|
27
|
+
|
|
28
|
+
import InkIcon from './InkIcon.svelte'
|
|
29
|
+
|
|
30
|
+
import type { SearchState } from '@inkbar/core/dom'
|
|
31
|
+
|
|
32
|
+
let { class: className, ...rest }: InkFindBarProps = $props()
|
|
33
|
+
|
|
34
|
+
const ctx = useInkContext('InkFindBar')
|
|
35
|
+
const search = useSearch()
|
|
36
|
+
const scroll = useReadable(() => ctx.viewportEl, null)
|
|
37
|
+
/** The viewport root: the scroll element's parent, once InkViewport has mounted. */
|
|
38
|
+
const target = $derived(scroll.current?.parentElement ?? null)
|
|
39
|
+
|
|
40
|
+
let bar = $state<HTMLDivElement | null>(null)
|
|
41
|
+
let field = $state<HTMLInputElement | null>(null)
|
|
42
|
+
let live = $state<HTMLDivElement | null>(null)
|
|
43
|
+
/** Where focus was when the bar opened, and whether it was open on the last pass. */
|
|
44
|
+
let before: HTMLElement | null = null
|
|
45
|
+
let wasOpen = false
|
|
46
|
+
|
|
47
|
+
const open = $derived(search.state.open)
|
|
48
|
+
const query = $derived(search.state.query)
|
|
49
|
+
|
|
50
|
+
// After the update, so the bar is shown (and can take focus) when it opens.
|
|
51
|
+
$effect(() => {
|
|
52
|
+
const el = bar
|
|
53
|
+
const input = field
|
|
54
|
+
const isOpen = open
|
|
55
|
+
const text = query
|
|
56
|
+
if (!el || !input) return
|
|
57
|
+
untrack(() => {
|
|
58
|
+
if (isOpen !== wasOpen) {
|
|
59
|
+
wasOpen = isOpen
|
|
60
|
+
const doc = el.ownerDocument
|
|
61
|
+
if (isOpen) {
|
|
62
|
+
const active = doc.activeElement
|
|
63
|
+
before =
|
|
64
|
+
active instanceof HTMLElement && active !== doc.body && !el.contains(active)
|
|
65
|
+
? active
|
|
66
|
+
: null
|
|
67
|
+
input.focus()
|
|
68
|
+
input.select()
|
|
69
|
+
} else {
|
|
70
|
+
// Focus inside the bar is about to be lost with it; focus elsewhere is left alone.
|
|
71
|
+
const hadFocus =
|
|
72
|
+
el.contains(doc.activeElement) || doc.activeElement === doc.body
|
|
73
|
+
if (hadFocus) {
|
|
74
|
+
const to =
|
|
75
|
+
before?.isConnected && !el.contains(before) ? before : scroll.current
|
|
76
|
+
to?.focus({ preventScroll: true })
|
|
77
|
+
}
|
|
78
|
+
before = null
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (input.value !== text) {
|
|
82
|
+
input.value = text
|
|
83
|
+
// A query set from code arrives after the bar opened: keep the new text selected.
|
|
84
|
+
if (el.ownerDocument.activeElement === input) input.select()
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
// Registered while mounted.
|
|
90
|
+
$effect(() => {
|
|
91
|
+
const controller = ctx.search
|
|
92
|
+
if (!controller || !target) return
|
|
93
|
+
return controller.attachBar()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// The live region follows the controller's state change by change, as the vanilla bar does,
|
|
97
|
+
// and is written directly. An update shows only the last of the changes batched into it, so
|
|
98
|
+
// two steps in one task that end where they began would otherwise say nothing.
|
|
99
|
+
$effect(() => {
|
|
100
|
+
const controller = ctx.search
|
|
101
|
+
const el = live
|
|
102
|
+
if (!controller || !el) return
|
|
103
|
+
return untrack(() => {
|
|
104
|
+
const announce = findAnnouncer()
|
|
105
|
+
const say = (s: SearchState): void => {
|
|
106
|
+
const text = announce(s)
|
|
107
|
+
if (el.textContent !== text) el.textContent = text
|
|
108
|
+
}
|
|
109
|
+
say(controller.state.get())
|
|
110
|
+
return controller.state.subscribe(say)
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// A native listener, so Escape stops at the bar as it does in the vanilla and Vue bars;
|
|
115
|
+
// Svelte's delegated events would stop it only after the root's own listeners saw it.
|
|
116
|
+
$effect(() => {
|
|
117
|
+
const controller = ctx.search
|
|
118
|
+
const el = bar
|
|
119
|
+
if (!controller || !el) return
|
|
120
|
+
const onKeydown = (e: KeyboardEvent): void => {
|
|
121
|
+
if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return
|
|
122
|
+
e.preventDefault()
|
|
123
|
+
e.stopPropagation()
|
|
124
|
+
controller.close()
|
|
125
|
+
}
|
|
126
|
+
el.addEventListener('keydown', onKeydown)
|
|
127
|
+
return () => el.removeEventListener('keydown', onKeydown)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Moves the bar itself into the viewport root, leaving nothing where it is written. Svelte
|
|
132
|
+
* removes it when its own `{#if}` branch goes, but not when an ancestor's block goes and
|
|
133
|
+
* takes only the nodes still in place, so the cleanup removes it too. A keyed `{#each}` that
|
|
134
|
+
* reorders would move the bar back to where it is written, so keep `InkFindBar` out of one.
|
|
135
|
+
*/
|
|
136
|
+
const into = (to: Element) => (el: HTMLElement) => {
|
|
137
|
+
to.appendChild(el)
|
|
138
|
+
return () => el.remove()
|
|
139
|
+
}
|
|
140
|
+
</script>
|
|
141
|
+
|
|
142
|
+
{#if target}
|
|
143
|
+
<div
|
|
144
|
+
{...rest}
|
|
145
|
+
bind:this={bar}
|
|
146
|
+
class={['ink-find-bar', className]}
|
|
147
|
+
data-ink-find-bar=""
|
|
148
|
+
role="search"
|
|
149
|
+
hidden={!open}
|
|
150
|
+
{@attach into(target)}
|
|
151
|
+
>
|
|
152
|
+
<input
|
|
153
|
+
bind:this={field}
|
|
154
|
+
type="text"
|
|
155
|
+
aria-label="Find in document"
|
|
156
|
+
autocomplete="off"
|
|
157
|
+
spellcheck={false}
|
|
158
|
+
oninput={(e) => search.setQuery(e.currentTarget.value)}
|
|
159
|
+
onkeydown={(e) => {
|
|
160
|
+
if (e.key !== 'Enter' || e.isComposing) return
|
|
161
|
+
e.preventDefault()
|
|
162
|
+
if (e.shiftKey) search.previous()
|
|
163
|
+
else search.next()
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
<span class="ink-find-count" data-ink-find-count=""
|
|
167
|
+
>{findCountText(search.state)}</span
|
|
168
|
+
>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
data-ink-find-previous=""
|
|
172
|
+
aria-label="Previous match"
|
|
173
|
+
title="Previous match"
|
|
174
|
+
onclick={() => search.previous()}
|
|
175
|
+
>
|
|
176
|
+
<InkIcon icon={ChevronUp} />
|
|
177
|
+
</button>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
data-ink-find-next=""
|
|
181
|
+
aria-label="Next match"
|
|
182
|
+
title="Next match"
|
|
183
|
+
onclick={() => search.next()}
|
|
184
|
+
>
|
|
185
|
+
<InkIcon icon={ChevronDown} />
|
|
186
|
+
</button>
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
data-ink-find-match-case=""
|
|
190
|
+
aria-label="Match case"
|
|
191
|
+
title="Match case"
|
|
192
|
+
aria-pressed={search.state.matchCase}
|
|
193
|
+
onclick={() => search.setMatchCase(!search.state.matchCase)}
|
|
194
|
+
>
|
|
195
|
+
<InkIcon icon={CaseSensitive} />
|
|
196
|
+
</button>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
data-ink-find-close=""
|
|
200
|
+
aria-label="Close find"
|
|
201
|
+
title="Close find"
|
|
202
|
+
onclick={() => search.close()}
|
|
203
|
+
>
|
|
204
|
+
<InkIcon icon={X} />
|
|
205
|
+
</button>
|
|
206
|
+
<div
|
|
207
|
+
bind:this={live}
|
|
208
|
+
class="ink-visually-hidden"
|
|
209
|
+
data-ink-find-status=""
|
|
210
|
+
aria-live="polite"
|
|
211
|
+
></div>
|
|
212
|
+
</div>
|
|
213
|
+
{/if}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
export type InkFindBarProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
|
|
3
|
+
declare const InkFindBar: import("svelte").Component<InkFindBarProps, {}, "">;
|
|
4
|
+
type InkFindBar = ReturnType<typeof InkFindBar>;
|
|
5
|
+
export default InkFindBar;
|
|
6
|
+
//# sourceMappingURL=InkFindBar.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InkFindBar.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkFindBar.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,UAAU,CAAC,CAC/E;AAmLD,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* A form script's `app.alert`: the message and OK, shown without blocking (no overlay, not
|
|
4
|
+
* modal), so the form stays usable around it. InkViewport mounts it; hosts compose nothing.
|
|
5
|
+
* OK, or Escape while focus is in the alert, closes it, and focus goes back where it was. A
|
|
6
|
+
* press outside leaves it open, and Escape anywhere else stays with the viewer. A new message
|
|
7
|
+
* replaces the one shown.
|
|
8
|
+
*
|
|
9
|
+
* The markup is core's `createFormAlert`, portalled to `<body>`. It is not a Bits UI dialog:
|
|
10
|
+
* Bits UI's escape layer takes every Escape on the page while it is open (it prevents the
|
|
11
|
+
* key's default), which would end the viewer's own use of the key.
|
|
12
|
+
*/
|
|
13
|
+
import { FORM_LABELS } from '@inkbar/core/dom'
|
|
14
|
+
import { Portal } from 'bits-ui'
|
|
15
|
+
import { untrack } from 'svelte'
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
message,
|
|
19
|
+
shown,
|
|
20
|
+
fallback,
|
|
21
|
+
onClose,
|
|
22
|
+
}: {
|
|
23
|
+
/** The message to show (text, not markup); `null` while none is. */
|
|
24
|
+
message: string | null
|
|
25
|
+
/** Counts the times a message was shown, so the same text shown again takes focus again. */
|
|
26
|
+
shown: number
|
|
27
|
+
/** Where focus goes when nothing it can return to is left. */
|
|
28
|
+
fallback: () => HTMLElement | null
|
|
29
|
+
onClose: () => void
|
|
30
|
+
} = $props()
|
|
31
|
+
|
|
32
|
+
const uid = $props.id()
|
|
33
|
+
let panel = $state<HTMLElement | null>(null)
|
|
34
|
+
let ok = $state<HTMLButtonElement | null>(null)
|
|
35
|
+
/** Where focus was when the alert opened. */
|
|
36
|
+
let returnTo: HTMLElement | null = null
|
|
37
|
+
let wasOpen = false
|
|
38
|
+
const open = $derived(message !== null)
|
|
39
|
+
|
|
40
|
+
// After the alert renders, before anything else moves focus: where focus was is read first.
|
|
41
|
+
$effect(() => {
|
|
42
|
+
const showing = open
|
|
43
|
+
void shown
|
|
44
|
+
const button = ok
|
|
45
|
+
if (!showing) {
|
|
46
|
+
wasOpen = false
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
if (!button) return
|
|
50
|
+
untrack(() => {
|
|
51
|
+
if (!wasOpen) {
|
|
52
|
+
wasOpen = true
|
|
53
|
+
const active = document.activeElement
|
|
54
|
+
returnTo =
|
|
55
|
+
active instanceof HTMLElement && active !== document.body ? active : null
|
|
56
|
+
}
|
|
57
|
+
button.focus({ preventScroll: true })
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
function close(): void {
|
|
62
|
+
// Focus that moved elsewhere while the alert showed stays there.
|
|
63
|
+
const hadFocus = !!panel?.contains(document.activeElement)
|
|
64
|
+
const back = returnTo?.isConnected === true ? returnTo : fallback()
|
|
65
|
+
onClose()
|
|
66
|
+
if (hadFocus) back?.focus({ preventScroll: true })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onkeydown(e: KeyboardEvent): void {
|
|
70
|
+
if (e.key !== 'Escape' || e.defaultPrevented) return
|
|
71
|
+
e.preventDefault()
|
|
72
|
+
e.stopPropagation()
|
|
73
|
+
close()
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
{#if message !== null}
|
|
78
|
+
<Portal>
|
|
79
|
+
<div
|
|
80
|
+
bind:this={panel}
|
|
81
|
+
role="alertdialog"
|
|
82
|
+
aria-labelledby="{uid}-title"
|
|
83
|
+
aria-describedby="{uid}-message"
|
|
84
|
+
class="ink-dialog ink-form-alert"
|
|
85
|
+
data-ink-form-alert=""
|
|
86
|
+
data-state="open"
|
|
87
|
+
tabindex="-1"
|
|
88
|
+
{onkeydown}
|
|
89
|
+
>
|
|
90
|
+
<h2 id="{uid}-title" class="ink-dialog-title">{FORM_LABELS.alert}</h2>
|
|
91
|
+
<p id="{uid}-message" class="ink-form-alert-message" data-ink-form-alert-message="">
|
|
92
|
+
{message}
|
|
93
|
+
</p>
|
|
94
|
+
<div class="ink-dialog-actions">
|
|
95
|
+
<button
|
|
96
|
+
bind:this={ok}
|
|
97
|
+
type="button"
|
|
98
|
+
class="ink-button"
|
|
99
|
+
data-variant="primary"
|
|
100
|
+
data-ink-form-alert-ok=""
|
|
101
|
+
onclick={close}
|
|
102
|
+
>
|
|
103
|
+
{FORM_LABELS.ok}
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</Portal>
|
|
108
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** The message to show (text, not markup); `null` while none is. */
|
|
3
|
+
message: string | null;
|
|
4
|
+
/** Counts the times a message was shown, so the same text shown again takes focus again. */
|
|
5
|
+
shown: number;
|
|
6
|
+
/** Where focus goes when nothing it can return to is left. */
|
|
7
|
+
fallback: () => HTMLElement | null;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const InkFormAlert: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type InkFormAlert = ReturnType<typeof InkFormAlert>;
|
|
12
|
+
export default InkFormAlert;
|
|
13
|
+
//# sourceMappingURL=InkFormAlert.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InkFormAlert.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkFormAlert.svelte.ts"],"names":[],"mappings":"AAkBC,KAAK,gBAAgB,GAAI;IACtB,oEAAoE;IACpE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,WAAW,GAAG,IAAI,CAAA;IAClC,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAC;AA6EJ,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
AnnotationChange,
|
|
6
6
|
AnnotationDefaults,
|
|
7
7
|
DocumentSource,
|
|
8
|
+
FieldsChange,
|
|
9
|
+
FieldValues,
|
|
10
|
+
FormAction,
|
|
8
11
|
InkDocument,
|
|
9
12
|
InkError,
|
|
10
13
|
InkStatus,
|
|
@@ -38,6 +41,21 @@
|
|
|
38
41
|
annotations?: Annotation[] | undefined
|
|
39
42
|
onAnnotationsChange?: ((next: Annotation[]) => void) | undefined
|
|
40
43
|
onChange?: ((change: AnnotationChange) => void) | undefined
|
|
44
|
+
/**
|
|
45
|
+
* Pass (with `bind:`) to put the form's field values under the host's control. A commit
|
|
46
|
+
* shows at once and is reported after; the host's push wins. The mode is fixed when a
|
|
47
|
+
* document opens, as for `annotations`.
|
|
48
|
+
*/
|
|
49
|
+
fields?: FieldValues | undefined
|
|
50
|
+
/**
|
|
51
|
+
* Committed field values, in full, with what changed them. Called only while the fields
|
|
52
|
+
* are controlled, after the bound value is written.
|
|
53
|
+
*/
|
|
54
|
+
onFieldsChange?: ((next: FieldValues, change: FieldsChange) => void) | undefined
|
|
55
|
+
/** The form asked to submit, open a link, send mail, print or prompt. Inkbar does none. */
|
|
56
|
+
onformaction?: ((action: FormAction) => void) | undefined
|
|
57
|
+
/** Tints fillable form fields. Default true. A change applies to the open document in place. */
|
|
58
|
+
formHighlight?: boolean | undefined
|
|
41
59
|
tool?: ToolId | undefined
|
|
42
60
|
onToolChange?: ((tool: ToolId) => void) | undefined
|
|
43
61
|
/**
|
|
@@ -78,6 +96,11 @@
|
|
|
78
96
|
/** Undo history. Defaults to on for uncontrolled, off for controlled. */
|
|
79
97
|
history?: boolean | undefined
|
|
80
98
|
onReady?: ((doc: InkDocument) => void) | undefined
|
|
99
|
+
/**
|
|
100
|
+
* Opening failed, or later: a form script failed (`FormScriptError`; the form stays
|
|
101
|
+
* fillable), or a signature field refused its signing (`FieldSigned`, `FieldReadOnly`).
|
|
102
|
+
* An error from while the document opened comes after `onReady`.
|
|
103
|
+
*/
|
|
81
104
|
onError?: ((error: InkError) => void) | undefined
|
|
82
105
|
onActivate?: ((id: string) => void) | undefined
|
|
83
106
|
/** The root element, for hosts that bind it. */
|
|
@@ -102,12 +125,18 @@
|
|
|
102
125
|
writable,
|
|
103
126
|
} from '@inkbar/core'
|
|
104
127
|
import {
|
|
128
|
+
attachHistoryKeys,
|
|
105
129
|
createPluginHost,
|
|
130
|
+
createSearchController,
|
|
106
131
|
createToolOptions,
|
|
107
132
|
cssVariableDefaults,
|
|
108
133
|
FONT_FAMILIES,
|
|
134
|
+
handleFindKey,
|
|
135
|
+
keepHistoryFocus,
|
|
136
|
+
pageSelectionText,
|
|
109
137
|
SIGNATURE_FONTS,
|
|
110
138
|
trackToolStyle,
|
|
139
|
+
viewerSearchOf,
|
|
111
140
|
} from '@inkbar/core/dom'
|
|
112
141
|
import { DEV } from 'esm-env'
|
|
113
142
|
import { untrack } from 'svelte'
|
|
@@ -117,7 +146,12 @@
|
|
|
117
146
|
import { useReadable } from '../use-readable.svelte.js'
|
|
118
147
|
|
|
119
148
|
import type { PluginHistory } from '@inkbar/core'
|
|
120
|
-
import type {
|
|
149
|
+
import type {
|
|
150
|
+
PluginHost,
|
|
151
|
+
SearchController,
|
|
152
|
+
ToolController,
|
|
153
|
+
Viewport,
|
|
154
|
+
} from '@inkbar/core/dom'
|
|
121
155
|
|
|
122
156
|
let {
|
|
123
157
|
src,
|
|
@@ -127,6 +161,10 @@
|
|
|
127
161
|
annotations = $bindable(),
|
|
128
162
|
onAnnotationsChange,
|
|
129
163
|
onChange,
|
|
164
|
+
fields = $bindable(),
|
|
165
|
+
onFieldsChange,
|
|
166
|
+
onformaction,
|
|
167
|
+
formHighlight,
|
|
130
168
|
tool = $bindable(),
|
|
131
169
|
onToolChange,
|
|
132
170
|
tools: hostTools,
|
|
@@ -228,6 +266,7 @@
|
|
|
228
266
|
let opened: InkDocument | null = null
|
|
229
267
|
/** The snapshot history's hold on a controlled document's store. */
|
|
230
268
|
let detach: () => void = noop
|
|
269
|
+
let stopFormErrors: () => void = noop
|
|
231
270
|
untrack(() => snapshotHistory.clear())
|
|
232
271
|
const source = untrack(() => src)
|
|
233
272
|
if (source === null) {
|
|
@@ -260,6 +299,20 @@
|
|
|
260
299
|
},
|
|
261
300
|
}
|
|
262
301
|
: { mode: 'uncontrolled' as const },
|
|
302
|
+
fields:
|
|
303
|
+
fields !== undefined
|
|
304
|
+
? {
|
|
305
|
+
mode: 'controlled' as const,
|
|
306
|
+
initial: fields,
|
|
307
|
+
onChange: (next: FieldValues, change: FieldsChange) => {
|
|
308
|
+
// As for annotations: a commit made in the tick this root unmounts or
|
|
309
|
+
// reopens never reaches the host.
|
|
310
|
+
if (cancelled) return
|
|
311
|
+
fields = next
|
|
312
|
+
onFieldsChange?.(next, change)
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: { mode: 'uncontrolled' as const },
|
|
263
316
|
}))
|
|
264
317
|
)
|
|
265
318
|
if (cancelled) {
|
|
@@ -271,6 +324,17 @@
|
|
|
271
324
|
const current = untrack(() => annotations)
|
|
272
325
|
if (doc.annotations.mode === 'controlled' && current !== undefined)
|
|
273
326
|
doc.annotations.setControlledValue(current)
|
|
327
|
+
// The same for the fields; a document whose fields opened uncontrolled ignores the push.
|
|
328
|
+
const currentFields = untrack(() => fields)
|
|
329
|
+
if (currentFields !== undefined) doc.form.setControlledValue(currentFields)
|
|
330
|
+
// A form script that fails leaves the form fillable; the failure goes to `onError`.
|
|
331
|
+
// Subscribed before anyone hears of the document, so the form replays the errors from
|
|
332
|
+
// while it opened here; they are held until `onReady` has run.
|
|
333
|
+
let held: InkError[] | undefined = []
|
|
334
|
+
stopFormErrors = doc.form.on('error', (e) => {
|
|
335
|
+
if (held) held.push(e)
|
|
336
|
+
else onError?.(e)
|
|
337
|
+
})
|
|
274
338
|
documentNow = doc
|
|
275
339
|
document = doc
|
|
276
340
|
// Controlled edits run the plugins' reactions inside their own snapshot step.
|
|
@@ -281,8 +345,15 @@
|
|
|
281
345
|
override = null
|
|
282
346
|
setTestHook(ref, doc)
|
|
283
347
|
const s = doc.status.get()
|
|
284
|
-
|
|
285
|
-
|
|
348
|
+
try {
|
|
349
|
+
if (s.state === 'ready') onReady?.(doc)
|
|
350
|
+
else if (s.state === 'error') onError?.(s.error)
|
|
351
|
+
} finally {
|
|
352
|
+
// A host whose `onReady` throws still hears the held errors and every later one.
|
|
353
|
+
const early = held
|
|
354
|
+
held = undefined
|
|
355
|
+
if (!cancelled) for (const e of early) onError?.(e)
|
|
356
|
+
}
|
|
286
357
|
} catch (cause) {
|
|
287
358
|
if (cancelled) return
|
|
288
359
|
const error =
|
|
@@ -303,6 +374,7 @@
|
|
|
303
374
|
// Cleared first, so the plugins close their stores while the document is still alive.
|
|
304
375
|
hostDocument.set(null)
|
|
305
376
|
detach()
|
|
377
|
+
stopFormErrors()
|
|
306
378
|
opened?.destroy()
|
|
307
379
|
documentNow = null
|
|
308
380
|
document = null
|
|
@@ -318,6 +390,15 @@
|
|
|
318
390
|
untrack(() => doc.annotations.setControlledValue(list))
|
|
319
391
|
})
|
|
320
392
|
|
|
393
|
+
// The host's field values reach the form; the echo of a commit it applied changes nothing.
|
|
394
|
+
// Read through a derived, so a host that spreads its props pushes only a new value.
|
|
395
|
+
const pushedFields = $derived(fields)
|
|
396
|
+
$effect(() => {
|
|
397
|
+
const values = pushedFields
|
|
398
|
+
const doc = document
|
|
399
|
+
if (values && doc) untrack(() => doc.form.setControlledValue(values))
|
|
400
|
+
})
|
|
401
|
+
|
|
321
402
|
function setTool(id: ToolId): void {
|
|
322
403
|
// Picking the active tool again starts it afresh, as after a one-shot tool's use.
|
|
323
404
|
if (currentTool === id && tools?.active.get() === id) tools.set(id)
|
|
@@ -362,6 +443,21 @@
|
|
|
362
443
|
react: (fn) => pluginHistory.react(fn),
|
|
363
444
|
}
|
|
364
445
|
|
|
446
|
+
// The undo and redo keys anywhere in the root do what InkUndo and InkRedo do. When either
|
|
447
|
+
// button undoes or redoes the last step and disables itself, focus goes back to the document.
|
|
448
|
+
$effect(() => {
|
|
449
|
+
const el = ref
|
|
450
|
+
if (!el) return
|
|
451
|
+
const stopKeys = attachHistoryKeys(el, () => inkHistory)
|
|
452
|
+
const stopFocus = keepHistoryFocus(el, () =>
|
|
453
|
+
el.querySelector<HTMLElement>('[data-ink-scroll]')
|
|
454
|
+
)
|
|
455
|
+
return () => {
|
|
456
|
+
stopKeys()
|
|
457
|
+
stopFocus()
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
|
|
365
461
|
/**
|
|
366
462
|
* The history the plugins reach, picked by the open document's mode when they call. The host
|
|
367
463
|
* reads it as soon as a document opens, so it goes by `documentNow`, which is set by then.
|
|
@@ -393,7 +489,15 @@
|
|
|
393
489
|
const hostActivated = writable<string | null>(null)
|
|
394
490
|
const viewportEl = writable<HTMLElement | null>(null)
|
|
395
491
|
let pluginHost = $state.raw<PluginHost | null>(null)
|
|
492
|
+
/** One search controller for the root's life, built with the plugin host that shares it. */
|
|
493
|
+
let search = $state.raw<SearchController | null>(null)
|
|
396
494
|
$effect(() => {
|
|
495
|
+
const controller = untrack(() =>
|
|
496
|
+
createSearchController({
|
|
497
|
+
document: readonly(hostDocument),
|
|
498
|
+
viewport: () => viewport,
|
|
499
|
+
})
|
|
500
|
+
)
|
|
397
501
|
const host = untrack(() =>
|
|
398
502
|
createPluginHost({
|
|
399
503
|
// Read once, as documented on the prop.
|
|
@@ -408,13 +512,33 @@
|
|
|
408
512
|
root: readonly(viewportEl),
|
|
409
513
|
decorate: () => decorateAnnotation,
|
|
410
514
|
viewport: () => viewport,
|
|
515
|
+
search: viewerSearchOf(controller),
|
|
411
516
|
})
|
|
412
517
|
)
|
|
518
|
+
search = controller
|
|
413
519
|
pluginHost = host
|
|
414
520
|
// Destroyed after InkViewport's engine and its plugin layers, as in `createViewer`: Svelte
|
|
415
521
|
// creates a component's effects after its template and tears them down in creation order,
|
|
416
522
|
// so the children's teardowns run first.
|
|
417
|
-
return () =>
|
|
523
|
+
return () => {
|
|
524
|
+
host.destroy()
|
|
525
|
+
controller.destroy()
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
/*
|
|
530
|
+
* Ctrl/Cmd+F while focus is in the root and a find bar is mounted; else the browser's. A
|
|
531
|
+
* native listener: the handler reads the event's `currentTarget`, which is the root here.
|
|
532
|
+
*/
|
|
533
|
+
$effect(() => {
|
|
534
|
+
const el = ref
|
|
535
|
+
const controller = search
|
|
536
|
+
if (!el || !controller) return
|
|
537
|
+
const onKeydown = (e: KeyboardEvent): void => {
|
|
538
|
+
handleFindKey(e, controller, () => pageSelectionText(el))
|
|
539
|
+
}
|
|
540
|
+
el.addEventListener('keydown', onKeydown)
|
|
541
|
+
return () => el.removeEventListener('keydown', onKeydown)
|
|
418
542
|
})
|
|
419
543
|
|
|
420
544
|
/** The document's tool style; it batches through `inkHistory`, whichever mode is on. */
|
|
@@ -455,6 +579,9 @@
|
|
|
455
579
|
get pluginHost() {
|
|
456
580
|
return pluginHost
|
|
457
581
|
},
|
|
582
|
+
get search() {
|
|
583
|
+
return search
|
|
584
|
+
},
|
|
458
585
|
viewportEl,
|
|
459
586
|
get viewport() {
|
|
460
587
|
return viewport
|
|
@@ -505,6 +632,11 @@
|
|
|
505
632
|
viewport = vp
|
|
506
633
|
tools = tc
|
|
507
634
|
},
|
|
635
|
+
get formHighlight() {
|
|
636
|
+
return formHighlight
|
|
637
|
+
},
|
|
638
|
+
reportFormAction: (a) => onformaction?.(a),
|
|
639
|
+
reportError: (e) => onError?.(e),
|
|
508
640
|
}
|
|
509
641
|
setInkContext(context, () => activatedNow)
|
|
510
642
|
|