aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-08-30 13:43:04 -0400
committerSam Wilkins <samwilkins333@gmail.com>2019-08-30 13:43:04 -0400
commitc53d721dd95bdb4ccddc7a89dbdda72a88d29058 (patch)
treedb1093ff2f233dd574a48b4df66e538ef5cfc3b7
parent916aa5377a9f105cd128264f46c83b987861c713 (diff)
model sharing
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts22
-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/nodes/FormattedTextBox.tsx50
-rw-r--r--src/new_fields/RichTextUtils.ts82
7 files changed, 159 insertions, 55 deletions
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index ae7c2f997..9bf3cae38 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -3,6 +3,8 @@ 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";
@@ -40,6 +42,11 @@ export namespace GoogleApiClientUtils {
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;
+ data: RichTextField;
+ }
export interface CreateOptions {
title?: string; // if excluded, will use a default title annotated with the current date
@@ -87,13 +94,16 @@ export namespace GoogleApiClientUtils {
export namespace Utils {
- export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
+ export type ExtractResult = { text: string, runs: docs_v1.Schema$TextRun[] };
+ export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
let runs = extractTextRuns(document);
- const text = runs.map(run => run.content).join("");
- return removeNewlines ? text.ReplaceAll("\n", "") : text;
+ let text = runs.map(run => run.content).join("");
+ text = text.substring(0, text.length - 1);
+ removeNewlines && text.ReplaceAll("\n", "");
+ return { text, runs };
};
- export const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => {
+ const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => {
const fragments: docs_v1.Schema$TextRun[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
@@ -160,7 +170,7 @@ export namespace GoogleApiClientUtils {
return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
let title = document.title!;
- let body = Utils.extractText(document, options.removeNewlines);
+ let body = Utils.extractText(document, options.removeNewlines).text;
return { title, body };
}
});
@@ -170,7 +180,7 @@ export namespace GoogleApiClientUtils {
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));
return { title, bodyLines };
}
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 579806a89..109228b54 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";
@@ -698,9 +700,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/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index eefac2285..83011e590 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -60,16 +60,14 @@ export const GoogleRef = "googleDocId";
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
-type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.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 = () => {
- return EditorState.create(FormattedTextBox.Instance._configuration);
- }
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance._configuration);
public static Instance: FormattedTextBox;
private _configuration: any;
private _ref: React.RefObject<HTMLDivElement>;
@@ -434,7 +432,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+ this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
let modes = GoogleApiClientUtils.Docs.WriteMode;
let mode = modes.Replace;
let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
@@ -457,7 +455,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return;
}
let content: GoogleApiClientUtils.Docs.Content = {
- text: exportState.body,
+ text: exportState.text,
requests: []
};
if (reference && content) {
@@ -472,42 +470,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
pullFromGoogleDoc = async (handler: PullHandler) => {
let dataDoc = this.dataDoc;
let documentId = StrCast(dataDoc[GoogleRef]);
- let test = await RichTextUtils.GoogleDocs.Import(documentId);
- let exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>;
+ let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
if (documentId) {
- exportState = await GoogleApiClientUtils.Docs.read({ documentId });
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId);
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
- updateState = (exportState: Opt<GoogleApiClientUtils.Docs.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 = RichTextUtils.Synthesize(exportState.body, data);
- 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 = exportState.data;
+ 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: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+ checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
if (exportState && this._editorView) {
let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
- let receivedPlainText = exportState.body;
+ let receivedPlainText = exportState.text;
let storedTitle = dataDoc.title;
let receivedTitle = exportState.title;
let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 189819591..4ca51d311 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -1,5 +1,5 @@
import { EditorState } from "prosemirror-state";
-import { Node } from "prosemirror-model";
+import { Node, Fragment, Mark } from "prosemirror-model";
import { RichTextField } from "./RichTextField";
import { docs_v1 } from "googleapis";
import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
@@ -95,27 +95,75 @@ export namespace RichTextUtils {
};
};
- export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId) => {
- let document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
+ export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+ let Docs = GoogleApiClientUtils.Docs;
+ let document = await Docs.retrieve({ documentId });
+
if (!document) {
- return;
+ return undefined;
}
- // let title = document.title!;
- let runs = GoogleApiClientUtils.Docs.Utils.extractTextRuns(document);
+
+ let title = document.title!;
+
+ let { text, runs } = Docs.Utils.extractText(document);
+ let segments = runs[Symbol.iterator]();
+
let state = FormattedTextBox.blankState();
+ let breaks: number[] = [];
let from = 0;
- runs.map(run => {
- let text = run.content!;
- state = state.apply(state.tr.insertText(text, from));
- let to = from + text.length + 1;
- let href: Opt<string>;
- if (run.textStyle && run.textStyle.link && (href = run.textStyle.link.url)) {
- let mark = state.schema.mark(state.schema.marks.link, { href });
- state = state.apply(state.tr.addMark(from, to, mark));
+ let result = segments.next();
+ while (!result.done) {
+ let run = result.value;
+ let fragment = run.content!;
+ if (fragment.hasNewline()) {
+ let trimmed = fragment.removeTrailingNewlines();
+ if (fragment.length === 1) {
+ breaks.push(from);
+ } else {
+ let content = Fragment.from(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
+ let node = state.schema.node("paragraph", null, content);
+ state = state.apply(state.tr.insert(from, node));
+ from += node.nodeSize;
+ }
+ result = segments.next();
+ } else {
+ let nodes: Node[] = [];
+ nodes.push(state.schema.text(fragment, styleToMarks(state.schema, run.textStyle)));
+ result = segments.next();
+ while (!result.done) {
+ run = result.value;
+ fragment = run.content!;
+ let trimmed = fragment.removeTrailingNewlines();
+ nodes.push(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
+ if (fragment.hasNewline()) {
+ let node = state.schema.node("paragraph", null, Fragment.fromArray(nodes));
+ state = state.apply(state.tr.insert(from, node));
+ from += node.nodeSize;
+ result = segments.next();
+ break;
+ }
+ result = segments.next();
+ }
+ if (result.done) {
+ break;
+ }
}
- from = to;
- });
- // return { title, body };
+ }
+ breaks.forEach(position => state = state.apply(state.tr.insert(position, state.schema.node("paragraph"))));
+ let data = new RichTextField(JSON.stringify(state.toJSON()));
+ return { title, text, data };
+ };
+
+ const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
+ if (!textStyle) {
+ return undefined;
+ }
+ let marks: Mark[] = [];
+ if (textStyle.link) {
+ let href = textStyle.link.url;
+ marks.push(schema.mark(schema.marks.link, { href }));
+ }
+ return marks;
};
interface LinkInformation {