diff --git a/apps/mobile/app.json b/apps/mobile/app.json index dcc54dc1..972f12ac 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -3,7 +3,7 @@ "name": "EdgeEver", "slug": "edgeever", "scheme": "edgeever", - "version": "1.2.0", + "version": "1.3.0", "runtimeVersion": { "policy": "appVersion" }, @@ -28,7 +28,7 @@ }, "android": { "package": "org.edgeever.mobile", - "versionCode": 27, + "versionCode": 28, "adaptiveIcon": { "backgroundColor": "#d1fae5", "backgroundImage": "./assets/icon.png", diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 8158b5e8..b3123909 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -22,6 +22,7 @@ "@tanstack/react-query-persist-client": "^5.101.2", "@tiptap/extension-image": "3.27.1", "@tiptap/extension-placeholder": "3.27.1", + "@tiptap/extension-table": "3.27.1", "@tiptap/react": "3.27.1", "@tiptap/starter-kit": "3.27.1", "expo": "~57.0.7", diff --git a/apps/mobile/src/components/LocalTiptapEditor.tsx b/apps/mobile/src/components/LocalTiptapEditor.tsx index 8a031e34..0b7a9981 100644 --- a/apps/mobile/src/components/LocalTiptapEditor.tsx +++ b/apps/mobile/src/components/LocalTiptapEditor.tsx @@ -2,6 +2,7 @@ import Image from "@tiptap/extension-image"; import Placeholder from "@tiptap/extension-placeholder"; +import { TableKit } from "@tiptap/extension-table"; import { EditorContent, useEditor, useEditorState } from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; import type { TiptapDoc } from "@edgeever/shared"; @@ -20,6 +21,7 @@ import { getMobileEditorPlaceholder, getMobileEditorToolbarActionLabel, getMobileEditorToolbarLabel, + type MobileEditorTableActionId, type MobileEditorToolbarActionId, } from "@edgeever/shared/mobile-editor"; import { useDOMImperativeHandle, type DOMImperativeFactory, type DOMProps } from "expo/dom"; @@ -89,6 +91,9 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { extensions: [ StarterKit, protectedImageExtension, + TableKit.configure({ + table: { renderWrapper: true }, + }), Placeholder.configure({ placeholder: getMobileEditorPlaceholder(props.locale), }), @@ -250,7 +255,8 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { selector: ({ editor: activeEditor }) => (activeEditor?.isActive("bold") ? MOBILE_EDITOR_ACTIVE_FLAGS.bold : 0) | (activeEditor?.isActive("bulletList") ? MOBILE_EDITOR_ACTIVE_FLAGS.bulletList : 0) | - (activeEditor?.isActive("blockquote") ? MOBILE_EDITOR_ACTIVE_FLAGS.blockquote : 0), + (activeEditor?.isActive("blockquote") ? MOBILE_EDITOR_ACTIVE_FLAGS.blockquote : 0) | + (activeEditor?.isActive("table") ? MOBILE_EDITOR_ACTIVE_FLAGS.table : 0), }); const insertImage = async () => { @@ -272,12 +278,49 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { } }; + const runTableAction = (action: MobileEditorTableActionId) => { + const chain = editor?.chain().focus(); + if (!chain) { + return; + } + + switch (action) { + case "insertTable": + chain.insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(); + return; + case "addTableRow": + chain.addRowAfter().run(); + return; + case "deleteTableRow": + chain.deleteRow().run(); + return; + case "addTableColumn": + chain.addColumnAfter().run(); + return; + case "deleteTableColumn": + chain.deleteColumn().run(); + return; + case "toggleTableHeader": + chain.toggleHeaderRow().run(); + return; + case "deleteTable": + chain.deleteTable().run(); + } + }; + const toolbarIcons: Record = { image: , bold: , bulletList: , blockquote: , horizontalRule: , + insertTable: , + addTableRow: , + deleteTableRow: , + addTableColumn: , + deleteTableColumn: , + toggleTableHeader: , + deleteTable: , }; const toolbarHandlers: Record void> = { image: () => void insertImage(), @@ -285,32 +328,45 @@ export default function LocalTiptapEditor(props: LocalTiptapEditorProps) { bulletList: () => editor?.chain().focus().toggleBulletList().run(), blockquote: () => editor?.chain().focus().toggleBlockquote().run(), horizontalRule: () => editor?.chain().focus().setHorizontalRule().run(), + insertTable: () => runTableAction("insertTable"), + addTableRow: () => runTableAction("addTableRow"), + deleteTableRow: () => runTableAction("deleteTableRow"), + addTableColumn: () => runTableAction("addTableColumn"), + deleteTableColumn: () => runTableAction("deleteTableColumn"), + toggleTableHeader: () => runTableAction("toggleTableHeader"), + deleteTable: () => runTableAction("deleteTable"), }; return (
- {MOBILE_EDITOR_TOOLBAR_ACTIONS.map((action) => ( + {MOBILE_EDITOR_TOOLBAR_ACTIONS + .filter((action) => toolbarState & MOBILE_EDITOR_ACTIVE_FLAGS.table + ? action.id !== "insertTable" + : !action.requiresTable) + .map((action) => ( 0 && Boolean(toolbarState & action.activeFlag)} + disabled={(action.requiresTable && !(toolbarState & MOBILE_EDITOR_ACTIVE_FLAGS.table)) || (action.id === "insertTable" && Boolean(toolbarState & MOBILE_EDITOR_ACTIVE_FLAGS.table))} icon={toolbarIcons[action.id]} label={getMobileEditorToolbarActionLabel(action.id, props.locale)} onRun={toolbarHandlers[action.id]} /> - ))} + ))}
); } -const ToolbarButton = ({ active = false, icon, label, onRun }: { active?: boolean; icon: ReactNode; label: string; onRun: () => void }) => ( +const ToolbarButton = ({ active = false, disabled = false, icon, label, onRun }: { active?: boolean; disabled?: boolean; icon: ReactNode; label: string; onRun: () => void }) => (