Add get{Current/Latest}EditorContent() (#682)

This commit is contained in:
Gerard Rovira
2021-10-08 12:28:59 +01:00
committed by acywatson
parent 980f51cf59
commit 17eab656c8
6 changed files with 69 additions and 15 deletions

View File

@ -43,7 +43,7 @@ export default function useTypeahead(editor: OutlineEditor): void {
// Monitor entered text // Monitor entered text
useEffect(() => { useEffect(() => {
return editor.addListener('update', (viewModel) => { return editor.addListener('update', (viewModel) => {
const text = editor.getTextContent(); const text = editor.getCurrentTextContent();
setText(text); setText(text);
}); });
}, [editor]); }, [editor]);

View File

@ -41,7 +41,7 @@ export function useCharacterLimit(
const Segmenter = Intl.Segmenter; const Segmenter = Intl.Segmenter;
let offsetUtf16 = 0; let offsetUtf16 = 0;
let offset = 0; let offset = 0;
const text = editor.getTextContent(); const text = editor.getCurrentTextContent();
if (typeof Segmenter === 'function') { if (typeof Segmenter === 'function') {
const segmenter = new Segmenter(); const segmenter = new Segmenter();
const graphemes = segmenter.segment(text); const graphemes = segmenter.segment(text);
@ -79,7 +79,7 @@ export function useCharacterLimit(
useEffect(() => { useEffect(() => {
editor.registerNodeType('overflow', OverflowNode); editor.registerNodeType('overflow', OverflowNode);
(() => { (() => {
const textLength = strlen(editor.getTextContent()); const textLength = strlen(editor.getCurrentTextContent());
const diff = maxCharacters - textLength; const diff = maxCharacters - textLength;
remainingCharacters(diff); remainingCharacters(diff);
execute(); execute();
@ -90,7 +90,7 @@ export function useCharacterLimit(
'update', 'update',
(viewModel: ViewModel, dirtyNodes: Set<NodeKey> | null) => { (viewModel: ViewModel, dirtyNodes: Set<NodeKey> | null) => {
const isComposing = editor.isComposing(); const isComposing = editor.isComposing();
const text = editor.getTextContent(); const text = editor.getCurrentTextContent();
const utf16TextLength = text.length; const utf16TextLength = text.length;
const hasDirtyNodes = dirtyNodes !== null && dirtyNodes.size > 0; const hasDirtyNodes = dirtyNodes !== null && dirtyNodes.size > 0;
if ( if (
@ -99,7 +99,7 @@ export function useCharacterLimit(
) { ) {
return; return;
} }
const textLength = strlen(editor.getTextContent()); const textLength = strlen(editor.getCurrentTextContent());
const textLengthAboveThreshold = const textLengthAboveThreshold =
textLength > maxCharacters || textLength > maxCharacters ||
(lastTextLength !== null && lastTextLength > maxCharacters); (lastTextLength !== null && lastTextLength > maxCharacters);

View File

@ -507,6 +507,32 @@ describe('OutlineEditor tests', () => {
expect(parsedSelection.focus.key).toEqual(parsedText.__key); expect(parsedSelection.focus.key).toEqual(parsedText.__key);
}); });
}); });
it('getCurrentTextContent() / getLatestTextContent()', async () => {
editor.update((view: View) => {
const root = view.getRoot();
const paragraph = createParagraphNode();
const text1 = createTextNode('1');
root.append(paragraph);
paragraph.append(text1);
});
editor.update((view: View) => {
const root = view.getRoot();
const paragraph = root.getFirstChild();
const text2 = createTextNode('2');
paragraph.append(text2);
});
expect(editor.getCurrentTextContent()).toBe('');
expect(
editor.getLatestTextContent((text) => {
expect(text).toBe('12');
}),
);
await Promise.resolve();
expect(editor.getCurrentTextContent()).toBe('12');
});
}); });
describe('Node children', () => { describe('Node children', () => {
@ -575,7 +601,7 @@ describe('OutlineEditor tests', () => {
textToKey.set(previousText, textNode.__key); textToKey.set(previousText, textNode.__key);
} }
}); });
expect(editor.getTextContent()).toBe(previous.join('')); expect(editor.getCurrentTextContent()).toBe(previous.join(''));
// Next editor state // Next editor state
const previousSet = new Set(previous); const previousSet = new Set(previous);
@ -609,7 +635,7 @@ describe('OutlineEditor tests', () => {
}); });
}); });
// Expect text content + HTML to be correct // Expect text content + HTML to be correct
expect(editor.getTextContent()).toBe(next.join('')); expect(editor.getCurrentTextContent()).toBe(next.join(''));
expect(container.innerHTML).toBe( expect(container.innerHTML).toBe(
`<div contenteditable="true" data-outline-editor="true"><p>${ `<div contenteditable="true" data-outline-editor="true"><p>${
next.length > 0 next.length > 0

View File

@ -118,13 +118,13 @@ describe('OutlineTextNode tests', () => {
view.getRoot().getFirstChild().append(textNode); view.getRoot().getFirstChild().append(textNode);
}); });
expect(editor.getTextContent()).toBe('Text'); expect(editor.getCurrentTextContent()).toBe('Text');
// Make sure that the editor content is still set after further reconciliations // Make sure that the editor content is still set after further reconciliations
await update((view) => { await update((view) => {
view.markNodeAsDirty(view.getNodeByKey(nodeKey)); view.markNodeAsDirty(view.getNodeByKey(nodeKey));
}); });
expect(editor.getTextContent()).toBe('Text'); expect(editor.getCurrentTextContent()).toBe('Text');
}); });
test('inert nodes', async () => { test('inert nodes', async () => {
@ -140,13 +140,13 @@ describe('OutlineTextNode tests', () => {
view.getRoot().getFirstChild().append(textNode); view.getRoot().getFirstChild().append(textNode);
}); });
expect(editor.getTextContent()).toBe(''); expect(editor.getCurrentTextContent()).toBe('');
// Make sure that the editor content is still empty after further reconciliations // Make sure that the editor content is still empty after further reconciliations
await update((view) => { await update((view) => {
view.markNodeAsDirty(view.getNodeByKey(nodeKey)); view.markNodeAsDirty(view.getNodeByKey(nodeKey));
}); });
expect(editor.getTextContent()).toBe(''); expect(editor.getCurrentTextContent()).toBe('');
}); });
test('prepend node', async () => { test('prepend node', async () => {
@ -161,7 +161,7 @@ describe('OutlineTextNode tests', () => {
previousTextNode.insertBefore(textNode); previousTextNode.insertBefore(textNode);
}); });
expect(editor.getTextContent()).toBe('Hello World'); expect(editor.getCurrentTextContent()).toBe('Hello World');
}); });
}); });

