aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts26
-rw-r--r--src/client/documents/Documents.ts7
-rw-r--r--src/client/views/MainView.tsx26
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx51
-rw-r--r--src/new_fields/RichTextField.ts49
-rw-r--r--src/new_fields/RichTextUtils.ts129
6 files changed, 173 insertions, 115 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 61df69d5c..689009254 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -27,11 +27,14 @@ export namespace GoogleApiClientUtils {
export type Identifier = string;
export type Reference = Identifier | CreateOptions;
- export type TextContent = string | string[];
+ export interface Content {
+ text: string | string[];
+ links: docs_v1.Schema$Request[];
+ }
export type IdHandler = (id: Identifier) => any;
export type CreationResult = Opt<Identifier>;
export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
- export type ReadResult = { title?: string, body?: string };
+ export type ReadResult = { title: string, body: string };
export interface CreateOptions {
service: Service;
@@ -50,7 +53,7 @@ export namespace GoogleApiClientUtils {
export interface WriteOptions {
mode: WriteMode;
- content: TextContent;
+ content: Content;
reference: Reference;
index?: number; // if excluded, will compute the last index of the document and append the content there
}
@@ -165,28 +168,24 @@ export namespace GoogleApiClientUtils {
}
};
- export const read = async (options: ReadOptions): Promise<ReadResult> => {
+ export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadResult = {};
if (document) {
- let title = document.title;
+ let title = document.title!;
let body = Utils.extractText(document, options.removeNewlines);
- result = { title, body };
+ return { title, body };
}
- return result;
});
};
- export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
+ export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadLinesResult = {};
if (document) {
let title = document.title;
let bodyLines = Utils.extractText(document).split("\n");
options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
- result = { title, bodyLines };
+ return { title, bodyLines };
}
- return result;
});
};
@@ -227,7 +226,7 @@ export namespace GoogleApiClientUtils {
});
index = 1;
}
- const text = options.content;
+ const text = options.content.text;
text.length && requests.push({
insertText: {
text: isArray(text) ? text.join("\n") : text,
@@ -237,6 +236,7 @@ export namespace GoogleApiClientUtils {
if (!requests.length) {
return undefined;
}
+ requests.push(...options.content.links);
let replies: any = await update({ documentId: identifier, requests });
if ("errors" in replies) {
console.log("Write operation failed:");
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 47df17329..e40e095d6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -496,10 +496,13 @@ export namespace Docs {
* @param title an optional title to give to the highest parent document in the hierarchy
*/
export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
- if (input === null || ![...primitives, "object"].includes(typeof input)) {
+ if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) {
return undefined;
}
- let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+ let parsed = input;
+ if (typeof input === "string") {
+ parsed = JSONUtils.tryParse(input);
+ }
let converted: Doc;
if (typeof parsed === "object" && !(parsed instanceof Array)) {
converted = convertObject(parsed, title);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b35b2d331..ab87f0c7b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -122,32 +122,6 @@ export class MainView extends React.Component {
componentWillMount() {
var tag = document.createElement('script');
- let requests: docs_v1.Schema$Request[] =
- [{
- updateTextStyle: {
- fields: "*",
- range: {
- startIndex: 1,
- endIndex: 15
- },
- textStyle: {
- bold: true,
- link: { url: window.location.href },
- foregroundColor: {
- color: {
- rgbColor: {
- red: 1.0,
- green: 0.0,
- blue: 0.0
- }
- }
- }
- }
- }
- }];
- let documentId = "1xBwN4akVePW_Zp8wbiq0WNjlzGAE2PyNVvwzFbUyv3I";
- GoogleApiClientUtils.Docs.setStyle({ documentId, requests });
-
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d6ba1700a..02bee2f82 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -12,7 +12,7 @@ import { DateField } from '../../../new_fields/DateField';
import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
+import { RichTextField } from "../../../new_fields/RichTextField";
import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
import { Utils } from '../../../Utils';
@@ -37,12 +37,11 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { DictationManager } from '../../util/DictationManager';
import { ReplaceStep } from 'prosemirror-transform';
import { DocumentType } from '../../documents/DocumentTypes';
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
-export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
-
export interface FormattedTextBoxProps {
isOverlay?: boolean;
hideOnLeave?: boolean;
@@ -61,7 +60,7 @@ export const GoogleRef = "googleDocId";
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
-type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => void;
@observer
export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
@@ -363,7 +362,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._reactionDisposer = reaction(
() => {
const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : Blank;
+ return field ? field.Data : RichTextUtils.Initialize();
},
incomingValue => {
if (this._editorView && !this._applyingChange) {
@@ -431,7 +430,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
let modes = GoogleApiClientUtils.WriteMode;
let mode = modes.Replace;
let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
@@ -440,9 +439,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
}
let redo = async () => {
- let data = Cast(this.dataDoc.data, RichTextField);
- if (this._editorView && reference && data) {
- let content = data[ToPlainText]();
+ if (this._editorView && reference) {
+ let content = RichTextUtils.GoogleDocs.Convert(this._editorView.state);
let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
response && (this.dataDoc[GoogleRef] = response.documentId);
let pushSuccess = response !== undefined && !("errors" in response);
@@ -451,7 +449,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
};
let undo = () => {
- let content = exportState.body;
+ if (!exportState) {
+ return;
+ }
+ let content = {
+ text: exportState.body,
+ links: []
+ };
if (reference && content) {
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
@@ -464,20 +468,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
pullFromGoogleDoc = async (handler: PullHandler) => {
let dataDoc = this.dataDoc;
let documentId = StrCast(dataDoc[GoogleRef]);
- let exportState: GoogleApiClientUtils.ReadResult = {};
+ let exportState: Opt<GoogleApiClientUtils.ReadResult>;
if (documentId) {
exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
- updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ updateState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
let pullSuccess = false;
if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
const data = Cast(dataDoc.data, RichTextField);
if (data instanceof RichTextField) {
pullSuccess = true;
- dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+ dataDoc.data = RichTextUtils.Synthesize(exportState.body, data);
setTimeout(() => {
if (this._editorView) {
let state = this._editorView.state;
@@ -495,18 +499,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
DocumentDecorations.Instance.startPullOutcome(pullSuccess);
}
- checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
- let data = Cast(dataDoc.data, RichTextField);
- if (data) {
- let storedPlainText = data[ToPlainText]() + "\n";
- let receivedPlainText = exportState.body;
- let storedTitle = dataDoc.title;
- let receivedTitle = exportState.title;
- let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
- dataDoc.unchanged = unchanged;
- DocumentDecorations.Instance.setPullState(unchanged);
- }
+ checkState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
+ if (exportState && this._editorView) {
+ let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
+ let receivedPlainText = exportState.body;
+ let storedTitle = dataDoc.title;
+ let receivedTitle = exportState.title;
+ let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
+ dataDoc.unchanged = unchanged;
+ DocumentDecorations.Instance.setPullState(unchanged);
}
}
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 1b52e6f82..d2f76c969 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,11 +4,6 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
-export const ToPlainText = Symbol("PlainText");
-export const FromPlainText = Symbol("PlainText");
-const delimiter = "\n";
-const joiner = "";
-
@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@@ -28,48 +23,4 @@ export class RichTextField extends ObjectField {
return `new RichTextField("${this.Data}")`;
}
- public static Initialize = (initial: string) => {
- !initial.length && (initial = " ");
- let pos = initial.length + 1;
- return `{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"${initial}"}]}]},"selection":{"type":"text","anchor":${pos},"head":${pos}}}`;
- }
-
- [ToPlainText]() {
- // 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");
-
- // 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
- let textParagraphs: string[] = paragraphs.map(concatenateParagraph);
- let plainText = textParagraphs.join(joiner);
- return plainText.substring(0, plainText.length - 1);
- }
-
- [FromPlainText](plainText: string) {
- // 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" };
- text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
- return paragraph;
- });
-
- // 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);
- }
-
} \ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
new file mode 100644
index 000000000..b2b1dbaee
--- /dev/null
+++ b/src/new_fields/RichTextUtils.ts
@@ -0,0 +1,129 @@
+import { EditorState } from "prosemirror-state";
+import { Node } from "prosemirror-model";
+import { RichTextField } from "./RichTextField";
+import { docs_v1 } from "googleapis";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+
+export namespace RichTextUtils {
+
+ const delimiter = "\n";
+ const joiner = "";
+
+
+ export const Initialize = (initial?: string) => {
+ let content: any[] = [];
+ let state = {
+ doc: {
+ type: "doc",
+ content,
+ },
+ selection: {
+ type: "text",
+ anchor: 0,
+ head: 0
+ }
+ };
+ if (initial && initial.length) {
+ content.push({
+ type: "paragraph",
+ content: {
+ type: "text",
+ text: initial
+ }
+ });
+ state.selection.anchor = state.selection.head = initial.length + 1;
+ }
+ return JSON.stringify(state);
+ };
+
+ export const Synthesize = (plainText: string, oldState?: RichTextField) => {
+ return new RichTextField(ToProsemirrorState(plainText, oldState));
+ };
+
+ export const ToPlainText = (state: EditorState) => {
+ // Because we're working with plain text, just concatenate all paragraphs
+ let content = state.doc.content;
+ let paragraphs: Node<any>[] = [];
+ content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node));
+
+ // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
+ // Concatentate paragraphs and string the result together
+ let textParagraphs: string[] = paragraphs.map(paragraph => {
+ let text: string[] = [];
+ paragraph.content.forEach(node => node.text && text.push(node.text));
+ return text.join(joiner) + delimiter;
+ });
+ let plainText = textParagraphs.join(joiner);
+ return plainText.substring(0, plainText.length - 1);
+ };
+
+ export const ToProsemirrorState = (plainText: string, oldState?: RichTextField) => {
+ // 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(oldState ? oldState.Data : Initialize());
+ parsed.doc.content = elements.map(text => {
+ let paragraph: any = { type: "paragraph" };
+ text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
+ return paragraph;
+ });
+
+ // 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 just built
+ return JSON.stringify(parsed);
+ };
+
+ export namespace GoogleDocs {
+
+ export const Convert = (state: EditorState): GoogleApiClientUtils.Content => {
+ let textNodes: Node<any>[] = [];
+ let text = ToPlainText(state);
+ let content = state.doc.content;
+ content.forEach(node => node.content.forEach(node => node.type.name === "text" && textNodes.push(node)));
+ let links: docs_v1.Schema$Request[] = [];
+ let position = 1;
+ for (let node of textNodes) {
+ let link, length = node.nodeSize;
+ let marks = node.marks;
+ if (marks.length && (link = marks.find(mark => mark.type.name === "link"))) {
+ links.push(encode({
+ startIndex: position,
+ endIndex: position + length,
+ url: link.attrs.href,
+ }));
+ }
+ position += length;
+ }
+ return { text, links };
+ };
+
+ interface LinkInformation {
+ startIndex: number;
+ endIndex: number;
+ url: string;
+ }
+ const encode = (information: LinkInformation) => {
+ return {
+ updateTextStyle: {
+ fields: "*",
+ range: {
+ startIndex: information.startIndex,
+ endIndex: information.endIndex
+ },
+ textStyle: {
+ bold: true,
+ link: { url: information.url },
+ foregroundColor: { color: { rgbColor: { red: 0.0, green: 0.0, blue: 1.0 } } }
+ }
+ }
+ };
+ };
+ }
+
+} \ No newline at end of file