Advanced Setup
Make sure you read the core concepts page before setup advance and check if the simple usage is good enough for you.
See full working example here https://github.com/10play/10TapAdvancedExample
Setting Up Our Custom Editor Directory
Step 1 - creating the directory
Create a new folder in your project called editor-web (or any other name) - advance setup will let you full control on what running inside the editor webview, so we need to create a separate folder that will include code that will run inside the webview and will have different build process and tsconfig
Step 2 - adding tsconfig
Create editor-web/tsconfig.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"_version": "2.0.0",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"paths": {
"@10play/tentap-editor": [
"../node_modules/@10play/tentap-editor/lib-web/typescript/webEditorUtils/index.d.ts"
]
},
"moduleResolution": "bundler",
"target": "es2015",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}
It's important to configure paths so on web the web types will be used instead of native types.
Step 3 - updating root tsconfig
Add exclude on the root tsconfig.json: "exclude": ["./editor-web"]
Step 4 - Add index.html AdvancedEditor.tsx and index.tsx
Add the following files
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RichTextEditor</title>
</head>
<style>
body {
margin: 0;
padding: 0;
}
#root > div:nth-of-type(1) {
position: absolute;
height: 100%;
overflow: auto;
width: 100%;
top: 0;
bottom: 0;
}
#root > div.dynamic-height {
height: unset;
}
#root div .ProseMirror {
min-height: 100%;
overflow: visible;
}
#root div.dynamic-height .ProseMirror {
height: auto;
min-height: 0;
display: flow-root;
}
.ProseMirror:focus {
outline: none;
}
.highlight-background {
background-color: #e6e6ff;
}
</style>
<body>
<div id="root"></div>
<script type="module" src="/index.tsx"></script>
</body>
</html>
import React from 'react';
import { EditorContent, useTenTap, TenTapStartKit } from '@10play/tentap-editor';
import { CounterBridge } from '../CounterBridge';
/**
* Here we control the web side of our custom editor
*/
export const AdvancedEditor = () => {
const editor = useTenTap({
bridges: [...TenTapStartKit, CounterBridge],
});
return (
<EditorContent
editor={editor}
className={window.dynamicHeight ? 'dynamic-height' : undefined}
/>
);
};
import React from 'react';
import { createRoot } from 'react-dom/client';
import { AdvancedEditor } from './AdvancedEditor';
/**
* This is the entrypoint for the "web" part of our editor that will be built with vite
*/
declare global {
interface Window {
contentInjected: boolean | undefined;
}
}
/**
* On android - react-native-webview there is a bug where sometimes the content
* is injected after the window is loaded https://github.com/react-native-webview/react-native-webview/pull/2960
* To overcome this we will check if the content is injected before rendering the editor
*/
const contentInjected = () => window.contentInjected;
let interval: NodeJS.Timeout;
interval = setInterval(() => {
if (!contentInjected()) return;
// Once content is injected into the webview, we can render the editor
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<AdvancedEditor />);
clearInterval(interval);
return;
}, 1);
Step 5 - adding a bundler
We chose to use vite as our bundler so first run
yarn add -D vite @vitejs/plugin-react vite-plugin-singlefile
And now let's create vite.config.ts and configure it
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
// This config is used to build the web editor into a single file
export default defineConfig({
root: 'src/editor-web', // This should be the directory of your index.html
build: {
outDir: 'build',
},
resolve: {
alias: [
{
find: '@10play/tentap-editor', // On our web bundle we only want to include web related code
replacement: '@10play/tentap-editor/web',
},
// We alias tiptap view and state to use the internal version of tiptap to avoid this error https://github.com/ueberdostiptap/issues/3869#issuecomment-2167931620
{
find: '@tiptap/pm/view',
replacement: '@10play/tentap-editor/web',
},
{
find: '@tiptap/pm/state',
replacement: '@10play/tentap-editor/web',
},
],
},
plugins: [react(), viteSingleFile()],
server: {
port: 3000,
},
});
It's important to have the alias configuration so vite will not load react-native code
Sharing tiptap with @10play/tentap-editor/web
@10play/tentap-editor/web is a prebuilt bundle with its own copy of tiptap and ProseMirror inlined. Code you import from @tiptap/* in your editor-web, such as extra extensions, is bundled separately, and the two copies don't recognize each other's objects. Bundler options such as vite's resolve.dedupe cannot merge them, because the copy inside @10play/tentap-editor/web is not a package in node_modules.
- Import
EditorContent(anduseEditorif you need it) from@10play/tentap-editor, not from@tiptap/react, so the editor and the component that renders it come from the same copy. - Keep the
@tiptap/pm/viewand@tiptap/pm/statealiases above, so extensions share ProseMirror's view and state classes with the bundled editor. - Install extra
@tiptap/*extensions at the same version that@10play/tentap-editoruses (npm ls @tiptap/coreshows it).
Step 6 - adding build scripts
Add scripts on package.json so it will be easy to run/build editor-web:
"scripts": {
...
"editor:dev": "vite --config ./editor-web/vite.config.ts",
"editor:build": "vite --config ./editor-web/vite.config.ts build && yarn editor:post-build",
"editor:post-build":"node ./node_modules/@10play/tentap-editor/scripts/buildEditor.js ./editor-web/build/index.html",
"reverse-android": "adb reverse tcp:3000 tcp:3000",
}
editor:dev - Run the editor web in dev mode
editor:build - Build the editor web, probably need to add to your deploy/build process
editor:post-build - This will take your built html file and write editorHtml.js (with editorHtml.d.ts), which exports it as the string editorHtml
which we can then later use as the source of our RichText.
reverse-android - On android need to reverse ports so it will be able to work with DEV / DEV_SERVER_URL props
Step 7 - using our custom editor
Outside of editor-web update your EditorBridge to used the html we just built.
import { editorHtml } from './editor-web/build/editorHtml';
const editor = useEditorBridge({
customSource: editorHtml,
// ...your other options
});
Step 8 - Install react-dom
yarn add react-dom
yarn add -D @types/react-dom
Install the react-dom version that matches your react.
You basically done with the advance setup now you have full control of the editor-web you can write your own bridgeExtensions add additional tiptapExtensions
useTenTap
useTenTap creates the TipTap editor inside the WebView and connects it to React Native.
import { TenTapStartKit, useTenTap } from '@10play/tentap-editor';
import { CounterBridge } from '../CounterBridge';
const editor = useTenTap({
bridges: [...TenTapStartKit, CounterBridge],
tiptapOptions: {
editorProps: { attributes: { spellcheck: 'false' } },
},
});
bridges: the bridges the editor-web knows about. A bridge with a TipTap extension is enabled when the native side'sbridgeExtensionscontains a bridge with the same name. A bridge without one, such as a bridge that only adds to the editor state or handles your own messages, always runs. Bridges and extensions are resolved once, when the editor is created.tiptapOptions: options passed to TipTap'suseEditor. Itsextensionsare added after the bridges' extensions, so pass only extensions that none of your bridges adds.CoreBridge(part ofTenTapStartKit) already addsDocument,ParagraphandText; passing them again makes TipTap warn about duplicate extension names. The event handlersonUpdate,onSelectionUpdateandonTransactionrun after tentap's own handlers instead of replacing them, andonCreateis yours alone (tentap no longer uses it to announce the editor), so none of them can break the connection to React Native. Every other option, such ascontent,editable,parseOptions,editorPropsorshouldRerenderOnTransaction, overrides tentap's default.stateUpdateDebounceMs: how long editor state updates are debounced before they are sent to React Native. default:10. The first state update after the editor is created is always sent right away.
useTenTap returns the TipTap Editor (or null before it exists). Its options type is exported as UseTenTapArgs.
Upgrading your editor-web
Rebuild your editor bundle (yarn editor:build) whenever you upgrade @10play/tentap-editor. Bundles built with an older version keep working, but miss the newer web-side features: insertContent, getSelectedText, parseOptions, and the dynamic height and readiness fixes. getSelectedText rejects with TentapUnsupportedError on such a bundle.
When upgrading from 1.0, also:
- change
#root div.dynamic-height .ProseMirrorin yourindex.htmltoheight: auto; min-height: 0; display: flow-root;as shown above, so dynamic height can shrink, doesn't loop on iOS and includes the first and last paragraph margins (1.1 also injects this rule, so a bundle that wasn't rebuilt yet gets it too); - preferably import
EditorContentfrom@10play/tentap-editorinstead of@tiptap/react, and follow Sharing tiptap.
The contentInjected polling in index.tsx still works. In 1.1 the configuration is written into the customSource HTML before your script runs, so the first check already passes.
Alternative Setup
In case you are using Expo you have to use this Alternative Setup for dev mode on your advance setup editor
There is an alternative vite setup that does not require a vite dev server as suggested in #80. This setup does not require we add the DEV prop
Update vite.config.ts
In vite.config.ts, add a plugin that runs editor:post-build after every build, and set emptyOutDir: false:
import { exec } from 'child_process';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { viteSingleFile } from 'vite-plugin-singlefile';
export default defineConfig({
root: 'src/editor-web',
build: {
outDir: 'build',
emptyOutDir: false,
},
resolve: {
// the same aliases as above
},
plugins: [
react(),
viteSingleFile(),
{
name: 'postbuild-commands',
closeBundle: async () => {
exec('yarn editor:post-build', (error) => {
if (error) console.error(`exec error: ${error}`);
});
},
},
],
});
Update package.json
Update the editor:dev script to watch the advanced editor instead of running a dev-server, the hot reload will be handled by metro and not vite.
"editor:dev": "vite --config ./editor-web/vite.config.ts -w build"