From 7ea6b44b10e1bf23287ba33e0081cacbfc595780 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 19 Aug 2019 14:58:02 -0400 Subject: seemed to fix syncing with remote google doc --- src/client/views/DocumentDecorations.tsx | 14 ++++++++ src/client/views/nodes/FormattedTextBox.tsx | 41 ++++++++++++++--------- src/new_fields/RichTextField.ts | 52 +++++++++++++---------------- 3 files changed, 63 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 6616d5d58..963722fe3 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,5 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faLink, faTag } from '@fortawesome/free-solid-svg-icons'; +import * as fa from '@fortawesome/free-brands-svg-icons'; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { action, computed, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; @@ -36,6 +37,7 @@ export const Flyout = higflyout.default; library.add(faLink); library.add(faTag); +library.add(fa.faGoogleDrive as any); @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { @@ -618,6 +620,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> ); } + considerGoogleDoc = () => { + let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; + let canEmbed = thisDoc.data && thisDoc.data instanceof RichTextField; + if (!canEmbed) return (null); + return ( +
+ +
+ ); + } + considerTooltip = () => { let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document; let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField; @@ -768,6 +781,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> {this.metadataMenu} {this.considerEmbed()} + {this.considerGoogleDoc()} {/* {this.considerTooltip()} */} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index ad29ce775..bc057bb5f 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -60,7 +60,7 @@ const richTextSchema = createSchema({ documentText: "string" }); -const googleDocKey = "googleDocId"; +const googleDocId = "googleDocId"; type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); @@ -84,6 +84,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe private _proxyReactionDisposer: Opt; private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + private isOpening = false; @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; @@ -295,17 +296,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe pollExportedCounterpart = async () => { let dataDoc = Doc.GetProto(this.props.Document); - let documentId = StrCast(dataDoc[googleDocKey]); + let documentId = StrCast(dataDoc[googleDocId]); if (documentId) { let exportState = await GoogleApiClientUtils.Docs.read({ documentId }); if (exportState) { let data = Cast(dataDoc.data, RichTextField); if (data) { + this.isOpening = true; dataDoc.data = new RichTextField(data[FromGoogleDocText](exportState)); } } else { - delete dataDoc[googleDocKey]; + delete dataDoc[googleDocId]; } + this.tryUpdateHeight(); } } @@ -346,9 +349,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined; return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`; }, - field2 => { - this._editorView && !this._applyingChange && - this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field2))); + incomingValue => { + if (this._editorView && !this._applyingChange) { + let updatedState = JSON.parse(incomingValue); + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + // manually sets cursor selection at the end of the text on focus + if (this.isOpening) { + this.isOpening = false; + let end = this._editorView.state.doc.content.size - 1; + updatedState.selection = { type: "text", anchor: end, head: end }; + this._editorView.updateState(EditorState.fromJSON(config, updatedState)); + } + } } ); @@ -686,7 +698,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt" }); ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" }); - if (!(googleDocKey in Doc.GetProto(this.props.Document))) { + if (!(googleDocId in Doc.GetProto(this.props.Document))) { ContextMenu.Instance.addItem({ description: "Export to Google Doc...", event: this.updateGoogleDoc, @@ -698,21 +710,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe updateGoogleDoc = () => { let modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; - let reference: Opt = Cast(this.dataDoc[googleDocKey], "string"); + let reference: Opt = Cast(this.dataDoc[googleDocId], "string"); if (!reference) { mode = modes.Insert; reference = { title: StrCast(this.dataDoc.title), - handler: id => this.dataDoc[googleDocKey] = id + handler: id => this.dataDoc[googleDocId] = id }; } - const data = Cast(this.dataDoc.data, RichTextField); - if (data) { - GoogleApiClientUtils.Docs.write({ - mode, - content: data[ToGoogleDocText](), - reference - }); + if (this._editorView) { + let data = Cast(this.dataDoc.data, RichTextField); + let content = data ? data[ToGoogleDocText]() : this._editorView.state.doc.textContent; + GoogleApiClientUtils.Docs.write({ reference, content, mode }); } } diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts index 8963682c3..ec08293e9 100644 --- a/src/new_fields/RichTextField.ts +++ b/src/new_fields/RichTextField.ts @@ -6,6 +6,8 @@ import { scriptingGlobal } from "../client/util/Scripting"; export const ToGoogleDocText = Symbol("PlainText"); export const FromGoogleDocText = Symbol("PlainText"); +const delimiter = "\n"; +const joiner = ""; @scriptingGlobal @Deserializable("RichTextField") @@ -27,45 +29,39 @@ export class RichTextField extends ObjectField { } [ToGoogleDocText]() { - let state = JSON.parse(this.Data); - let text = state.doc.textBetween(0, state.doc.content.size, "\n\n"); - console.log(text); + // Because we're working with plain text, just concatenate all paragraphs let content = JSON.parse(this.Data).doc.content; let paragraphs = content.filter((item: any) => item.type === "paragraph"); - let output = ""; - for (let i = 0; i < paragraphs.length; i++) { - let paragraph = paragraphs[i]; - let addNewLine = i > 0 ? paragraphs[i - 1].content : false; - if (paragraph.content) { - output += paragraph.content.map((block: any) => block.text).join(""); - } else { - output += i === 0 ? "" : "\n"; - } - addNewLine && (output += "\n"); - } - return output; + + // Functions to flatten ProseMirror paragraph objects (and their components) to plain text + // While this function already exists in state.doc.textBeteen(), it doesn't account for newlines + let blockText = (block: any) => block.text; + let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter; + + // Concatentate paragraphs and string the result together. Trim the last newline, an artifact. + let textParagraphs = paragraphs.map(concatenateParagraph); + return textParagraphs.join(joiner).trimEnd(delimiter); } [FromGoogleDocText](plainText: string) { - let elements = plainText.split("\n"); + // Remap the text, creating blocks split on newlines + let elements = plainText.split(delimiter); + + // Google Docs adds in an extra carriage return automatically, so this counteracts it !elements[elements.length - 1].length && elements.pop(); + + // Preserve the current state, but re-write the content to be the blocks let parsed = JSON.parse(this.Data); parsed.doc.content = elements.map(text => { let paragraph: any = { type: "paragraph" }; - if (text.length) { - paragraph.content = [{ - type: "text", - marks: [], - text - }]; - } + text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break return paragraph; }); - parsed.selection = { - type: "text", - anchor: 1, - head: 1 - }; + + // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it + parsed.selection = { type: "text", anchor: 1, head: 1 }; + + // Export the ProseMirror-compatible state object we've jsut built return JSON.stringify(parsed); } -- cgit v1.2.3-70-g09d2