@inkbar/svelte 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 +27 -3
- package/dist/components/InkColorMenu.svelte +7 -18
- package/dist/components/InkColorMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkCommand.svelte +63 -0
- package/dist/components/InkCommand.svelte.d.ts +17 -0
- package/dist/components/InkCommand.svelte.d.ts.map +1 -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/InkHighlightMenu.svelte +6 -13
- package/dist/components/InkHighlightMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkLineStyleMenu.svelte +7 -17
- package/dist/components/InkLineStyleMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkRoot.svelte +138 -3
- package/dist/components/InkRoot.svelte.d.ts +7 -1
- 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/InkShapeMenu.svelte +6 -13
- package/dist/components/InkShapeMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkSignatureMenu.svelte +6 -13
- package/dist/components/InkSignatureMenu.svelte.d.ts.map +1 -1
- package/dist/components/InkToolButton.svelte +11 -3
- package/dist/components/InkToolButton.svelte.d.ts.map +1 -1
- package/dist/components/InkViewport.svelte +15 -0
- package/dist/components/InkViewport.svelte.d.ts.map +1 -1
- package/dist/components/menu-open-focus.d.ts +12 -0
- package/dist/components/menu-open-focus.d.ts.map +1 -0
- package/dist/components/menu-open-focus.js +35 -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 +21 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/hooks/useCommand.svelte.d.ts +23 -0
- package/dist/hooks/useCommand.svelte.d.ts.map +1 -0
- package/dist/hooks/useCommand.svelte.js +44 -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 +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -157,11 +157,35 @@ The highlight tool leaves `contents` free for the comment. `doc.text(page)` retu
|
|
|
157
157
|
|
|
158
158
|
`InkSignatureMenu` lists saved signatures, picks one for the `signature` tool, removes them, and opens `InkSignatureDialog` to draw, type or upload a new one. These are marks on the page, not cryptographic signatures. Pass `signatures` to `InkRoot` to keep them somewhere: `createLocalSignatureStore(key)` from `@inkbar/core` uses `localStorage`, and without it they last until reload. `signatureFonts` lists the handwriting fonts the Type tab offers; load them with `@font-face`. A placed signature keeps its aspect ratio when resized.
|
|
159
159
|
|
|
160
|
+
## Plugins
|
|
161
|
+
|
|
162
|
+
A plugin from `definePlugin` (in `@inkbar/core/dom`) adds commands, tools, decorations, page layers and its own record stores. Pass plugins to `InkRoot` once: the prop is read when the root is created. Nothing a plugin registers shows until you place it:
|
|
163
|
+
|
|
164
|
+
```svelte
|
|
165
|
+
<InkRoot {src} {assets} plugins={[suggestions]}>
|
|
166
|
+
<InkToolbar>
|
|
167
|
+
<InkCommand id="sandbox.suggest" />
|
|
168
|
+
<InkToolButton tool="sandbox.stamp" />
|
|
169
|
+
</InkToolbar>
|
|
170
|
+
<InkViewport />
|
|
171
|
+
</InkRoot>
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`InkCommand` is a button named by the command's label, disabled while the command is, and its status in a live region beside it. While the command runs, the button keeps focus and has `aria-busy` and `aria-disabled`, and pressing it does nothing. It shows the command's icon, or its label as text when the command has no icon. To build your own, `useCommand(id)` returns `run`, `enabled`, `busy`, `status`, `label` and `icon`. Plugins are set up once `InkRoot` has mounted in the browser, never on the server. Until then the command is disabled and has no label, and `InkCommand` keeps its button out of the accessibility tree (`aria-hidden`). Read `label` and `icon` off the handle, as you read `enabled`, so they fill in then. An id no plugin registered stays that way: `InkCommand` and `useCommand` log an `InkError('UnknownCommand')` to the console once, naming the id and the registered ones, and nothing throws. `InkToolButton` takes a plugin tool's own label and icon.
|
|
175
|
+
|
|
176
|
+
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
|
+
|
|
178
|
+
## Search
|
|
179
|
+
|
|
180
|
+
`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.
|
|
181
|
+
|
|
182
|
+
The bar handles Escape itself and stops it there, so an `onkeydown` you pass to `InkFindBar` never sees Escape.
|
|
183
|
+
|
|
160
184
|
## Custom rendering
|
|
161
185
|
|
|
162
|
-
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 }`. The props each receives are typed as `AnnotationSlotProps` and `ThreadPopoverSlotProps`, the same types the Vue and React packages export.
|
|
186
|
+
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.
|
|
163
187
|
|
|
164
|
-
`InkRoot`, `InkToolbar`, `InkToolButton`, `InkUndo`, `InkRedo`, `InkPrevPage`, `InkNextPage`, `InkZoomIn` and `InkZoomOut` take a `child` snippet to render your own element. Spread the `props` it gives you on that element. The snippet type is exported as `ChildSnippet`.
|
|
188
|
+
`InkRoot`, `InkToolbar`, `InkToolButton`, `InkCommand`, `InkUndo`, `InkRedo`, `InkPrevPage`, `InkNextPage`, `InkZoomIn` and `InkZoomOut` take a `child` snippet to render your own element. Spread the `props` it gives you on that element. The snippet type is exported as `ChildSnippet`.
|
|
165
189
|
|
|
166
190
|
```svelte
|
|
167
191
|
<InkToolbar>
|
|
@@ -195,7 +219,7 @@ Components carry state on data attributes, for example `[data-annotation][data-s
|
|
|
195
219
|
|
|
196
220
|
## Hooks
|
|
197
221
|
|
|
198
|
-
`useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory` and `
|
|
222
|
+
`useDocument`, `useAnnotations`, `useTool`, `useSelection`, `useZoom`, `usePage`, `useHistory`, `useEmbedded` and `useCommand` read the state of the nearest `InkRoot`. Call them during component initialisation, as you would `getContext`. Each returns an object whose reactive members are getters: read `useZoom().zoom` in your template or an effect and it tracks; destructure it (`const { zoom } = useZoom()`) and you copy the value once. Keep the object and read through it. Tool options, such as the shape kind, last as long as `InkRoot`. The tool style that `useToolStyle` reads and sets (colours, line style, text style) belongs to the document: opening another one starts again from the theme's defaults.
|
|
199
223
|
|
|
200
224
|
## Licence
|
|
201
225
|
|
|
@@ -34,13 +34,7 @@
|
|
|
34
34
|
</script>
|
|
35
35
|
|
|
36
36
|
<script lang="ts">
|
|
37
|
-
import {
|
|
38
|
-
COLOR_COLUMNS,
|
|
39
|
-
colorMenuModel,
|
|
40
|
-
focusInitialMenuItem,
|
|
41
|
-
handleMenuKeys,
|
|
42
|
-
menuItems,
|
|
43
|
-
} from '@inkbar/core/dom'
|
|
37
|
+
import { COLOR_COLUMNS, colorMenuModel, handleMenuKeys } from '@inkbar/core/dom'
|
|
44
38
|
import { Check } from '@inkbar/icons'
|
|
45
39
|
import { Popover, Toolbar } from 'bits-ui'
|
|
46
40
|
|
|
@@ -48,6 +42,7 @@
|
|
|
48
42
|
|
|
49
43
|
import { asBitsProps, composeHandlers } from './child-snippet.js'
|
|
50
44
|
import InkIcon from './InkIcon.svelte'
|
|
45
|
+
import { createMenuOpenFocus } from './menu-open-focus.js'
|
|
51
46
|
|
|
52
47
|
let {
|
|
53
48
|
property,
|
|
@@ -82,15 +77,14 @@
|
|
|
82
77
|
})
|
|
83
78
|
|
|
84
79
|
/** ArrowDown opens on the first item; a click opens on the checked one. */
|
|
85
|
-
|
|
86
|
-
// Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
|
|
80
|
+
const openFocus = createMenuOpenFocus()
|
|
87
81
|
$effect(() => {
|
|
88
|
-
if (!open)
|
|
82
|
+
if (!open) openFocus.reset()
|
|
89
83
|
})
|
|
90
84
|
function onTriggerKeydown(e: KeyboardEvent): void {
|
|
91
85
|
if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
|
|
92
86
|
e.preventDefault()
|
|
93
|
-
|
|
87
|
+
openFocus.first()
|
|
94
88
|
open = true
|
|
95
89
|
}
|
|
96
90
|
|
|
@@ -164,13 +158,8 @@
|
|
|
164
158
|
align="start"
|
|
165
159
|
sideOffset={4}
|
|
166
160
|
collisionPadding={8}
|
|
167
|
-
onOpenAutoFocus={(e) =>
|
|
168
|
-
|
|
169
|
-
if (menu) {
|
|
170
|
-
if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
|
|
171
|
-
else focusInitialMenuItem(menu)
|
|
172
|
-
}
|
|
173
|
-
}}
|
|
161
|
+
onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
|
|
162
|
+
onfocusin={openFocus.onfocusin}
|
|
174
163
|
onkeydown={(e) => {
|
|
175
164
|
// Nested in another panel, Escape closes this menu alone; Bits would otherwise pick
|
|
176
165
|
// the layer to close by registration order.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InkColorMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkColorMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;;GAMG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAC/E,iCAAiC;IACjC,QAAQ,EAAE,aAAa,CAAA;IACvB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;
|
|
1
|
+
{"version":3,"file":"InkColorMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkColorMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;;GAMG;AACH,MAAM,WAAW,iBAAkB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IAC/E,iCAAiC;IACjC,QAAQ,EAAE,aAAa,CAAA;IACvB,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC9B;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAChC;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,GAAG,SAAS,CAAA;IAC9B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B;AAkJH,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { ChildSnippet } from './child-snippet.js'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import type { HTMLButtonAttributes } from 'svelte/elements'
|
|
5
|
+
|
|
6
|
+
export interface InkCommandProps extends Omit<HTMLButtonAttributes, 'id' | 'children'> {
|
|
7
|
+
/** The command's namespaced id, such as `sandbox.suggest`. Read once. */
|
|
8
|
+
id: string
|
|
9
|
+
children?: Snippet<[{ label: string; busy: boolean }]> | undefined
|
|
10
|
+
/** Render your own button: spread `props` on it. */
|
|
11
|
+
child?: ChildSnippet | undefined
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<script lang="ts">
|
|
16
|
+
/*
|
|
17
|
+
* A plugin command as a toolbar button with its status beside it: the markup of core's
|
|
18
|
+
* `createCommandButton`, so the theme styles both alike. The button is named by the command's
|
|
19
|
+
* label and disabled while the command is, except while it runs: then it keeps focus, with
|
|
20
|
+
* `aria-busy` and `aria-disabled`, and a press does nothing. The status is a live region. A command without an icon shows its label as the button's text. Until the plugins
|
|
21
|
+
* are set up (once InkRoot has mounted; never on the server) the button is disabled and
|
|
22
|
+
* `aria-hidden`. An id no plugin registered stays that way and logs
|
|
23
|
+
* `InkError('UnknownCommand')` to the console once.
|
|
24
|
+
*/
|
|
25
|
+
import { Toolbar } from 'bits-ui'
|
|
26
|
+
import { untrack } from 'svelte'
|
|
27
|
+
|
|
28
|
+
import { useCommand } from '../hooks/useCommand.svelte.js'
|
|
29
|
+
|
|
30
|
+
import { asBitsProps } from './child-snippet.js'
|
|
31
|
+
import InkIcon from './InkIcon.svelte'
|
|
32
|
+
|
|
33
|
+
let { id, children, child, disabled, onclick, ...rest }: InkCommandProps = $props()
|
|
34
|
+
|
|
35
|
+
// The id is read once, as `useCommand` does: a different command is a different component.
|
|
36
|
+
const commandId = untrack(() => id)
|
|
37
|
+
const command = useCommand(commandId)
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="ink-command">
|
|
41
|
+
<Toolbar.Button
|
|
42
|
+
type="button"
|
|
43
|
+
data-command={commandId}
|
|
44
|
+
aria-label={command.label || undefined}
|
|
45
|
+
title={command.label || undefined}
|
|
46
|
+
aria-hidden={command.label ? undefined : 'true'}
|
|
47
|
+
aria-busy={command.busy}
|
|
48
|
+
aria-disabled={command.busy ? 'true' : undefined}
|
|
49
|
+
disabled={(!command.enabled && !command.busy) || !!disabled}
|
|
50
|
+
{...child ? { child } : {}}
|
|
51
|
+
{...asBitsProps(rest)}
|
|
52
|
+
onclick={(e) => {
|
|
53
|
+
onclick?.(e)
|
|
54
|
+
if (!e.defaultPrevented) void command.run()
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{#if children}{@render children({
|
|
58
|
+
label: command.label,
|
|
59
|
+
busy: command.busy,
|
|
60
|
+
})}{:else if command.icon}<InkIcon icon={command.icon} />{:else}{command.label}{/if}
|
|
61
|
+
</Toolbar.Button>
|
|
62
|
+
<span class="ink-command-status" role="status">{command.status}</span>
|
|
63
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChildSnippet } from './child-snippet.js';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLButtonAttributes } from 'svelte/elements';
|
|
4
|
+
export interface InkCommandProps extends Omit<HTMLButtonAttributes, 'id' | 'children'> {
|
|
5
|
+
/** The command's namespaced id, such as `sandbox.suggest`. Read once. */
|
|
6
|
+
id: string;
|
|
7
|
+
children?: Snippet<[{
|
|
8
|
+
label: string;
|
|
9
|
+
busy: boolean;
|
|
10
|
+
}]> | undefined;
|
|
11
|
+
/** Render your own button: spread `props` on it. */
|
|
12
|
+
child?: ChildSnippet | undefined;
|
|
13
|
+
}
|
|
14
|
+
declare const InkCommand: import("svelte").Component<InkCommandProps, {}, "">;
|
|
15
|
+
type InkCommand = ReturnType<typeof InkCommand>;
|
|
16
|
+
export default InkCommand;
|
|
17
|
+
//# sourceMappingURL=InkCommand.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"InkCommand.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkCommand.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,IAAI,GAAG,UAAU,CAAC;IACpF,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC,GAAG,SAAS,CAAA;IAClE,oDAAoD;IACpD,KAAK,CAAC,EAAE,YAAY,GAAG,SAAS,CAAA;CACjC;AAsDH,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
|
|
@@ -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"}
|
|
@@ -19,12 +19,10 @@
|
|
|
19
19
|
|
|
20
20
|
<script lang="ts">
|
|
21
21
|
import {
|
|
22
|
-
focusInitialMenuItem,
|
|
23
22
|
handleMenuKeys,
|
|
24
23
|
highlightMenuModel,
|
|
25
24
|
MARKUP_ICONS,
|
|
26
25
|
MARKUP_KINDS,
|
|
27
|
-
menuItems,
|
|
28
26
|
type HighlightOptions,
|
|
29
27
|
type MarkupKind,
|
|
30
28
|
} from '@inkbar/core/dom'
|
|
@@ -37,6 +35,7 @@
|
|
|
37
35
|
|
|
38
36
|
import InkColorMenu from './InkColorMenu.svelte'
|
|
39
37
|
import InkIcon from './InkIcon.svelte'
|
|
38
|
+
import { createMenuOpenFocus } from './menu-open-focus.js'
|
|
40
39
|
|
|
41
40
|
let { class: className, children, ...rest }: InkHighlightMenuProps = $props()
|
|
42
41
|
|
|
@@ -55,15 +54,14 @@
|
|
|
55
54
|
let menu = $state<HTMLElement | null>(null)
|
|
56
55
|
|
|
57
56
|
/** ArrowDown opens on the first item; a click opens on the checked one. */
|
|
58
|
-
|
|
59
|
-
// Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
|
|
57
|
+
const openFocus = createMenuOpenFocus()
|
|
60
58
|
$effect(() => {
|
|
61
|
-
if (!open)
|
|
59
|
+
if (!open) openFocus.reset()
|
|
62
60
|
})
|
|
63
61
|
function onTriggerKeydown(e: KeyboardEvent): void {
|
|
64
62
|
if (e.key !== 'ArrowDown' || e.defaultPrevented || open) return
|
|
65
63
|
e.preventDefault()
|
|
66
|
-
|
|
64
|
+
openFocus.first()
|
|
67
65
|
open = true
|
|
68
66
|
}
|
|
69
67
|
|
|
@@ -128,13 +126,8 @@
|
|
|
128
126
|
align="start"
|
|
129
127
|
sideOffset={4}
|
|
130
128
|
collisionPadding={8}
|
|
131
|
-
onOpenAutoFocus={(e) =>
|
|
132
|
-
|
|
133
|
-
if (menu) {
|
|
134
|
-
if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
|
|
135
|
-
else focusInitialMenuItem(menu)
|
|
136
|
-
}
|
|
137
|
-
}}
|
|
129
|
+
onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
|
|
130
|
+
onfocusin={openFocus.onfocusin}
|
|
138
131
|
onkeydowncapture={(e: KeyboardEvent) => menu && handleMenuKeys(e, menu)}
|
|
139
132
|
>
|
|
140
133
|
<div role="group" aria-label="Kinds" class="ink-highlight-kinds">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;
|
|
1
|
+
{"version":3,"file":"InkHighlightMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkHighlightMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAErD;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CACjD,cAAc,CAAC,cAAc,CAAC,EAC9B,UAAU,CACX;IACC,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AA2GH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -14,12 +14,7 @@
|
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<script lang="ts">
|
|
17
|
-
import {
|
|
18
|
-
focusInitialMenuItem,
|
|
19
|
-
handleMenuKeys,
|
|
20
|
-
lineStyleMenuModel,
|
|
21
|
-
menuItems,
|
|
22
|
-
} from '@inkbar/core/dom'
|
|
17
|
+
import { handleMenuKeys, lineStyleMenuModel } from '@inkbar/core/dom'
|
|
23
18
|
import { Check, Minus } from '@inkbar/icons'
|
|
24
19
|
import { Popover, Toolbar } from 'bits-ui'
|
|
25
20
|
|
|
@@ -27,6 +22,7 @@
|
|
|
27
22
|
|
|
28
23
|
import { asBitsProps, composeHandlers } from './child-snippet.js'
|
|
29
24
|
import InkIcon from './InkIcon.svelte'
|
|
25
|
+
import { createMenuOpenFocus } from './menu-open-focus.js'
|
|
30
26
|
|
|
31
27
|
let {
|
|
32
28
|
class: className,
|
|
@@ -50,15 +46,14 @@
|
|
|
50
46
|
})
|
|
51
47
|
|
|
52
48
|
/** ArrowDown opens on the first item; a click opens on the checked one. */
|
|
53
|
-
|
|
54
|
-
// Bits runs the open-focus callback more than once, so the flag outlives it; reset on close.
|
|
49
|
+
const openFocus = createMenuOpenFocus()
|
|
55
50
|
$effect(() => {
|
|
56
|
-
if (!open)
|
|
51
|
+
if (!open) openFocus.reset()
|
|
57
52
|
})
|
|
58
53
|
function onTriggerKeydown(e: KeyboardEvent): void {
|
|
59
54
|
if (e.key !== 'ArrowDown' || e.defaultPrevented || open || !model.enabled) return
|
|
60
55
|
e.preventDefault()
|
|
61
|
-
|
|
56
|
+
openFocus.first()
|
|
62
57
|
open = true
|
|
63
58
|
}
|
|
64
59
|
</script>
|
|
@@ -96,13 +91,8 @@
|
|
|
96
91
|
align="start"
|
|
97
92
|
sideOffset={4}
|
|
98
93
|
collisionPadding={8}
|
|
99
|
-
onOpenAutoFocus={(e) =>
|
|
100
|
-
|
|
101
|
-
if (menu) {
|
|
102
|
-
if (focusFirst) menuItems(menu)[0]?.focus({ preventScroll: true })
|
|
103
|
-
else focusInitialMenuItem(menu)
|
|
104
|
-
}
|
|
105
|
-
}}
|
|
94
|
+
onOpenAutoFocus={(e) => openFocus.onOpenAutoFocus(e, menu)}
|
|
95
|
+
onfocusin={openFocus.onfocusin}
|
|
106
96
|
onkeydown={(e) => menu && handleMenuKeys(e, menu)}
|
|
107
97
|
>
|
|
108
98
|
{#each model.sections as section (section.label)}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InkLineStyleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkLineStyleMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;
|
|
1
|
+
{"version":3,"file":"InkLineStyleMenu.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/InkLineStyleMenu.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA;AAE3D;;;;;GAKG;AACH,MAAM,WAAW,qBAAsB,SAAQ,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CAC/B;AAyFH,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|