Skip to main content
Native code calls functions that your shim exposes on the page, and the shim posts events back.

Build a single offline page

Bundle your shim, Inkkit and its stylesheet into one HTML file with no external requests. With Vite, the vite-plugin-singlefile plugin inlines every script and stylesheet into index.html:
Inkkit bundles all of its runtime dependencies except Mermaid. A single-file page inlines Mermaid whenever any module imports @aicayzer/inkkit, even if no diagram ever renders. Import from @aicayzer/inkkit/core throughout the page unless you need diagram previews. Compatibility has the page sizes for each entry.
Declare UTF-8 in the page, for example with <meta charset="utf-8">. The bundle contains non-ASCII text, and WebKit reads an undeclared single-file page as Latin-1, which stops the editor from loading.
If the page sets a Content Security Policy, it must allow the inline script and style that a single-file build produces, and blob: images. Inkkit decodes images and diagrams through blob: URLs for clipboard and print output. Without img-src blob:, printableSnapshot() rejects for documents with images or diagrams. Also allow any custom scheme your adapters use for display URLs.

Follow the bridge rules

These rules apply to every call that crosses between native code and the page:
  • Pass the generation your native model holds as the expected generation on every call that takes one. A call meant for a document that has since been replaced then fails with stale-document instead of changing the new one. See Documents and saving.
  • Send JSON null for a missing value. Inkkit treats null as omitted for optional parameters and option fields.
  • Await promises in the shim. mount, printableSnapshot, clipboardSnapshot, paste and destroy return promises. Every other member returns its result at once.
  • Send code and message to native code, not the error object. JSON serialization drops an error’s message, and native code branches on code. See Errors.
  • Encode bytes. A Uint8Array has no useful JSON form. Encode image bytes, for example as base64, on both sides. This applies to CapturedImage, PortableImage, ClipboardInput.images, ClipboardOutput images and PrintableDocument.assets.
  • Convert coordinates. Points and rectangles, such as ClientPoint and TextRect, are CSS pixels in the web view’s client area, not device pixels. Convert them in native code.
  • Wait for the page. mount is asynchronous. Have the shim post a ready message after mount resolves, and send no calls before it arrives.

Write the shim and the native side

The shim mounts the editor, exposes a few functions on window, and posts events through window.webkit.messageHandlers. Each function returns a reply object, so a failure reaches native code as a code and message instead of a rejected promise. The native side calls the functions with callAsyncJavaScript and receives the messages in a WKScriptMessageHandler.
The changed message carries only the document version. Take the text with snapshot at the save boundary. If snapshotText() throws, don’t save and keep the document open: a failed snapshot is not an unchanged document. Documents and saving has the full save loop, and reloadDocument for files that change on disk.

Call native code from adapters

An adapter is a set of JavaScript functions, so it lives in the shim. To keep images or files in native storage, have each asynchronous adapter function call native code and return its answer as a promise. Register the handler with addScriptMessageHandler(_:contentWorld:name:) and a WKScriptMessageHandlerWithReply, so that postMessage returns a promise that native code resolves or rejects. ImageAdapter.presentation and WikiLinkAdapter.resolve are synchronous. Answer them in the shim without waiting for native code. For images, return a display URL that the shim can build from the reference, such as one in a custom scheme that native code serves through a WKURLSchemeHandler:
Send the DocumentContext with each native request, and check it in native code before you act on the answer. Images, files and wiki links covers what each adapter call must do.

Pass native text input

Most hosts let the web view receive keys and text as usual. When your native layer receives typed text or a key before the web view does, pass it on:
  • insertText(text, generation) inserts exactly that text, as typing would. It returns false, and inserts nothing, during composition.
  • keyDown(input, generation) runs a key press as if it reached the editor. It returns true when Inkkit handled the key; then don’t also handle it natively.
Both require the generation, so input meant for a replaced document fails instead of landing in the new one. See Native input for the keys Inkkit handles.

Manage focus and disposal

Loading a document doesn’t focus the editor. Call focus(generation) when editing should start at once, and blur(generation) to take focus off the editor’s text surfaces. Don’t find Inkkit’s elements to blur them. Before you close the document or tear down the web view, take a fresh snapshot and save it. Then await destroy(), and only after it resolves remove the root or release the web view. The content controller keeps a strong reference to its message handlers, so remove them with removeScriptMessageHandler(forName:) when you are done, as tearDown() does above.