aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts265
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts51
-rw-r--r--src/client/documents/Documents.ts7
-rw-r--r--src/client/northstar/utils/Extensions.ts2
-rw-r--r--src/client/views/DocumentDecorations.scss31
-rw-r--r--src/client/views/DocumentDecorations.tsx7
-rw-r--r--src/client/views/Main.tsx20
-rw-r--r--src/client/views/MainView.tsx12
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx87
9 files changed, 291 insertions, 191 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 798886def..828d4451a 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -3,111 +3,130 @@ import { PostToServer } from "../../../Utils";
import { RouteStore } from "../../../server/RouteStore";
import { Opt } from "../../../new_fields/Doc";
import { isArray } from "util";
+import { EditorState } from "prosemirror-state";
+import { RichTextField } from "../../../new_fields/RichTextField";
export const Pulls = "googleDocsPullCount";
export const Pushes = "googleDocsPushCount";
export namespace GoogleApiClientUtils {
- export enum Service {
- Documents = "Documents",
- Slides = "Slides"
- }
-
export enum Actions {
Create = "create",
Retrieve = "retrieve",
Update = "update"
}
- export enum WriteMode {
- Insert,
- Replace
- }
-
- export type Identifier = string;
- export type Reference = Identifier | CreateOptions;
- export type TextContent = string | string[];
- 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 namespace Docs {
- export interface CreateOptions {
- service: Service;
- title?: string; // if excluded, will use a default title annotated with the current date
- }
+ export type RetrievalResult = Opt<docs_v1.Schema$Document>;
+ export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
- export interface RetrieveOptions {
- service: Service;
- identifier: Identifier;
- }
+ export interface UpdateOptions {
+ documentId: DocumentId;
+ requests: docs_v1.Schema$Request[];
+ }
- export interface ReadOptions {
- identifier: Identifier;
- removeNewlines?: boolean;
- }
+ export enum WriteMode {
+ Insert,
+ Replace
+ }
- export interface WriteOptions {
- mode: WriteMode;
- content: TextContent;
- reference: Reference;
- index?: number; // if excluded, will compute the last index of the document and append the content there
- }
+ export type DocumentId = string;
+ export type Reference = DocumentId | CreateOptions;
+ export interface Content {
+ text: string | string[];
+ requests: docs_v1.Schema$Request[];
+ }
+ export type IdHandler = (id: DocumentId) => any;
+ export type CreationResult = Opt<DocumentId>;
+ export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
+ export type ReadResult = { title: string, body: string };
+ export interface ImportResult {
+ title: string;
+ text: string;
+ state: EditorState;
+ }
- /**
- * After following the authentication routine, which connects this API call to the current signed in account
- * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
- * should appear in the user's Google Doc library instantaneously.
- *
- * @param options the title to assign to the new document, and the information necessary
- * to store the new documentId returned from the creation process
- * @returns the documentId of the newly generated document, or undefined if the creation process fails.
- */
- export const create = async (options: CreateOptions): Promise<CreationResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Create}`;
- const parameters = {
- requestBody: {
- title: options.title || `Dash Export (${new Date().toDateString()})`
- }
- };
- try {
- const schema: any = await PostToServer(path, parameters);
- let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id";
- return schema[key];
- } catch {
- return undefined;
+ export interface CreateOptions {
+ title?: string; // if excluded, will use a default title annotated with the current date
}
- };
- export namespace Docs {
+ export interface RetrieveOptions {
+ documentId: DocumentId;
+ }
- export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>;
- export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+ export interface ReadOptions {
+ documentId: DocumentId;
+ removeNewlines?: boolean;
+ }
- export interface UpdateOptions {
- documentId: Identifier;
- requests: docs_v1.Schema$Request[];
+ export interface WriteOptions {
+ mode: WriteMode;
+ content: Content;
+ reference: Reference;
+ index?: number; // if excluded, will compute the last index of the document and append the content there
}
+ /**
+ * After following the authentication routine, which connects this API call to the current signed in account
+ * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
+ * should appear in the user's Google Doc library instantaneously.
+ *
+ * @param options the title to assign to the new document, and the information necessary
+ * to store the new documentId returned from the creation process
+ * @returns the documentId of the newly generated document, or undefined if the creation process fails.
+ */
+ export const create = async (options: CreateOptions): Promise<CreationResult> => {
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+ const parameters = {
+ requestBody: {
+ title: options.title || `Dash Export (${new Date().toDateString()})`
+ }
+ };
+ try {
+ const schema: docs_v1.Schema$Document = await PostToServer(path, parameters);
+ return schema.documentId;
+ } catch {
+ return undefined;
+ }
+ };
+
export namespace Utils {
- export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
- const fragments: string[] = [];
+ export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
+ export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
+ let paragraphs = extractParagraphs(document);
+ let text = paragraphs.map(paragraph => paragraph.runs.map(run => run.content).join("")).join("");
+ text = text.substring(0, text.length - 1);
+ removeNewlines && text.ReplaceAll("\n", "");
+ return { text, paragraphs };
+ };
+
+ export type DeconstructedParagraph = { runs: docs_v1.Schema$TextRun[], bullet: Opt<number> };
+ const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
+ const fragments: DeconstructedParagraph[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
- if (element.paragraph && element.paragraph.elements) {
- for (const inner of element.paragraph.elements) {
- if (inner && inner.textRun) {
- const fragment = inner.textRun.content;
- fragment && fragments.push(fragment);
+ let runs: docs_v1.Schema$TextRun[] = [];
+ let bullet: Opt<number>;
+ if (element.paragraph) {
+ if (element.paragraph.elements) {
+ for (const inner of element.paragraph.elements) {
+ if (inner && inner.textRun) {
+ let run = inner.textRun;
+ (run.content || !filterEmpty) && runs.push(inner.textRun);
+ }
}
}
+ if (element.paragraph.bullet) {
+ bullet = element.paragraph.bullet.nestingLevel || 0;
+ }
}
+ (runs.length || !filterEmpty) && fragments.push({ runs, bullet });
}
}
- const text = fragments.join("");
- return removeNewlines ? text.ReplaceAll("\n", "") : text;
+ return fragments;
};
export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
@@ -130,27 +149,19 @@ export namespace GoogleApiClientUtils {
}
- const KeyMapping = new Map<Service, string>([
- [Service.Documents, "documentId"],
- [Service.Slides, "presentationId"]
- ]);
-
export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
try {
- let parameters: any = {}, key: string | undefined;
- if ((key = KeyMapping.get(options.service))) {
- parameters[key] = options.identifier;
- const schema: RetrievalResult = await PostToServer(path, parameters);
- return schema;
- }
+ const parameters = { documentId: options.documentId };
+ const schema: RetrievalResult = await PostToServer(path, parameters);
+ return schema;
} catch {
return undefined;
}
};
export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
- const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
const parameters = {
documentId: options.documentId,
requestBody: {
@@ -165,41 +176,49 @@ export namespace GoogleApiClientUtils {
}
};
- export const read = async (options: ReadOptions): Promise<ReadResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadResult = {};
+ export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
- let title = document.title;
- let body = Utils.extractText(document, options.removeNewlines);
- result = { title, body };
+ let title = document.title!;
+ let body = Utils.extractText(document, options.removeNewlines).text;
+ return { title, body };
}
- return result;
});
};
- export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadLinesResult = {};
+ export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
let title = document.title;
- let bodyLines = Utils.extractText(document).split("\n");
+ let bodyLines = Utils.extractText(document).text.split("\n");
options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
- result = { title, bodyLines };
+ return { title, bodyLines };
}
- return result;
});
};
+ export const setStyle = async (options: UpdateOptions) => {
+ let replies: any = await update({
+ documentId: options.documentId,
+ requests: options.requests
+ });
+ if ("errors" in replies) {
+ console.log("Write operation failed:");
+ console.log(replies.errors.map((error: any) => error.message));
+ }
+ return replies;
+ };
+
export const write = async (options: WriteOptions): Promise<UpdateResult> => {
const requests: docs_v1.Schema$Request[] = [];
- const identifier = await Utils.initialize(options.reference);
- if (!identifier) {
+ const documentId = await Utils.initialize(options.reference);
+ if (!documentId) {
return undefined;
}
let index = options.index;
const mode = options.mode;
if (!(index && mode === WriteMode.Insert)) {
- let schema = await retrieve({ identifier, service: Service.Documents });
+ let schema = await retrieve({ documentId });
if (!schema || !(index = Utils.endOf(schema))) {
return undefined;
}
@@ -215,7 +234,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,
@@ -225,47 +244,15 @@ export namespace GoogleApiClientUtils {
if (!requests.length) {
return undefined;
}
- let replies: any = await update({ documentId: identifier, requests });
- let errors = "errors";
- if (errors in replies) {
+ requests.push(...options.content.requests);
+ let replies: any = await update({ documentId: documentId, requests });
+ if ("errors" in replies) {
console.log("Write operation failed:");
- console.log(replies[errors].map((error: any) => error.message));
+ console.log(replies.errors.map((error: any) => error.message));
}
return replies;
};
}
- export namespace Slides {
-
- export namespace Utils {
-
- export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => {
- slides.map(slide => {
- let elements = slide.pageElements;
- if (elements) {
- let textboxes: slides_v1.Schema$TextContent[] = [];
- for (let element of elements) {
- if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) {
- textboxes.push(element.shape.text);
- }
- }
- textboxes.map(text => {
- if (text.textElements) {
- text.textElements.map(element => {
-
- });
- }
- if (text.lists) {
-
- }
- });
- }
- });
- };
-
- }
-
- }
-
} \ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
new file mode 100644
index 000000000..0864ebdb1
--- /dev/null
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -0,0 +1,51 @@
+import { Album } from "../../../server/apis/google/typings/albums";
+import { PostToServer } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { ImageField } from "../../../new_fields/URLField";
+
+export namespace GooglePhotosClientUtils {
+
+ export const Create = async (title: string) => {
+ let parameters = {
+ action: Album.Action.Create,
+ body: { album: { title } }
+ } as Album.Create;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const List = async (options?: Partial<Album.ListOptions>) => {
+ let parameters = {
+ action: Album.Action.List,
+ parameters: {
+ pageSize: (options ? options.pageSize : 20) || 20,
+ pageToken: (options ? options.pageToken : undefined) || undefined,
+ excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false,
+ } as Album.ListOptions
+ } as Album.List;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const Get = async (albumId: string) => {
+ let parameters = {
+ action: Album.Action.Get,
+ albumId
+ } as Album.Get;
+ return PostToServer(RouteStore.googlePhotosQuery, parameters);
+ };
+
+ export const toDataURL = (field: ImageField | undefined) => {
+ if (!field) {
+ return undefined;
+ }
+ const image = document.createElement("img");
+ image.src = field.url.href;
+ const canvas = document.createElement("canvas");
+ canvas.width = image.width;
+ canvas.height = image.height;
+ const ctx = canvas.getContext("2d")!;
+ ctx.drawImage(image, 0, 0);
+ const dataUrl = canvas.toDataURL("image/png");
+ return dataUrl.replace(/^data:image\/(png|jpg);base64,/, "");
+ };
+
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ef8b68c2f..4b7f1eeb6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -506,10 +506,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/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
index df14d4da0..ab9384f1f 100644
--- a/src/client/northstar/utils/Extensions.ts
+++ b/src/client/northstar/utils/Extensions.ts
@@ -1,6 +1,8 @@
interface String {
ReplaceAll(toReplace: string, replacement: string): string;
Truncate(length: number, replacement: string): String;
+ removeTrailingNewlines(): string;
+ hasNewline(): boolean;
}
String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ac8497bd0..470365627 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -266,6 +266,31 @@ $linkGap : 3px;
}
}
-@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
-@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
-@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } \ No newline at end of file
+@-moz-keyframes spin {
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
+}
+
+@-webkit-keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
+@keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes shadow-pulse {
+ 0% {
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 35px rgba(0, 0, 0, 0);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 700a4b49d..35c45b03c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -82,6 +82,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable public pullIcon: IconProp = "arrow-alt-circle-down";
@observable public pullColor: string = "white";
@observable public isAnimatingFetch = false;
+ @observable public isAnimatingPulse = false;
@observable public openHover = false;
public pullColorAnimating = false;
@@ -102,6 +103,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
public startPushOutcome = action((success: boolean) => {
if (!this.pushAnimating) {
this.pushAnimating = true;
+ this.isAnimatingPulse = false;
this.pushIcon = success ? "check-circle" : "stop-circle";
setTimeout(() => runInAction(() => {
this.pushIcon = "arrow-alt-circle-up";
@@ -707,9 +709,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return (
<div className={"linkButtonWrapper"}>
<div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+ if (!published) {
+ runInAction(() => this.isAnimatingPulse = true);
+ }
DocumentDecorations.hasPushedHack = false;
this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
- }}>
+ }} style={{ animation: this.isAnimatingPulse ? "shadow-pulse 1s infinite" : "none" }}>
<FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
</div>
</div>
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 0e687737d..271326f70 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,26 @@ import { Doc, DocListCastAsync } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { DocServer } from "../DocServer";
+String.prototype.removeTrailingNewlines = function () {
+ let sliced = this;
+ while (sliced.endsWith("\n")) {
+ sliced = sliced.substring(0, this.length - 1);
+ }
+ return sliced as string;
+};
+
+String.prototype.hasNewline = function () {
+ return this.endsWith("\n");
+};
+
+(Array.prototype as any).lastElement = function (this: any[]) {
+ if (!this.length) {
+ return undefined;
+ }
+ return this[this.length - 1];
+};
+
+
let swapDocs = async () => {
let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
// Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b64986084..f01083fbb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils';
import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
import { ClientUtils } from '../util/ClientUtils';
@@ -40,8 +40,11 @@ import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import PresModeMenu from './presentationview/PresentationModeMenu';
import { PresBox } from './nodes/PresBox';
+import { docs_v1 } from 'googleapis';
+import { Album } from '../../server/apis/google/typings/albums';
+import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils';
+import { ImageField } from '../../new_fields/URLField';
import { LinkFollowBox } from './linking/LinkFollowBox';
-import { DocumentManager } from '../util/DocumentManager';
@observer
export class MainView extends React.Component {
@@ -128,6 +131,11 @@ export class MainView extends React.Component {
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let image = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
+ let parameters = { title: StrCast(image.title), MEDIA_BINARY_DATA: GooglePhotosClientUtils.toDataURL(Cast(image.data, ImageField)) };
+ PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log);
+
reaction(() => {
let workspaces = CurrentUserUtils.UserDocument.workspaces;
let recent = CurrentUserUtils.UserDocument.recentlyClosed;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index bb44c5ac6..b671d06ea 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, numberRange } from '../../../Utils';
@@ -37,13 +37,13 @@ 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';
+import * as _ from "lodash";
import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
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;
@@ -62,13 +62,15 @@ 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.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
+ public static Instance: FormattedTextBox;
private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
@@ -380,7 +382,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) {
@@ -450,18 +452,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- let modes = GoogleApiClientUtils.WriteMode;
+ this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
+ let modes = GoogleApiClientUtils.Docs.WriteMode;
let mode = modes.Replace;
- let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
if (!reference) {
mode = modes.Insert;
- reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
+ reference = { 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.Export(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);
@@ -470,7 +471,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
};
let undo = () => {
- let content = exportState.body;
+ if (!exportState) {
+ return;
+ }
+ let content: GoogleApiClientUtils.Docs.Content = {
+ text: exportState.text,
+ requests: []
+ };
if (reference && content) {
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
@@ -483,49 +490,41 @@ 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.Docs.ImportResult>;
if (documentId) {
- exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId);
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
- updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, 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));
- setTimeout(() => {
- if (this._editorView) {
- let state = this._editorView.state;
- let end = state.doc.content.size - 1;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
- }
- }, 0);
- dataDoc.title = exportState.title;
- this.Document.customTitle = true;
- dataDoc.unchanged = true;
- }
+ if (exportState !== undefined) {
+ pullSuccess = true;
+ dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
+ setTimeout(() => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let end = state.doc.content.size - 1;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+ }
+ }, 0);
+ dataDoc.title = exportState.title;
+ this.Document.customTitle = true;
+ dataDoc.unchanged = true;
} else {
delete dataDoc[GoogleRef];
}
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.Docs.ImportResult>, dataDoc: Doc) => {
+ if (exportState && this._editorView) {
+ let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc);
+ let equalTitles = dataDoc.title === exportState.title;
+ let unchanged = equalContent && equalTitles;
+ dataDoc.unchanged = unchanged;
+ DocumentDecorations.Instance.setPullState(unchanged);
}
}