View File

@ -107,6 +107,22 @@ export type ListenerType =
| 'root' | 'root'
| 'decorator'; | 'decorator';
let isPreparingPendingViewUpdate = false;
export function asyncErrorOnPreparingPendingViewUpdate(
fnName: 'Editor.getLatestTextContent()',
): void {
if (
isPreparingPendingViewUpdate &&
fnName === 'Editor.getLatestTextContent()'
) {
invariant(
false,
'Editor.getLatestTextContent() can be asynchronous and cannot be used within Editor.update()',
);
}
}
export function resetEditor( export function resetEditor(
editor: OutlineEditor, editor: OutlineEditor,
prevRootElement: null | HTMLElement, prevRootElement: null | HTMLElement,
@ -167,6 +183,7 @@ function updateEditor(
viewModelWasCloned = true; viewModelWasCloned = true;
} }
isPreparingPendingViewUpdate = true;
const error = preparePendingViewUpdate( const error = preparePendingViewUpdate(
pendingViewModel, pendingViewModel,
updateFn, updateFn,
@ -174,6 +191,7 @@ function updateEditor(
markAllTextNodesAsDirty, markAllTextNodesAsDirty,
editor, editor,
); );
isPreparingPendingViewUpdate = false;
if (error !== null) { if (error !== null) {
// Report errors // Report errors
@ -326,9 +344,17 @@ class BaseOutlineEditor {
getRootElement(): null | HTMLElement { getRootElement(): null | HTMLElement {
return this._rootElement; return this._rootElement;
} }
getTextContent(): string { getCurrentTextContent(): string {
return this._textContent; return this._textContent;
} }
getLatestTextContent(callback: (text: string) => void): void {
asyncErrorOnPreparingPendingViewUpdate('Editor.getLatestTextContent()');
if (this._pendingViewModel === null) {
callback(this._textContent);
return;
}
this._deferred.push(() => callback(this._textContent));
}
setRootElement(nextRootElement: null | HTMLElement): void { setRootElement(nextRootElement: null | HTMLElement): void {
const prevRootElement = this._rootElement; const prevRootElement = this._rootElement;
if (nextRootElement !== prevRootElement) { if (nextRootElement !== prevRootElement) {
@ -478,7 +504,8 @@ declare export class OutlineEditor {
getDecorators(): {[NodeKey]: ReactNode}; getDecorators(): {[NodeKey]: ReactNode};
getRootElement(): null | HTMLElement; getRootElement(): null | HTMLElement;
setRootElement(rootElement: null | HTMLElement): void; setRootElement(rootElement: null | HTMLElement): void;
getTextContent(): string; getCurrentTextContent(): string;
getLatestTextContent((text: string) => void): () => void;
getElementByKey(key: NodeKey): null | HTMLElement; getElementByKey(key: NodeKey): null | HTMLElement;
getViewModel(): ViewModel; getViewModel(): ViewModel;
setViewModel(viewModel: ViewModel): void; setViewModel(viewModel: ViewModel): void;

View File

@ -46,5 +46,6 @@
"44": "reconcileNode: prevNode or nextNode does not exist in nodeMap", "44": "reconcileNode: prevNode or nextNode does not exist in nodeMap",
"45": "reconcileNode: parentDOM is null", "45": "reconcileNode: parentDOM is null",
"46": "Reconciliation: could not find DOM element for node key \"${key}\"", "46": "Reconciliation: could not find DOM element for node key \"${key}\"",
"47": "clearEditor expected plain text root first child to be a ParagraphNode" "47": "clearEditor expected plain text root first child to be a ParagraphNode",
"48": "Editor.getLatestTextContent() can be asynchronous and cannot be used within Editor.update()"
} }