diff --git a/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt b/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt index ab173102..fb2c2e6b 100644 --- a/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt +++ b/android/app/src/main/java/com/example/gutenbergkit/EditorActivity.kt @@ -1,5 +1,6 @@ package com.example.gutenbergkit +import android.app.AlertDialog import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo @@ -311,6 +312,22 @@ fun EditorScreen( } } }) + setAutocompleterTriggeredListener(object : GutenbergView.AutocompleterTriggeredListener { + override fun onAutocompleterTriggered(type: String) { + val suggestions = when (type) { + "at-symbol" -> arrayOf("alice", "bob", "charlie") + "plus-symbol" -> arrayOf("photoblog", "traveldiaries", "dailydev") + else -> return + } + AlertDialog.Builder(context) + .setTitle("Select a suggestion") + .setItems(suggestions) { _, which -> + appendTextAtCursor(suggestions[which] + " ") + } + .setNegativeButton("Cancel", null) + .show() + } + }) // Demo app has no persistence layer, so return null. // In a real app, return the persisted title and content from autosave. setLatestContentProvider(object : GutenbergView.LatestContentProvider { diff --git a/ios/Demo-iOS/Sources/Views/EditorView.swift b/ios/Demo-iOS/Sources/Views/EditorView.swift index 23dcf736..ca1aaed8 100644 --- a/ios/Demo-iOS/Sources/Views/EditorView.swift +++ b/ios/Demo-iOS/Sources/Views/EditorView.swift @@ -216,7 +216,24 @@ private struct _EditorView: UIViewControllerRepresentable { } func editor(_ viewController: EditorViewController, didTriggerAutocompleter type: String) { - // No-op for demo + let suggestions: [String] + switch type { + case "at-symbol": + suggestions = ["alice", "bob", "charlie"] + case "plus-symbol": + suggestions = ["photoblog", "traveldiaries", "dailydev"] + default: + return + } + + let alert = UIAlertController(title: "Select a suggestion", message: nil, preferredStyle: .actionSheet) + for suggestion in suggestions { + alert.addAction(UIAlertAction(title: suggestion, style: .default) { _ in + viewController.appendTextAtCursor(suggestion + " ") + }) + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + viewController.present(alert, animated: true) } func editor(_ viewController: EditorViewController, didOpenModalDialog dialogType: String) { diff --git a/src/components/editor/test/use-host-bridge.test.jsx b/src/components/editor/test/use-host-bridge.test.jsx index cd6ee49b..33d29a79 100644 --- a/src/components/editor/test/use-host-bridge.test.jsx +++ b/src/components/editor/test/use-host-bridge.test.jsx @@ -332,6 +332,49 @@ describe( 'useHostBridge', () => { } ); } ); + it( 'appendTextAtCursor preserves existing content when block attribute is a RichTextData object', () => { + const existingContent = 'Existing block content @'; + const richTextData = { + toString: () => existingContent, + valueOf: () => existingContent, + toHTMLString: () => existingContent, + }; + + mockGetSelectedBlockClientId.mockReturnValue( 'block-1' ); + mockGetBlock.mockReturnValue( { + name: 'core/paragraph', + clientId: 'block-1', + attributes: { content: richTextData }, + } ); + getBlockType.mockReturnValue( { + attributes: { content: { type: 'string' } }, + } ); + mockGetSelectionStart.mockReturnValue( { + clientId: 'block-1', + attributeKey: 'content', + offset: existingContent.length, + } ); + mockGetSelectionEnd.mockReturnValue( { + clientId: 'block-1', + attributeKey: 'content', + offset: existingContent.length, + } ); + + renderHook( () => + useHostBridge( defaultPost, editorRef, markBridgeReady ) + ); + + window.editor.appendTextAtCursor( 'username ' ); + + // The block should contain the original content plus the + // appended text — not just the appended text alone. + expect( mockUpdateBlock ).toHaveBeenCalledWith( 'block-1', { + attributes: expect.objectContaining( { + content: 'Existing block content @username ', + } ), + } ); + } ); + it( 'appendTextAtCursor returns false when no block is selected', () => { mockGetSelectedBlockClientId.mockReturnValue( null ); diff --git a/src/components/editor/use-host-bridge.js b/src/components/editor/use-host-bridge.js index 073aec4f..f91392f2 100644 --- a/src/components/editor/use-host-bridge.js +++ b/src/components/editor/use-host-bridge.js @@ -150,9 +150,7 @@ export function useHostBridge( post, editorRef, markBridgeReady ) { return false; } - const blockContent = normalizeAttribute( - block.attributes?.content - ); + const blockContent = block.attributes?.content || ''; const currentValue = create( { html: blockContent } ); const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd();