diff --git a/.gitignore b/.gitignore index 4cf80d9..927abd8 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ android/keystores/debug.keystore # generated by bob lib/ +lib-web/ diff --git a/example/src/Examples/Advanced/AdvancedRichText.tsx b/example/src/Examples/Advanced/AdvancedRichText.tsx index ea7ab99..a682fe4 100644 --- a/example/src/Examples/Advanced/AdvancedRichText.tsx +++ b/example/src/Examples/Advanced/AdvancedRichText.tsx @@ -43,6 +43,7 @@ const Counter = ({ editor }: { editor: EditorInstance }) => { export const Advanced = ({}: NativeStackScreenProps) => { const editor = useNativeEditor({ + autofocus: true, initialContent: `

This is a basic example of implementing images.

s sdfdsf fd dsfd ssdfd dsfdsfdsfdsfd

`, plugins: [ TenTapStartKit, @@ -65,7 +66,6 @@ export const Advanced = ({}: NativeStackScreenProps) => { avoidIosKeyboard editor={editor} DEV - autofocus customSource={AdvancedEditor} /> diff --git a/example/src/Examples/Advanced/Editor/AdvancedEditor.tsx b/example/src/Examples/Advanced/Editor/AdvancedEditor.tsx index 655d814..7d3b382 100644 --- a/example/src/Examples/Advanced/Editor/AdvancedEditor.tsx +++ b/example/src/Examples/Advanced/Editor/AdvancedEditor.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { EditorContent } from '@tiptap/react'; -import { useTenTap } from 'tentap'; +import { useTenTap } from 'tentap/web'; import Document from '@tiptap/extension-document'; import Paragraph from '@tiptap/extension-paragraph'; import Text from '@tiptap/extension-text'; diff --git a/example/src/Examples/Advanced/Editor/vite.config.ts b/example/src/Examples/Advanced/Editor/vite.config.ts index dd293dc..a0d371f 100644 --- a/example/src/Examples/Advanced/Editor/vite.config.ts +++ b/example/src/Examples/Advanced/Editor/vite.config.ts @@ -16,6 +16,10 @@ export default defineConfig({ find: 'tentap', replacement: resolve(__dirname, '../../../../../src/webEditorUtils'), }, + { + find: 'tentap/web', + replacement: resolve(__dirname, '../../../../../src/webEditorUtils'), + }, ], }, plugins: [react(), viteSingleFile()], diff --git a/example/src/Examples/Basic.tsx b/example/src/Examples/Basic.tsx index c42ce0c..b3797f6 100644 --- a/example/src/Examples/Basic.tsx +++ b/example/src/Examples/Basic.tsx @@ -27,7 +27,6 @@ import { CustomKeyboard } from '../../../src/RichText/Keyboard'; const exampleStyles = StyleSheet.create({ fullScreen: { flex: 1, - borderWidth: 1, }, keyboardAvoidingView: { position: 'absolute', @@ -38,6 +37,7 @@ const exampleStyles = StyleSheet.create({ export const Basic = ({}: NativeStackScreenProps) => { const editor = useNativeEditor({ + autofocus: true, initialContent: `

This is a basic example of implementing images.

s

