Skip to content

Commit

Permalink
fix: Improve autosave and unsaved changes detection
Browse files Browse the repository at this point in the history
Previously, host apps determined whether content changes were present by
comparing the content received from GutenbergKit to the latest
server-provided post content. This is problematic as the server makes
slight modifications to content markup, creating perpetually changed
content.

To address this, we need GutenbergKit to communicate changed content.
We accomplish by sending a new `changed` parameter alongside the title
and content. Rather than updating the post and content refs on every
content change, we now do so at the time at which the host app requests
the latest content. This ensures the host and GutenbergKit synchronize
their latest perception of changes.

We also now rely upon `AutosaveMonitor` to throttle how often we request
the host app persist the latest changes, rather than a generic store
subscription.
  • Loading branch information
dcalhoun committed Feb 20, 2025
1 parent 45db259 commit a014374
Show file tree
Hide file tree
Showing 7 changed files with 45 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ class GutenbergView : WebView {
private var siteApiRoot: String = ""
private var siteApiNamespace: String = ""
private var authHeader: String = ""
private var lastUpdatedTitle: CharSequence? = null
private var lastUpdatedContent: CharSequence? = null

private val handler = Handler(Looper.getMainLooper())
private var editorDidBecomeAvailable: ((GutenbergView) -> Unit)? = null
Expand Down Expand Up @@ -276,7 +274,7 @@ class GutenbergView : WebView {
}

interface TitleAndContentCallback {
fun onResult(title: CharSequence, content: CharSequence)
fun onResult(title: CharSequence, content: CharSequence, changed: Boolean)
}

interface ContentChangeListener {
Expand Down Expand Up @@ -321,15 +319,25 @@ class GutenbergView : WebView {
}
handler.post {
this.evaluateJavascript("editor.getTitleAndContent($completeComposition);") { result ->
var lastUpdatedTitle: CharSequence? = null
var lastUpdatedContent: CharSequence? = null
var changed = false
try {
val jsonObject = JSONObject(result)
lastUpdatedTitle = jsonObject.getString("title")
lastUpdatedContent = jsonObject.getString("content")
changed = jsonObject.getBoolean("changed")
} catch (e: JSONException) {
Log.e("GutenbergView", "Received invalid JSON from editor.getTitleAndContent")
}

callback.onResult(lastUpdatedTitle ?: "", lastUpdatedContent ?: originalContent)
val title = lastUpdatedTitle ?: ""
val content = if (changed) {
lastUpdatedContent ?: ""
} else {
originalContent
}
callback.onResult(title, content, changed)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions ios/Sources/GutenbergKit/Sources/EditorTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ struct EditorBlockType: Decodable, Identifiable {
public struct EditorTitleAndContent: Decodable {
public let title: String
public let content: String
public let changed: Bool
}

5 changes: 3 additions & 2 deletions ios/Sources/GutenbergKit/Sources/EditorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,11 @@ public final class EditorViewController: UIViewController, GutenbergEditorContro
let result = try await webView.evaluateJavaScript("editor.getTitleAndContent();")
guard let dictionary = result as? [String: Any],
let title = dictionary["title"] as? String,
let content = dictionary["content"] as? String else {
let content = dictionary["content"] as? String,
let changed = dictionary["changed"] as? Bool else {
throw NSError(domain: "Invalid data format", code: 0, userInfo: nil)
}
return EditorTitleAndContent(title: title, content: content)
return EditorTitleAndContent(title: title, content: content, changed: changed)
}

/// Steps backwards in the editor history state
Expand Down
12 changes: 0 additions & 12 deletions src/components/editor/use-editor-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as editorStore } from '@wordpress/editor';
import { getBlockTypes, unregisterBlockType } from '@wordpress/blocks';
import { registerCoreBlocks } from '@wordpress/block-library';
import { unregisterFormatType } from '@wordpress/rich-text';

/**
* Internal dependencies
Expand All @@ -24,20 +21,11 @@ export function useEditorSetup( post ) {
receiveEntityRecords( 'postType', post.type, post );

setupEditor( post, {} );
registerCoreBlocks();

editorLoaded();
// Temp, check why this isn't being called in the provider.
setEditedPost( post.type, post.id );

return () => {
getBlockTypes().forEach( ( block ) => {
unregisterBlockType( block.name );
} );
// `unregisterBlockType` does not un-register the format type
// See: https://github.com/WordPress/gutenberg/pull/63554
unregisterFormatType( 'core/footnote' );
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [] );
}
47 changes: 20 additions & 27 deletions src/components/editor/use-host-bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
* WordPress dependencies
*/
import { useEffect, useCallback, useRef } from '@wordpress/element';
import { useDispatch, useSelect, subscribe } from '@wordpress/data';
import { useDispatch, useSelect } from '@wordpress/data';
import { store as coreStore } from '@wordpress/core-data';
import { store as editorStore } from '@wordpress/editor';

/**
* Internal dependencies
*/
import { onEditorContentChanged } from '../../utils/bridge';
import { parse, serialize } from '@wordpress/blocks';

window.editor = window.editor || {};

Expand All @@ -26,6 +22,12 @@ export function useHostBridge( post, editorRef ) {
[ editEntityRecord, post.id, post.type ]
);

const postTitleRef = useRef( post.title.raw );
const postContentRef = useRef( null );
if ( postContentRef.current === null ) {
postContentRef.current = serialize( parse( post.content.raw || '' ) );
}

useEffect( () => {
window.editor.setContent = ( content ) => {
editContent( { content: decodeURIComponent( content ) } );
Expand All @@ -48,10 +50,18 @@ export function useHostBridge( post, editorRef ) {
endComposition( editorRef.current );
}

return {
title: getEditedPostAttribute( 'title' ),
content: getEditedPostContent(),
};
const title = getEditedPostAttribute( 'title' );
const content = getEditedPostContent();
const changed =
title !== postTitleRef.current ||
content !== postContentRef.current;

if ( changed ) {
postTitleRef.current = title;
postContentRef.current = content;
}

return { title, content, changed };
};

window.editor.undo = () => {
Expand Down Expand Up @@ -87,23 +97,6 @@ export function useHostBridge( post, editorRef ) {
switchEditorMode,
undo,
] );

const postTitleRef = useRef( post.title );
const postContentRef = useRef( post.content );

useEffect( () => {
return subscribe( () => {
const { title, content } = window.editor.getTitleAndContent();
if (
title !== postTitleRef.current ||
content !== postContentRef.current
) {
onEditorContentChanged();
postTitleRef.current = title;
postContentRef.current = content;
}
} );
}, [] );
}

/**
Expand Down
8 changes: 7 additions & 1 deletion src/components/layout/index.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
/**
* WordPress dependencies
*/
import { EditorSnackbars, ErrorBoundary } from '@wordpress/editor';
import {
EditorSnackbars,
ErrorBoundary,
AutosaveMonitor,
} from '@wordpress/editor';

/**
* Internal dependencies
*/
import Editor from '../editor';
import { onEditorContentChanged } from '../../utils/bridge';
import EditorLoadNotice from '../editor-load-notice';
import './style.scss';

Expand All @@ -20,6 +25,7 @@ import './style.scss';
export default function Layout( props ) {
return (
<ErrorBoundary canCopyContent>
<AutosaveMonitor autosave={ onEditorContentChanged } />
<Editor { ...props }>
<EditorSnackbars />
</Editor>
Expand Down
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { dispatch } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { store as preferencesStore } from '@wordpress/preferences';
import defaultEditorStyles from '@wordpress/block-editor/build-style/default-editor-styles.css?inline';
import { registerCoreBlocks } from '@wordpress/block-library';

/**
* Internal dependencies
Expand Down Expand Up @@ -42,6 +43,7 @@ function initializeEditor() {
themeStyles,
} );

registerCoreBlocks();
const post = getPost();

createRoot( document.getElementById( 'root' ) ).render(
Expand Down

0 comments on commit a014374

Please sign in to comment.