`, plugins: [ CoreBridge, @@ -57,7 +57,7 @@ export const Basic = ({}: NativeStackScreenProps) => { return ( - + ) => { const editor = useNativeEditor({ + autofocus: true, plugins: [TenTapStartKit, CoreBridge], initialContent: '

Initial lovely message...

', }); @@ -102,7 +103,7 @@ export const EditorStickToKeyboardExample = ({}: NativeStackScreenProps< style={exampleStyles.keyboardAvoidingView} > - + { const { keyboardHeight: iosKeyboardHeight, isKeyboardUp } = useKeyboard(); const source: WebViewProps['source'] = DEV @@ -55,19 +53,8 @@ export const RichText = ({ // Parse the message sent from the editor const { type, payload } = JSON.parse(data) as EditorMessage; editor.plugins?.forEach((e) => { - e.onEditorMessage && e.onEditorMessage({ type, payload }); + e.onEditorMessage && e.onEditorMessage({ type, payload }, editor); }); - switch (type) { - case EditorMessageType.EditorReady: - if (autofocus) { - console.log('focus'); - editor.focus('end'); - } - break; - case EditorMessageType.StateUpdate: - editor._updateEditorState(payload); - break; - } }; useEffect(() => { @@ -115,7 +102,7 @@ export const RichText = ({ return ( <> - {autofocus && Platform.OS === 'android' && ( + {editor.autofocus && Platform.OS === 'android' && ( )} = (cb: (val: T) => void) => () => void; export const useNativeEditor = (options?: { plugins?: TenTapBridge[]; initialContent?: string; + autofocus?: boolean; }): EditorInstance => { const webviewRef = useRef(null); // Till we will implement default per plugin @@ -54,33 +54,12 @@ export const useNativeEditor = (options?: { }); }; - const updateScrollThresholdAndMargin = (bottom: number) => - sendAction({ - type: EditorUpdateSettings.UpdateScrollThresholdAndMargin, - payload: bottom, - }); - - const focus = (pos: 'start' | 'end' | 'all' | number | boolean | null) => { - webviewRef.current?.requestFocus(); - if (editorStateRef.current) { - _updateEditorState({ - ...(editorStateRef.current as EditorNativeState), - isFocused: true, - }); - } - sendAction({ - type: EditorUpdateSettings.Focus, - payload: pos, - }); - }; - const editorInstance = { plugins: options?.plugins, + initialContent: options?.initialContent, + autofocus: options?.autofocus, webviewRef, - updateScrollThresholdAndMargin, getEditorState, - initialContent: options?.initialContent, - focus, _updateEditorState, _subscribeToEditorStateUpdate, }; @@ -88,7 +67,18 @@ export const useNativeEditor = (options?: { const editorInstanceExtendByPlugins = (options?.plugins || []).reduce( (acc, cur) => { if (!cur.extendEditorInstance) return acc; - return Object.assign(acc, cur.extendEditorInstance(sendAction)); + return Object.assign( + acc, + cur.extendEditorInstance( + sendAction, + webviewRef, + editorStateRef, + _updateEditorState + ), + webviewRef, + editorStateRef.current, + _updateEditorState + ); }, editorInstance ) as EditorInstance; diff --git a/src/bridges/base.ts b/src/bridges/base.ts index 1daef01..d6775aa 100644 --- a/src/bridges/base.ts +++ b/src/bridges/base.ts @@ -1,4 +1,7 @@ import { Editor, type AnyExtension } from '@tiptap/core'; +import type { EditorInstance, EditorNativeState } from '../types'; +import type WebView from 'react-native-webview'; +import type { RefObject } from 'react'; interface TenTapBridge { name: string; @@ -9,9 +12,14 @@ interface TenTapBridge { message: M, sendMessageBack: (message: M) => void ) => boolean; - onEditorMessage?: (message: M) => boolean; + onEditorMessage?: (message: M, editorInstance: EditorInstance) => boolean; extendEditorState?: (editor: Editor) => T; - extendEditorInstance?: (sendBridgeMessage: (message: M) => void) => E; + extendEditorInstance?: ( + sendBridgeMessage: (message: M) => void, + webviewRef?: RefObject, + editorState?: RefObject, + _setEditorState?: (editorState: EditorNativeState) => void + ) => E; extendCSS?: string | undefined; config?: string; } diff --git a/src/bridges/core.ts b/src/bridges/core.ts index 35b1c60..dc5a777 100644 --- a/src/bridges/core.ts +++ b/src/bridges/core.ts @@ -1,14 +1,22 @@ import TenTapBridge from './base'; import { asyncMessages } from '../RichText/AsyncMessages'; +import type { EditorNativeState } from '../types'; +import { focusListener } from '../webEditorUtils/focusListener'; type CoreEditorState = { selection: { from: number; to: number }; + isFocused: boolean; + isReady: boolean; }; +type focusArgs = 'start' | 'end' | 'all' | number | boolean | null; + type CoreEditorInstance = { getContent: () => Promise; setContent: (content: string) => void; setSelection: (from: number, to: number) => void; + updateScrollThresholdAndMargin: (offset: number) => void; + focus: (pos: focusArgs) => void; }; declare module '../types/EditorNativeState' { @@ -21,6 +29,10 @@ export enum CoreEditorActionType { GetContent = 'get-content', SetContent = 'set-content', SendContentToNative = 'send-content-back', + StateUpdate = 'stateUpdate', + Focus = 'Focus', + EditorReady = 'editor-ready', + UpdateScrollThresholdAndMargin = 'update-scroll-threshold-and-margin', } type MessageToNative = { @@ -31,7 +43,7 @@ type MessageToNative = { }; }; -type CoreMessages = +export type CoreMessages = | MessageToNative | { type: CoreEditorActionType.GetContent; @@ -45,6 +57,22 @@ type CoreMessages = content: string; }; } + | { + type: CoreEditorActionType.StateUpdate; + payload: EditorNativeState; + } + | { + type: CoreEditorActionType.EditorReady; + payload: undefined; + } + | { + type: CoreEditorActionType.Focus; + payload: focusArgs; + } + | { + type: CoreEditorActionType.UpdateScrollThresholdAndMargin; + payload: number; + } | { type: CoreEditorActionType.SetSelection; payload: { @@ -80,18 +108,55 @@ export const CoreBridge = new TenTapBridge< }); return true; } + if (message.type === CoreEditorActionType.Focus) { + editor.commands.focus(message.payload); + return true; + } + if (message.type === CoreEditorActionType.UpdateScrollThresholdAndMargin) { + editor.setOptions({ + editorProps: { + scrollThreshold: { + top: 0, + bottom: message.payload, + right: 0, + left: 0, + }, + scrollMargin: { top: 0, bottom: message.payload, right: 0, left: 0 }, + }, + }); + return true; + } return false; }, - onEditorMessage: ({ type, payload }) => { + onEditorMessage: ({ type, payload }, editorInstance) => { if (type === CoreEditorActionType.SendContentToNative) { asyncMessages.onMessage(payload.messageId, payload.content); return true; } + + if (type === CoreEditorActionType.EditorReady) { + if (editorInstance.autofocus) { + editorInstance.focus('end'); + } + } + if (type === CoreEditorActionType.StateUpdate) { + editorInstance._updateEditorState(payload); + } return false; }, - extendEditorInstance: (sendBridgeMessage) => { + extendEditorInstance: ( + sendBridgeMessage, + webviewRef, + editorStateRef, + _updateEditorState + ) => { return { + updateScrollThresholdAndMargin: (bottom: number) => + sendBridgeMessage({ + type: CoreEditorActionType.UpdateScrollThresholdAndMargin, + payload: bottom, + }), setSelection: (from, to) => { sendBridgeMessage({ type: CoreEditorActionType.SetSelection, @@ -118,10 +183,26 @@ export const CoreBridge = new TenTapBridge< )) as string; return html; }, + focus: (pos: 'start' | 'end' | 'all' | number | boolean | null) => { + webviewRef?.current?.requestFocus(); + if (editorStateRef && editorStateRef.current) { + _updateEditorState && + _updateEditorState({ + ...(editorStateRef.current as EditorNativeState), + isFocused: true, + }); + } + sendBridgeMessage({ + type: CoreEditorActionType.Focus, + payload: pos, + }); + }, }; }, extendEditorState: (editor) => { return { + isFocused: focusListener.isFocused, + isReady: true, selection: { from: editor.state.selection.from, to: editor.state.selection.to, diff --git a/src/simpleWebEditor/utils/index.ts b/src/simpleWebEditor/utils/index.ts deleted file mode 100644 index 77ac35e..0000000 --- a/src/simpleWebEditor/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as FocusListener } from './focusListener'; diff --git a/src/types/Actions.ts b/src/types/Actions.ts index 5faf5f6..6ad8ee7 100644 --- a/src/types/Actions.ts +++ b/src/types/Actions.ts @@ -18,13 +18,5 @@ export interface RegularAction { } export enum EditorUpdateSettings { - UpdateScrollThresholdAndMargin = 'update-scroll-threshold-and-margin', Focus = 'focus', } - -type EditorUpdateSettingsActions = - EditorUpdateSettings.UpdateScrollThresholdAndMargin; -export interface EditorUpdateSettingsAction { - type: EditorUpdateSettingsActions; - payload?: any; -} diff --git a/src/types/EditorNativeState.ts b/src/types/EditorNativeState.ts index e4ec329..bae4282 100644 --- a/src/types/EditorNativeState.ts +++ b/src/types/EditorNativeState.ts @@ -2,18 +2,15 @@ import type { RefObject } from 'react'; import type WebView from 'react-native-webview'; import type TenTapBridge from '../bridges/base'; -export interface EditorNativeState { - isFocused: boolean; - isReady: boolean; -} +export interface EditorNativeState {} type Subscription = (cb: (val: T) => void) => () => void; export interface EditorInstance { + autofocus: boolean; focus: (pos?: 'start' | 'end' | 'all' | number | boolean | null) => void; initialContent?: string; webviewRef: RefObject; - updateScrollThresholdAndMargin: (offset: number) => void; getEditorState: () => EditorNativeState; _updateEditorState: (state: EditorNativeState) => void; _subscribeToEditorStateUpdate: Subscription; diff --git a/src/types/Messaging.ts b/src/types/Messaging.ts index 1f39981..8a40b23 100644 --- a/src/types/Messaging.ts +++ b/src/types/Messaging.ts @@ -1,19 +1,11 @@ -import type { EditorNativeState } from './EditorNativeState'; - +import type { CoreMessages } from '../bridges/core'; export enum EditorMessageType { Action = 'action', - StateUpdate = 'stateUpdate', - EditorReady = 'editor-ready', } export interface EditorActionMessage { - type: EditorMessageType.Action | EditorMessageType.EditorReady; + type: EditorMessageType.Action; payload: any; } -export interface EditorStateUpdateMessage { - type: EditorMessageType.StateUpdate; - payload: EditorNativeState; -} - -export type EditorMessage = EditorActionMessage | EditorStateUpdateMessage; +export type EditorMessage = EditorActionMessage | CoreMessages; diff --git a/src/simpleWebEditor/utils/focusListener.ts b/src/webEditorUtils/focusListener.tsx similarity index 66% rename from src/simpleWebEditor/utils/focusListener.ts rename to src/webEditorUtils/focusListener.tsx index 364c11c..c785bbb 100644 --- a/src/simpleWebEditor/utils/focusListener.ts +++ b/src/webEditorUtils/focusListener.tsx @@ -2,14 +2,16 @@ class FocusListener { private focus: boolean; constructor() { this.focus = false; - document.addEventListener( + // @ts-ignore + window.document.addEventListener( 'focus', () => { this.focus = true; }, true ); - document.addEventListener( + // @ts-ignore + window.document.addEventListener( 'blur', () => { this.focus = false; @@ -22,4 +24,4 @@ class FocusListener { } } -export default new FocusListener(); +export const focusListener = new FocusListener(); diff --git a/src/webEditorUtils/useTenTap.tsx b/src/webEditorUtils/useTenTap.tsx index e2f1ee0..19c3f33 100644 --- a/src/webEditorUtils/useTenTap.tsx +++ b/src/webEditorUtils/useTenTap.tsx @@ -3,10 +3,9 @@ import { useEffect } from 'react'; import { useEditor } from '@tiptap/react'; import { Editor } from '@tiptap/core'; import { type EditorMessage, EditorMessageType } from '../types/Messaging'; -import { EditorUpdateSettings } from '../types/Actions'; -import focusListener from '../simpleWebEditor/utils/focusListener'; import { type EditorNativeState } from '../types/EditorNativeState'; import type TenTapBridge from '../bridges/base'; +import { CoreEditorActionType } from '../bridges/core'; import { blueBackgroundPlugin } from '../bridges/HighlightSelection'; declare global { interface Window { @@ -56,11 +55,7 @@ export const useTenTap = (options?: useTenTapArgs) => { }; const sendStateUpdate = debounce((editor: Editor) => { - let payload = { - // core - isReady: true, - isFocused: focusListener.isFocused, - }; + let payload = {}; const state = bridges.reduce((acc, e) => { if (!e.extendEditorState) return acc; @@ -68,7 +63,7 @@ export const useTenTap = (options?: useTenTapArgs) => { }, payload) as EditorNativeState; sendMessage({ - type: EditorMessageType.StateUpdate, + type: CoreEditorActionType.StateUpdate, payload: state, }); }, 10); @@ -76,7 +71,10 @@ export const useTenTap = (options?: useTenTapArgs) => { const editor = useEditor({ content, onCreate: () => - sendMessage({ type: EditorMessageType.EditorReady, payload: null }), + sendMessage({ + type: CoreEditorActionType.EditorReady, + payload: undefined, + }), onUpdate: (onUpdate) => sendStateUpdate(onUpdate.editor), onSelectionUpdate: (onUpdate) => sendStateUpdate(onUpdate.editor), onTransaction: (onUpdate) => sendStateUpdate(onUpdate.editor), @@ -90,29 +88,14 @@ export const useTenTap = (options?: useTenTapArgs) => { bridges.forEach((e) => { e.onBridgeMessage && e.onBridgeMessage(editor, action, sendMessage); }); - if (action.type === EditorUpdateSettings.UpdateScrollThresholdAndMargin) { - editor.setOptions({ - editorProps: { - scrollThreshold: { - top: 0, - bottom: action.payload, - right: 0, - left: 0, - }, - scrollMargin: { top: 0, bottom: action.payload, right: 0, left: 0 }, - }, - }); - } }; const handleWebviewMessage = (event: MessageEvent | Event) => { if (!(event instanceof MessageEvent)) return; // TODO check android const { type, payload } = JSON.parse(event.data) as EditorMessage; console.log('Received message from webview', { type, payload }); + // todo: fix this - switch not needed switch (type) { case EditorMessageType.Action: - if (payload.type === EditorUpdateSettings.Focus) { - editor.commands.focus(payload.payload); - } // Handle actions handleEditorAction(payload); break; diff --git a/src/webEditorUtils/vite.config.ts b/src/webEditorUtils/vite.config.ts new file mode 100644 index 0000000..c04659b --- /dev/null +++ b/src/webEditorUtils/vite.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite'; +import { resolve } from 'path'; + +// This config is used to build the web editor into a single file + +export default defineConfig({ + build: { + lib: { + // Could also be a dictionary or array of multiple entry points + entry: resolve(__dirname, './index.ts'), + name: 'tentapWebutils', + // the proper extensions will be added + fileName: 'index', + }, + rollupOptions: { + // make sure to externalize deps that shouldn't be bundled + // into your library + external: ['react'], + output: { + dir: '../../lib-web', + // Provide global variables to use in the UMD build + // for externalized deps + globals: { + react: 'React', + }, + }, + }, + }, +}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 45c5957..ff67a9a 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig", - "exclude": ["./src/simpleWebEditor","./website", "./example/src/Examples/Advanced/Editor", "./src/webEditorUtils", "example"] + "exclude": ["./src/simpleWebEditor","./website", "./example/src/Examples/Advanced/Editor", "./src/webEditorUtils", "example", "./src/webEditorUtils/vite.config.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 62a5e4b..eb941a7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,5 +25,5 @@ "target": "esnext", "verbatimModuleSyntax": true, }, - "exclude": ["./src/simpleWebEditor","./website", "./example/src/Examples/Advanced/Editor", "./src/webEditorUtils"] + "exclude": ["./src/simpleWebEditor","./website", "./example/src/Examples/Advanced/Editor", "./src/webEditorUtils", "./src/webEditorUtils/vite.config.ts"] }