aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--src/client/DocServer.ts2
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts271
-rw-r--r--src/client/documents/DocumentTypes.ts22
-rw-r--r--src/client/documents/Documents.ts45
-rw-r--r--src/client/util/DictationManager.ts78
-rw-r--r--src/client/util/DocumentManager.ts4
-rw-r--r--src/client/util/DragManager.ts40
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts208
-rw-r--r--src/client/util/RichTextRules.ts7
-rw-r--r--src/client/util/RichTextSchema.tsx77
-rw-r--r--src/client/util/SelectionManager.ts1
-rw-r--r--src/client/util/TooltipTextMenu.tsx90
-rw-r--r--src/client/views/ContextMenu.tsx50
-rw-r--r--src/client/views/DocumentDecorations.scss23
-rw-r--r--src/client/views/DocumentDecorations.tsx179
-rw-r--r--src/client/views/EditableView.tsx28
-rw-r--r--src/client/views/GlobalKeyHandler.ts6
-rw-r--r--src/client/views/InkingCanvas.scss3
-rw-r--r--src/client/views/InkingCanvas.tsx20
-rw-r--r--src/client/views/Main.scss13
-rw-r--r--src/client/views/MainOverlayTextBox.tsx23
-rw-r--r--src/client/views/MainView.tsx94
-rw-r--r--src/client/views/MetadataEntryMenu.scss18
-rw-r--r--src/client/views/MetadataEntryMenu.tsx93
-rw-r--r--src/client/views/OverlayView.tsx30
-rw-r--r--src/client/views/PreviewCursor.tsx83
-rw-r--r--src/client/views/ScriptBox.tsx30
-rw-r--r--src/client/views/SearchBox.tsx183
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx49
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx3
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss48
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx21
-rw-r--r--src/client/views/collections/CollectionStackingView.scss26
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx184
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx90
-rw-r--r--src/client/views/collections/CollectionSubView.tsx21
-rw-r--r--src/client/views/collections/CollectionTreeView.scss13
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx159
-rw-r--r--src/client/views/collections/CollectionView.tsx36
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss105
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx404
-rw-r--r--src/client/views/collections/KeyRestrictionRow.tsx6
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx217
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx22
-rw-r--r--src/client/views/nodes/ButtonBox.scss19
-rw-r--r--src/client/views/nodes/ButtonBox.tsx91
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx2
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx18
-rw-r--r--src/client/views/nodes/DocumentView.tsx172
-rw-r--r--src/client/views/nodes/DragBox.scss13
-rw-r--r--src/client/views/nodes/DragBox.tsx101
-rw-r--r--src/client/views/nodes/FieldView.tsx10
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss43
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx479
-rw-r--r--src/client/views/nodes/ImageBox.scss45
-rw-r--r--src/client/views/nodes/ImageBox.tsx35
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx2
-rw-r--r--src/client/views/nodes/LinkMenu.tsx3
-rw-r--r--src/client/views/nodes/LinkMenuItem.tsx2
-rw-r--r--src/client/views/nodes/PDFBox.tsx2
-rw-r--r--src/client/views/nodes/PresBox.tsx529
-rw-r--r--src/client/views/nodes/VideoBox.tsx6
-rw-r--r--src/client/views/nodes/WebBox.scss70
-rw-r--r--src/client/views/nodes/WebBox.tsx121
-rw-r--r--src/client/views/pdf/Annotation.tsx6
-rw-r--r--src/client/views/pdf/PDFViewer.tsx7
-rw-r--r--src/client/views/pdf/Page.tsx20
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx615
-rw-r--r--src/client/views/presentationview/PresentationList.tsx52
-rw-r--r--src/client/views/presentationview/PresentationView.scss20
-rw-r--r--src/client/views/presentationview/PresentationView.tsx994
-rw-r--r--src/client/views/search/FilterBox.tsx2
-rw-r--r--src/client/views/search/IconBar.tsx1
-rw-r--r--src/client/views/search/IconButton.tsx2
-rw-r--r--src/client/views/search/SearchBox.scss5
-rw-r--r--src/client/views/search/SearchBox.tsx20
-rw-r--r--src/client/views/search/SearchItem.tsx4
-rw-r--r--src/new_fields/Doc.ts118
-rw-r--r--src/new_fields/ListSpec.ts0
-rw-r--r--src/new_fields/PresField.ts6
-rw-r--r--src/new_fields/RichTextField.ts50
-rw-r--r--src/new_fields/SchemaHeaderField.ts14
-rw-r--r--src/new_fields/Types.ts4
-rw-r--r--src/new_fields/util.ts3
-rw-r--r--src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloudbin0 -> 172 bytes
-rw-r--r--src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloudbin0 -> 172 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_NewO.docxbin2264571 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_OLPC.docxbin6721592 -> 0 bytes
-rw-r--r--src/server/Message.ts1
-rw-r--r--src/server/RouteStore.ts3
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts130
-rw-r--r--src/server/apis/youtube/youtubeApiSample.d.ts (renamed from src/server/youtubeApi/youtubeApiSample.d.ts)0
-rw-r--r--src/server/apis/youtube/youtubeApiSample.js (renamed from src/server/youtubeApi/youtubeApiSample.js)0
-rw-r--r--src/server/authentication/config/passport.ts10
-rw-r--r--src/server/authentication/models/current_user_utils.ts34
-rw-r--r--src/server/credentials/google_docs_credentials.json1
-rw-r--r--src/server/credentials/google_docs_token.json1
-rw-r--r--src/server/index.ts67
-rw-r--r--src/server/slides.json10820
-rw-r--r--views/stylesheets/authentication.css6
105 files changed, 14990 insertions, 2931 deletions
diff --git a/package.json b/package.json
index 1c7a10ac8..ec5af93b1 100644
--- a/package.json
+++ b/package.json
@@ -75,6 +75,7 @@
"@types/express-session": "^1.15.12",
"@types/express-validator": "^3.0.0",
"@types/formidable": "^1.0.31",
+ "@types/gapi": "0.0.39",
"@types/jquery": "^3.3.29",
"@types/jquery-awesome-cursor": "^0.3.0",
"@types/jsonwebtoken": "^8.3.2",
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index bf5168c22..2cec1046b 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -33,6 +33,8 @@ export namespace DocServer {
LivePlayground = 3,
}
+ export let AclsMode = WriteMode.Default;
+
const fieldWriteModes: { [field: string]: WriteMode } = {};
const docsWithUpdates: { [field: string]: Set<Doc> } = {};
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
new file mode 100644
index 000000000..798886def
--- /dev/null
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -0,0 +1,271 @@
+import { docs_v1, slides_v1 } from "googleapis";
+import { PostToServer } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { Opt } from "../../../new_fields/Doc";
+import { isArray } from "util";
+
+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 interface CreateOptions {
+ service: Service;
+ title?: string; // if excluded, will use a default title annotated with the current date
+ }
+
+ export interface RetrieveOptions {
+ service: Service;
+ identifier: Identifier;
+ }
+
+ export interface ReadOptions {
+ identifier: Identifier;
+ removeNewlines?: boolean;
+ }
+
+ 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
+ }
+
+ /**
+ * 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 namespace Docs {
+
+ export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>;
+ export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+
+ export interface UpdateOptions {
+ documentId: Identifier;
+ requests: docs_v1.Schema$Request[];
+ }
+
+ export namespace Utils {
+
+ export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
+ const fragments: string[] = [];
+ 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);
+ }
+ }
+ }
+ }
+ }
+ const text = fragments.join("");
+ return removeNewlines ? text.ReplaceAll("\n", "") : text;
+ };
+
+ export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
+ if (schema.body && schema.body.content) {
+ const paragraphs = schema.body.content.filter(el => el.paragraph);
+ if (paragraphs.length) {
+ const target = paragraphs[paragraphs.length - 1];
+ if (target.paragraph && target.paragraph.elements) {
+ length = target.paragraph.elements.length;
+ if (length) {
+ const final = target.paragraph.elements[length - 1];
+ return final.endIndex ? final.endIndex - 1 : undefined;
+ }
+ }
+ }
+ }
+ };
+
+ export const initialize = async (reference: Reference) => typeof reference === "string" ? reference : create(reference);
+
+ }
+
+ 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}`;
+ 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;
+ }
+ } catch {
+ return undefined;
+ }
+ };
+
+ export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
+ const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`;
+ const parameters = {
+ documentId: options.documentId,
+ requestBody: {
+ requests: options.requests
+ }
+ };
+ try {
+ const replies: UpdateResult = await PostToServer(path, parameters);
+ return replies;
+ } catch {
+ return undefined;
+ }
+ };
+
+ export const read = async (options: ReadOptions): Promise<ReadResult> => {
+ return retrieve({ ...options, service: Service.Documents }).then(document => {
+ let result: ReadResult = {};
+ if (document) {
+ let title = document.title;
+ let body = Utils.extractText(document, options.removeNewlines);
+ result = { title, body };
+ }
+ return result;
+ });
+ };
+
+ export const readLines = async (options: ReadOptions): Promise<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 result;
+ });
+ };
+
+ export const write = async (options: WriteOptions): Promise<UpdateResult> => {
+ const requests: docs_v1.Schema$Request[] = [];
+ const identifier = await Utils.initialize(options.reference);
+ if (!identifier) {
+ return undefined;
+ }
+ let index = options.index;
+ const mode = options.mode;
+ if (!(index && mode === WriteMode.Insert)) {
+ let schema = await retrieve({ identifier, service: Service.Documents });
+ if (!schema || !(index = Utils.endOf(schema))) {
+ return undefined;
+ }
+ }
+ if (mode === WriteMode.Replace) {
+ index > 1 && requests.push({
+ deleteContentRange: {
+ range: {
+ startIndex: 1,
+ endIndex: index
+ }
+ }
+ });
+ index = 1;
+ }
+ const text = options.content;
+ text.length && requests.push({
+ insertText: {
+ text: isArray(text) ? text.join("\n") : text,
+ location: { index }
+ }
+ });
+ if (!requests.length) {
+ return undefined;
+ }
+ let replies: any = await update({ documentId: identifier, requests });
+ let errors = "errors";
+ if (errors in replies) {
+ console.log("Write operation failed:");
+ 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/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
new file mode 100644
index 000000000..1578e49fe
--- /dev/null
+++ b/src/client/documents/DocumentTypes.ts
@@ -0,0 +1,22 @@
+export enum DocumentType {
+ NONE = "none",
+ TEXT = "text",
+ HIST = "histogram",
+ IMG = "image",
+ WEB = "web",
+ COL = "collection",
+ KVP = "kvp",
+ VID = "video",
+ AUDIO = "audio",
+ PDF = "pdf",
+ ICON = "icon",
+ IMPORT = "import",
+ LINK = "link",
+ LINKDOC = "linkdoc",
+ BUTTON = "button",
+ TEMPLATE = "template",
+ EXTENSION = "extension",
+ YOUTUBE = "youtube",
+ DRAGBOX = "dragbox",
+ PRES = "presentation",
+} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7dd853156..1e9d1687f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,24 +1,3 @@
-export enum DocumentType {
- NONE = "none",
- TEXT = "text",
- HIST = "histogram",
- IMG = "image",
- WEB = "web",
- COL = "collection",
- KVP = "kvp",
- VID = "video",
- AUDIO = "audio",
- PDF = "pdf",
- ICON = "icon",
- IMPORT = "import",
- LINK = "link",
- LINKDOC = "linkdoc",
- BUTTON = "button",
- TEMPLATE = "template",
- EXTENSION = "extension",
- YOUTUBE = "youtube",
-}
-
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
import { HistogramOperation } from "../northstar/operations/HistogramOperation";
@@ -60,9 +39,14 @@ import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
import { Scripting, CompileScript } from "../util/Scripting";
import { ButtonBox } from "../views/nodes/ButtonBox";
+import { DragBox } from "../views/nodes/DragBox";
import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
+import { PresBox } from "../views/nodes/PresBox";
import { ComputedField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
+import { DocumentType } from "./DocumentTypes";
+//import { PresBox } from "../views/nodes/PresBox";
+//import { PresField } from "../../new_fields/PresField";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -93,6 +77,7 @@ export interface DocumentOptions {
borderRounding?: string;
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
+ autoHeight?: boolean;
dbDoc?: Doc;
// [key: string]: Opt<Field>;
}
@@ -177,6 +162,14 @@ export namespace Docs {
}],
[DocumentType.BUTTON, {
layout: { view: ButtonBox },
+ }],
+ [DocumentType.PRES, {
+ layout: { view: PresBox },
+ options: {}
+ }],
+ [DocumentType.DRAGBOX, {
+ layout: { view: DragBox },
+ options: { width: 40, height: 40 },
}]
]);
@@ -298,7 +291,7 @@ export namespace Docs {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
- protoProps.author = CurrentUserUtils.email;
+ protoProps.author = Doc.CurrentUserEmail;
}
if (!("creationDate" in protoProps)) {
@@ -346,6 +339,9 @@ export namespace Docs {
.catch((err: any) => console.log(err));
return inst;
}
+ export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.PRES), initial, options);
+ }
export function VideoDocument(url: string, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options);
@@ -442,6 +438,11 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) });
}
+
+ export function DragboxDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), undefined, { ...(options || {}) });
+ }
+
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 9c61fe125..fb3c15cea 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -2,9 +2,10 @@ import { SelectionManager } from "./SelectionManager";
import { DocumentView } from "../views/nodes/DocumentView";
import { UndoManager } from "./UndoManager";
import * as interpreter from "words-to-numbers";
+import { DocumentType } from "../documents/DocumentTypes";
import { Doc } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
-import { Docs, DocumentType } from "../documents/Documents";
+import { Docs } from "../documents/Documents";
import { CollectionViewType } from "../views/collections/CollectionBaseView";
import { Cast, CastCtor } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
@@ -12,6 +13,7 @@ import { AudioField, ImageField } from "../../new_fields/URLField";
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { MainView } from "../views/MainView";
import { Utils } from "../../Utils";
+import { RichTextField } from "../../new_fields/RichTextField";
/**
* This namespace provides a singleton instance of a manager that
@@ -43,7 +45,7 @@ export namespace DictationManager {
export namespace Controls {
- const infringe = "unable to process: dictation manager still involved in previous session";
+ export const Infringed = "unable to process: dictation manager still involved in previous session";
const intraSession = ". ";
const interSession = " ... ";
@@ -62,35 +64,45 @@ export namespace DictationManager {
export type ListeningUIStatus = { interim: boolean } | false;
export interface ListeningOptions {
+ useOverlay: boolean;
language: string;
continuous: ContinuityArgs;
delimiters: DelimiterArgs;
interimHandler: InterimResultHandler;
tryExecute: boolean;
+ terminators: string[];
}
export const listen = async (options?: Partial<ListeningOptions>) => {
let results: string | undefined;
let main = MainView.Instance;
- main.dictationOverlayVisible = true;
- main.isListening = { interim: false };
+ let overlay = options !== undefined && options.useOverlay;
+ if (overlay) {
+ main.dictationOverlayVisible = true;
+ main.isListening = { interim: false };
+ }
try {
results = await listenImpl(options);
if (results) {
Utils.CopyText(results);
- main.isListening = false;
- let execute = options && options.tryExecute;
- main.dictatedPhrase = execute ? results.toLowerCase() : results;
- main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ if (overlay) {
+ main.isListening = false;
+ let execute = options && options.tryExecute;
+ main.dictatedPhrase = execute ? results.toLowerCase() : results;
+ main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ }
+ options && options.tryExecute && await DictationManager.Commands.execute(results);
}
} catch (e) {
- main.isListening = false;
- main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
- main.dictationSuccess = false;
+ if (overlay) {
+ main.isListening = false;
+ main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+ main.dictationSuccess = false;
+ }
} finally {
- main.initiateDictationFade();
+ overlay && main.initiateDictationFade();
}
return results;
@@ -98,7 +110,7 @@ export namespace DictationManager {
const listenImpl = (options?: Partial<ListeningOptions>) => {
if (isListening) {
- return infringe;
+ return Infringed;
}
isListening = true;
@@ -126,6 +138,12 @@ export namespace DictationManager {
recognizer.onresult = (e: SpeechRecognitionEvent) => {
current = synthesize(e, intra);
+ let matchedTerminator: string | undefined;
+ if (options && options.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) {
+ current = matchedTerminator;
+ recognizer.abort();
+ return complete();
+ }
handler && handler(current);
isManuallyStopped && complete();
};
@@ -161,13 +179,13 @@ export namespace DictationManager {
}
isManuallyStopped = true;
salvageSession ? recognizer.stop() : recognizer.abort();
- let main = MainView.Instance;
- if (main.dictationOverlayVisible) {
- main.cancelDictationFade();
- main.dictationOverlayVisible = false;
- main.dictationSuccess = undefined;
- setTimeout(() => main.dictatedPhrase = placeholder, 500);
- }
+ // let main = MainView.Instance;
+ // if (main.dictationOverlayVisible) {
+ // main.cancelDictationFade();
+ // main.dictationOverlayVisible = false;
+ // main.dictationSuccess = undefined;
+ // setTimeout(() => main.dictatedPhrase = placeholder, 500);
+ // }
};
const synthesize = (e: SpeechRecognitionEvent, delimiter?: string) => {
@@ -299,11 +317,20 @@ export namespace DictationManager {
}
}],
- ["promote", {
+ ["new outline", {
action: (target: DocumentView) => {
- console.log(target);
- },
- restrictTo: [DocumentType.TEXT]
+ let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
+ newBox.autoHeight = true;
+ let proto = newBox.proto!;
+ proto.page = -1;
+ let prompt = "Press alt + r to start dictating here...";
+ let head = 3;
+ let anchor = head + prompt.length;
+ let proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
+ proto.data = new RichTextField(proseMirrorState);
+ proto.backgroundColor = "#eeffff";
+ target.props.addDocTab(newBox, proto, "onRight");
+ }
}]
]);
@@ -317,6 +344,9 @@ export namespace DictationManager {
let what = matches[2];
let dataDoc = Doc.GetProto(target.props.Document);
let fieldKey = "data";
+ if (isNaN(count)) {
+ return;
+ }
for (let i = 0; i < count; i++) {
let created: Doc | undefined;
switch (what) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 7f526b247..ec731da84 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,6 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
import { undoBatch, UndoManager } from './UndoManager';
+import { Scripting } from './Scripting';
export class DocumentManager {
@@ -202,4 +203,5 @@ export class DocumentManager {
return 1;
}
}
-} \ No newline at end of file
+}
+Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index a7aaaed7c..4c9c9c17c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,5 +1,5 @@
import { action, runInAction } from "mobx";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, Field } from "../../new_fields/Doc";
import { Cast, StrCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
@@ -9,8 +9,11 @@ import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { DocumentDecorations } from "../views/DocumentDecorations";
-import { NumberLiteralType } from "typescript";
+import { Docs } from "../documents/Documents";
+import { CompileScript } from "./Scripting";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { List } from "../../new_fields/List";
+import { PrefetchProxy } from "../../new_fields/Proxy";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -142,6 +145,8 @@ export namespace DragManager {
withoutShiftDrag?: boolean;
+ finishDrag?: (dropData: { [id: string]: any }) => void;
+
offsetX?: number;
offsetY?: number;
@@ -211,6 +216,7 @@ export namespace DragManager {
dropAction: dropActionType;
userDropAction: dropActionType;
moveDocument?: MoveFunction;
+ applyAsTemplate?: boolean;
[id: string]: any;
}
@@ -235,7 +241,7 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
- StartDrag(eles, dragData, downX, downY, options,
+ StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
(dropData: { [id: string]: any }) => {
(dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
@@ -246,6 +252,28 @@ export namespace DragManager {
});
}
+ export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize?: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+ let dragData = new DragManager.DocumentDragData([], [undefined]);
+ runInAction(() => StartDragFunctions.map(func => func()));
+ StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
+ (dropData: { [id: string]: any }) => {
+ let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
+ let compiled = CompileScript(script, {
+ params: { doc: Doc.name },
+ typecheck: false,
+ editable: true
+ });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ bd.onClick = scriptField;
+ }
+ params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
+ initialize && initialize(bd);
+ bd.buttonParams = new List<string>(params);
+ dropData.droppedDocuments = [bd];
+ });
+ }
+
export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
dragData.moveDocument = moveLinkedDocument;
@@ -481,7 +509,7 @@ export namespace DragManager {
// if (parent && dragEle) parent.appendChild(dragEle);
});
if (target) {
- if (finishDrag) finishDrag(dragData);
+ finishDrag && finishDrag(dragData);
target.dispatchEvent(
new CustomEvent<DropEvent>("dashOnDrop", {
@@ -490,7 +518,7 @@ export namespace DragManager {
x: e.x,
y: e.y,
data: dragData,
- mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : ""
+ mods: e.altKey ? "AltKey" : e.ctrlKey ? "CtrlKey" : e.metaKey ? "MetaKey" : ""
}
})
);
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index c38f84551..3979d8a49 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -1,14 +1,10 @@
-import { Schema, NodeType } from "prosemirror-model";
-import {
- wrapIn, setBlockType, chainCommands, toggleMark, exitCode,
- joinUp, joinDown, lift, selectParentNode
-} from "prosemirror-commands";
-import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list";
-import { undo, redo } from "prosemirror-history";
+import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
import { undoInputRule } from "prosemirror-inputrules";
-import { Transaction, EditorState } from "prosemirror-state";
+import { Schema } from "prosemirror-model";
+import { liftListItem, splitListItem, wrapInList, sinkListItem } from "prosemirror-schema-list";
+import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { TooltipTextMenu } from "./TooltipTextMenu";
-import { Statement } from "../northstar/model/idea/idea";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -30,79 +26,151 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Shift-Mod-z", redo);
bind("Backspace", undoInputRule);
- if (!mac) {
- bind("Mod-y", redo);
- }
+ !mac && bind("Mod-y", redo);
bind("Alt-ArrowUp", joinUp);
bind("Alt-ArrowDown", joinDown);
bind("Mod-BracketLeft", lift);
bind("Escape", selectParentNode);
- if (type = schema.marks.strong) {
- bind("Mod-b", toggleMark(type));
- bind("Mod-B", toggleMark(type));
- }
- if (type = schema.marks.em) {
- bind("Mod-i", toggleMark(type));
- bind("Mod-I", toggleMark(type));
- }
- if (type = schema.marks.underline) {
- bind("Mod-u", toggleMark(type));
- bind("Mod-U", toggleMark(type));
- }
- if (type = schema.marks.code) {
- bind("Mod-`", toggleMark(type));
- }
+ bind("Mod-b", toggleMark(schema.marks.strong));
+ bind("Mod-B", toggleMark(schema.marks.strong));
- if (type = schema.nodes.bullet_list) {
- bind("Ctrl-.", wrapInList(type));
- }
- if (type = schema.nodes.ordered_list) {
- bind("Ctrl-n", wrapInList(type));
- }
- if (type = schema.nodes.blockquote) {
- bind("Ctrl->", wrapIn(type));
- }
- if (type = schema.nodes.hard_break) {
- let br = type, cmd = chainCommands(exitCode, (state, dispatch) => {
- if (dispatch) {
- dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView());
- return true;
- }
- return false;
- });
- bind("Mod-Enter", cmd);
- bind("Shift-Enter", cmd);
- if (mac) {
- bind("Ctrl-Enter", cmd);
+ bind("Mod-e", toggleMark(schema.marks.em));
+ bind("Mod-E", toggleMark(schema.marks.em));
+
+ bind("Mod-u", toggleMark(schema.marks.underline));
+ bind("Mod-U", toggleMark(schema.marks.underline));
+
+ bind("Mod-`", toggleMark(schema.marks.code));
+
+ bind("Ctrl-.", wrapInList(schema.nodes.bullet_list));
+
+ bind("Ctrl-n", wrapInList(schema.nodes.ordered_list));
+
+ bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+
+
+ let cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ return true;
}
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ mac && bind("Ctrl-Enter", cmd);
+
+
+ bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
+
+ bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
}
- if (type = schema.nodes.list_item) {
- bind("Enter", splitListItem(type));
- bind("Shift-Tab", liftListItem(type));
- bind("Tab", sinkListItem(type));
- }
- if (type = schema.nodes.paragraph) {
- bind("Shift-Ctrl-0", setBlockType(type));
- }
- if (type = schema.nodes.code_block) {
- bind("Shift-Ctrl-\\", setBlockType(type));
- }
- if (type = schema.nodes.heading) {
- for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, setBlockType(type, { level: i }));
+
+ let hr = schema.nodes.horizontal_rule;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ bind("Mod-s", TooltipTextMenu.insertStar);
+
+ let nodeTypeMark = (depth: number) => depth === 2 ? "indent2" : depth === 4 ? "indent3" : depth === 6 ? "indent4" : "indent1";
+
+ let bulletFunc = (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var ref = state.selection;
+ var range = ref.$from.blockRange(ref.$to);
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ let depth = range && range.depth ? range.depth : 0;
+ if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ const resolvedPos = tx2.doc.resolve(range!.start);
+
+ let path = (resolvedPos as any).path;
+ for (let i = path.length - 1; i > 0; i--) {
+ if (path[i].type === schema.nodes.ordered_list) {
+ path[i].attrs.bulletStyle = nodeTypeMark(depth);
+ break;
+ }
+ }
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ })) {
+ let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
+ let newstate = state.applyTransaction(sxf);
+ if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
+ const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
+ let path = (resolvedPos as any).path;
+ for (let i = path.length - 1; i > 0; i--) {
+ if (path[i].type === schema.nodes.ordered_list) {
+ path[i].attrs.bulletStyle = nodeTypeMark(depth);
+ break;
+ }
+ }
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ // marks && tx2.ensureMarks([...marks]);
+ // marks && tx2.setStoredMarks([...marks]);
+
+ dispatch(tx2);
+ })) {
+ console.log("bullet fail");
+ }
}
- }
- if (type = schema.nodes.horizontal_rule) {
- let hr = type;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
- return true;
+ };
+
+ bind("Tab", bulletFunc);
+
+ bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var ref = state.selection;
+ var range = ref.$from.blockRange(ref.$to);
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ let depth = range && range.depth > 3 ? range.depth - 4 : 0;
+ liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ try {
+ const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
+
+ let path = (resolvedPos as any).path;
+ for (let i = path.length - 1; i > 0; i--) {
+ if (path[i].type === schema.nodes.ordered_list) {
+ path[i].attrs.bulletStyle = nodeTypeMark(depth);
+ break;
+ }
+ }
+
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ dispatch(tx2);
+ } catch (e) {
+ dispatch(tx2);
+ }
});
- }
+ });
+
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => {
+ marks && tx3.ensureMarks(marks);
+ marks && tx3.setStoredMarks(marks);
+ dispatch(tx3);
+ })) {
+ if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
+ marks && tx3.ensureMarks(marks);
+ marks && tx3.setStoredMarks(marks);
+ if (!liftListItem(schema.nodes.list_item)(state, dispatch as ((tx: Transaction<Schema<any, any>>) => void))
+ ) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
- bind("Mod-s", TooltipTextMenu.insertStar);
return keys;
}
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 3b8396510..8c4c76027 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -26,6 +26,13 @@ export const inpRules = {
match => ({ order: +match[1] }),
(match, node) => node.childCount + node.attrs.order === +match[1]
),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^([a-z]+)\.\s$/,
+ schema.nodes.alphabet_list,
+ match => ({ order: +match[1] }),
+ (match, node) => node.childCount + node.attrs.order === +match[1]
+ ),
// * bullet list
wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index ce9e29b26..f567d803e 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,11 +1,7 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray, NodeType, Slice, Mark, Node } from "prosemirror-model";
-import { joinUp, lift, setBlockType, toggleMark, wrapIn, selectNodeForward, deleteSelection } from 'prosemirror-commands';
-import { redo, undo } from 'prosemirror-history';
-import { orderedList, bulletList, listItem, } from 'prosemirror-schema-list';
-import { EditorState, Transaction, NodeSelection, TextSelection, Selection, } from "prosemirror-state";
-import { EditorView, } from "prosemirror-view";
-import { View } from '@react-pdf/renderer';
+import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { TextSelection } from "prosemirror-state";
+import { Doc } from "../../new_fields/Doc";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -178,7 +174,20 @@ export const nodes: { [index: string]: NodeSpec } = {
ordered_list: {
...orderedList,
content: 'list_item+',
- group: 'block'
+ group: 'block',
+ attrs: {
+ bulletStyle: { default: "" },
+ mapStyle: { default: "decimal" }
+ },
+ toDOM(node: Node<any>) {
+ const bs = node.attrs.bulletStyle;
+ const decMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "decimal2" : bs === "indent3" ? "decimal3" : bs === "indent4" ? "decimal4" : "";
+ const multiMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "upper-alpha" : bs === "indent3" ? "lower-roman" : bs === "indent4" ? "lower-alpha" : "";
+ let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+ for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map;
+ return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
+ //return ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle};`, 0]
+ }
},
//this doesn't currently work for some reason
bullet_list: {
@@ -186,8 +195,12 @@ export const nodes: { [index: string]: NodeSpec } = {
content: 'list_item+',
group: 'block',
// parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
- // toDOM() { return ulDOM }
+ toDOM(node: Node<any>) {
+ for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = "";
+ return ['ul', 0];
+ }
},
+
//bullet_list: {
// content: 'list_item+',
// group: 'block',
@@ -197,9 +210,15 @@ export const nodes: { [index: string]: NodeSpec } = {
//select: state => true,
// },
list_item: {
+ attrs: {
+ className: { default: "" }
+ },
...listItem,
- content: 'paragraph block*'
- }
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ return ["li", { class: node.attrs.className }, 0];
+ }
+ },
};
const emDOM: DOMOutputSpecArray = ["em", 0];
@@ -230,7 +249,7 @@ export const marks: { [index: string]: MarkSpec } = {
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }],
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
toDOM() { return emDOM; }
},
@@ -282,6 +301,17 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM: () => ['sup']
},
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
highlight: {
parseDOM: [{ style: 'color: blue' }],
toDOM() {
@@ -300,6 +330,24 @@ export const marks: { [index: string]: MarkSpec } = {
}
},
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ hide_users: { default: [] },
+ opened: { default: false }
+ },
+ group: "inline",
+ inclusive: false,
+ toDOM(node: any) {
+ let hideUsers = node.attrs.hide_users;
+ let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
+ return hidden ?
+ ['span', { class: node.attrs.opened ? "userMarkOpen" : "userMark" }, 0] :
+ ['span', 0];
+ }
+ },
+
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
@@ -375,7 +423,6 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
fontSize: { default: 10 }
},
- inclusive: false,
parseDOM: [{ style: 'font-size: 10px;' }],
toDOM: (node) => ['span', {
style: `font-size: ${node.attrs.fontSize}px;`
@@ -554,6 +601,8 @@ export class SummarizedView {
attrs.textslice = newSelection.content().toJSON();
view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
+ let marks = view.state.storedMarks.filter((m: any) => m.type !== view.state.schema.marks.highlight);
+ view.state.storedMarks = marks;
self._collapsed.textContent = "㊉";
} else {
// node.attrs.visibility = !node.attrs.visibility;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index ee623d082..9efef888d 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,7 +4,6 @@ import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { NumCast, StrCast } from "../../new_fields/Types";
import { InkingControl } from "../views/InkingControl";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
export namespace SelectionManager {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index d33a52d7f..e979e6cde 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -1,32 +1,25 @@
-import { action, observable, observe } from "mobx";
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faTag, faPlus, faCloudUploadAlt } from '@fortawesome/free-solid-svg-icons';
-import { Dropdown, MenuItem, icons, } from "prosemirror-menu"; //no import css
-import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType, Mark, ResolvedPos } from "prosemirror-model";
-import { Node as ProsNode } from "prosemirror-model";
-import "./TooltipTextMenu.scss";
-const { toggleMark, setBlockType } = require("prosemirror-commands");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, liftListItem, } from 'prosemirror-schema-list';
import { faListUl } from '@fortawesome/free-solid-svg-icons';
-import { FieldViewProps } from "../views/nodes/FieldView";
-const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
-import { DragManager } from "./DragManager";
-import { Doc, Opt, Field } from "../../new_fields/Doc";
+import { action, observable } from "mobx";
+import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { liftListItem, wrapInList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorView } from "prosemirror-view";
+import { Doc, Field, Opt } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
+import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
+import { FieldViewProps } from "../views/nodes/FieldView";
+import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
-import { Id } from "../../new_fields/FieldSymbols";
-import { FormattedTextBoxProps, FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { typeAlias } from "babel-types";
-import React, { Children } from "react";
-import ReactDOM from "react-dom";
-import { Utils } from "../../Utils";
+import { DragManager } from "./DragManager";
import { LinkManager } from "./LinkManager";
-import { bool } from "prop-types";
+import { schema } from "./RichTextSchema";
+import "./TooltipTextMenu.scss";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.
export class TooltipTextMenu {
@@ -35,11 +28,11 @@ export class TooltipTextMenu {
private view: EditorView;
private fontStyles: MarkType[];
private fontSizes: MarkType[];
- private listTypes: NodeType[];
+ private listTypes: (NodeType | any)[];
private editorProps: FieldViewProps & FormattedTextBoxProps;
private fontSizeToNum: Map<MarkType, number>;
private fontStylesToName: Map<MarkType, string>;
- private listTypeToIcon: Map<NodeType, string>;
+ private listTypeToIcon: Map<NodeType | any, string>;
//private link: HTMLAnchorElement;
private wrapper: HTMLDivElement;
private extras: HTMLDivElement;
@@ -67,6 +60,8 @@ export class TooltipTextMenu {
@observable
private _storedMarks: Mark<any>[] | null | undefined;
+ public HackToFixTextSelectionGlitch: boolean = false;
+
constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
this.view = view;
@@ -170,13 +165,23 @@ export class TooltipTextMenu {
this.fontSizeToNum.set(schema.marks.p48, 48);
this.fontSizeToNum.set(schema.marks.p72, 72);
this.fontSizeToNum.set(schema.marks.pFontSize, 10);
- this.fontSizeToNum.set(schema.marks.pFontSize, 10);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 12);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 14);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 16);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 18);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 20);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 24);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 32);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 48);
+ // this.fontSizeToNum.set(schema.marks.pFontSize, 72);
this.fontSizes = Array.from(this.fontSizeToNum.keys());
//list types
this.listTypeToIcon = new Map();
this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
- this.listTypeToIcon.set(schema.nodes.ordered_list, "1)");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A");
+ // this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜");
this.listTypes = Array.from(this.listTypeToIcon.keys());
//custom tools
@@ -191,8 +196,6 @@ export class TooltipTextMenu {
this.update(view, undefined);
- //view.dom.parentNode!.parentNode!.insertBefore(this.tooltip, view.dom.parentNode);
-
// add tooltip to outerdiv to circumvent scaling problem
const outer_div = this.editorProps.outer_div;
outer_div && outer_div(this.wrapper);
@@ -510,10 +513,28 @@ export class TooltipTextMenu {
//remove all node typeand apply the passed-in one to the selected text
changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
- //remove old
- liftListItem(schema.nodes.list_item)(view.state, view.dispatch);
- if (nodeType) { //add new
+ //remove oldif (nodeType) { //add new
+ if (nodeType === schema.nodes.bullet_list) {
wrapInList(nodeType)(view.state, view.dispatch);
+ } else {
+ var ref = view.state.selection;
+ var range = ref.$from.blockRange(ref.$to);
+ var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
+ wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
+ const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
+ let path = resolvedPos.path;
+ for (let i = path.length - 1; i > 0; i--) {
+ if (path[i].type === schema.nodes.ordered_list) {
+ path[i].attrs.bulletStyle = "indent1";
+ path[i].attrs.mapStyle = (nodeType as any).attrs.mapStyle;
+ break;
+ }
+ }
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+
+ view.dispatch(tx2);
+ });
}
}
@@ -855,7 +876,8 @@ export class TooltipTextMenu {
this.updateFontSizeDropdown("Various");
}
}
- this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks));
+ !this.HackToFixTextSelectionGlitch &&
+ this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); // bcz: what's the purpose of this line? It messes up text selection without the Hack.
this.update_mark_doms();
}
@@ -969,7 +991,7 @@ export class TooltipTextMenu {
});
}
}
- if (!ref_node.isLeaf) {
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
ref_node = ref_node.child(0);
}
return ref_node;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 1bf6e383d..760736501 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,6 +1,6 @@
import React = require("react");
import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
-import { observable, action, computed } from "mobx";
+import { observable, action, computed, runInAction, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import "./ContextMenu.scss";
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -27,6 +27,13 @@ export class ContextMenu extends React.Component {
@observable private _width: number = 0;
@observable private _height: number = 0;
+ @observable private _mouseX: number = -1;
+ @observable private _mouseY: number = -1;
+ @observable private _shouldDisplay: boolean = false;
+ @observable private _mouseDown: boolean = false;
+
+ private _reactionDisposer?: IReactionDisposer;
+
constructor(props: Readonly<{}>) {
super(props);
@@ -34,6 +41,40 @@ export class ContextMenu extends React.Component {
}
@action
+ onPointerDown = (e: PointerEvent) => {
+ this._mouseDown = true;
+ this._mouseX = e.clientX;
+ this._mouseY = e.clientY;
+ }
+ @action
+ onPointerUp = (e: PointerEvent) => {
+ this._mouseDown = false;
+ let curX = e.clientX;
+ let curY = e.clientY;
+ if (this._mouseX !== curX || this._mouseY !== curY) {
+ this._shouldDisplay = false;
+ }
+
+ this._shouldDisplay && (this._display = true);
+ }
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onPointerDown);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ @action
+ componentDidMount = () => {
+ document.addEventListener("pointerdown", this.onPointerDown);
+ document.addEventListener("pointerup", this.onPointerUp);
+
+ this._reactionDisposer = reaction(
+ () => this._shouldDisplay,
+ () => this._shouldDisplay && !this._mouseDown && runInAction(() => this._display = true)
+ );
+ }
+
+ @action
clearItems() {
this._items = [];
}
@@ -83,22 +124,21 @@ export class ContextMenu extends React.Component {
}
@action
- displayMenu(x: number, y: number) {
+ displayMenu = (x: number, y: number) => {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
this._pageX = x;
this._pageY = y;
-
this._searchString = "";
-
- this._display = true;
+ this._shouldDisplay = true;
}
@action
closeMenu = () => {
this.clearItems();
this._display = false;
+ this._shouldDisplay = false;
}
@computed get filteredItems(): (OriginalMenuProps | string[])[] {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 0b7411fca..ac8497bd0 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -24,13 +24,13 @@ $linkGap : 3px;
.documentDecorations-resizer {
pointer-events: auto;
background: $alt-accent;
- opacity: 0.8;
+ opacity: 1;
}
.documentDecorations-radius {
pointer-events: auto;
background: black;
- opacity: 0.8;
+ opacity: 1;
transform: translate(10px, 10px);
grid-row: 4;
}
@@ -92,27 +92,30 @@ $linkGap : 3px;
.title {
background: $alt-accent;
+ opacity: 1;
grid-column-start: 3;
grid-column-end: 4;
pointer-events: auto;
overflow: hidden;
+ text-align: center;
}
}
.documentDecorations-closeButton {
background: $alt-accent;
- opacity: 0.8;
+ opacity: 1;
grid-column-start: 4;
grid-column-end: 6;
pointer-events: all;
text-align: center;
cursor: pointer;
+ padding-right: 10px;
}
.documentDecorations-minimizeButton {
background: $alt-accent;
- opacity: 0.8;
+ opacity: 1;
grid-column-start: 1;
grid-column-end: 3;
pointer-events: all;
@@ -121,6 +124,7 @@ $linkGap : 3px;
position: absolute;
left: 0px;
top: 0px;
+ padding-top: 5px;
width: $MINIMIZED_ICON_SIZE;
height: $MINIMIZED_ICON_SIZE;
}
@@ -219,6 +223,11 @@ $linkGap : 3px;
margin-top: 3px;
}
+.documentdecorations-times {
+ margin-top: 3px;
+ padding-right: 3px;
+}
+
.templating-button,
.docDecs-tagButton {
width: 20px;
@@ -255,4 +264,8 @@ $linkGap : 3px;
input {
margin-right: 10px;
}
-} \ 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); } } \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6616d5d58..203227241 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,7 +1,7 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag } from '@fortawesome/free-solid-svg-icons';
+import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
+import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
@@ -18,7 +18,7 @@ import { CollectionView } from "./collections/CollectionView";
import './DocumentDecorations.scss';
import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
import { LinkMenu } from "./nodes/LinkMenu";
import { TemplateMenu } from "./TemplateMenu";
@@ -26,16 +26,27 @@ import { Template, Templates } from "./Templates";
import React = require("react");
import { RichTextField } from '../../new_fields/RichTextField';
import { LinkManager } from '../util/LinkManager';
-import { ObjectField } from '../../new_fields/ObjectField';
import { MetadataEntryMenu } from './MetadataEntryMenu';
import { ImageBox } from './nodes/ImageBox';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
library.add(faLink);
library.add(faTag);
+library.add(faTimes);
+library.add(faArrowAltCircleDown);
+library.add(faArrowAltCircleUp);
+library.add(faStopCircle);
+library.add(faCheckCircle);
+library.add(faCloudUploadAlt);
+library.add(faSyncAlt);
+library.add(faShare);
+
+const cloud: IconProp = "cloud-upload-alt";
+const fetch: IconProp = "sync-alt";
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
@@ -65,6 +76,53 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable private _opacity = 1;
@observable private _removeIcon = false;
@observable public Interacting = false;
+ @observable private _isMoving = false;
+
+ @observable public pushIcon: IconProp = "arrow-alt-circle-up";
+ @observable public pullIcon: IconProp = "arrow-alt-circle-down";
+ @observable public pullColor: string = "white";
+ @observable public isAnimatingFetch = false;
+ @observable public openHover = false;
+ public pullColorAnimating = false;
+
+ private pullAnimating = false;
+ private pushAnimating = false;
+
+ public startPullOutcome = action((success: boolean) => {
+ if (!this.pullAnimating) {
+ this.pullAnimating = true;
+ this.pullIcon = success ? "check-circle" : "stop-circle";
+ setTimeout(() => runInAction(() => {
+ this.pullIcon = "arrow-alt-circle-down";
+ this.pullAnimating = false;
+ }), 1000);
+ }
+ });
+
+ public startPushOutcome = action((success: boolean) => {
+ if (!this.pushAnimating) {
+ this.pushAnimating = true;
+ this.pushIcon = success ? "check-circle" : "stop-circle";
+ setTimeout(() => runInAction(() => {
+ this.pushIcon = "arrow-alt-circle-up";
+ this.pushAnimating = false;
+ }), 1000);
+ }
+ });
+
+ public setPullState = action((unchanged: boolean) => {
+ this.isAnimatingFetch = false;
+ if (!this.pullColorAnimating) {
+ this.pullColorAnimating = true;
+ this.pullColor = unchanged ? "lawngreen" : "red";
+ setTimeout(this.clearPullColor, 1000);
+ }
+ });
+
+ private clearPullColor = action(() => {
+ this.pullColor = "white";
+ this.pullColorAnimating = false;
+ });
constructor(props: Readonly<{}>) {
super(props);
@@ -142,14 +200,22 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.onBackgroundUp(e);
}
+ @observable _forceUpdate = 0;
+ _lastBox = { x: 0, y: 0, r: 0, b: 0 };
@computed
get Bounds(): { x: number, y: number, b: number, r: number } {
- return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
+ let x = this._forceUpdate;
+ this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
if (documentView.props.renderDepth === 0 ||
Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
return bounds;
}
let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
+ if (transform.TranslateX === 0 && transform.TranslateY === 0) {
+ setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...
+ return this._lastBox;
+ }
+
var [sptX, sptY] = transform.transformPoint(0, 0);
let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
return {
@@ -157,6 +223,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
};
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ return this._lastBox;
}
onBackgroundDown = (e: React.PointerEvent): void => {
@@ -213,10 +280,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
@undoBatch
@action
- onCloseUp = (e: PointerEvent): void => {
+ onCloseUp = async (e: PointerEvent) => {
e.stopPropagation();
if (e.button === 0) {
- SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document));
+ const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+ SelectionManager.SelectedDocuments().map(dv => {
+ recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
+ dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
+ });
SelectionManager.DeselectAll();
document.removeEventListener("pointermove", this.onCloseMove);
document.removeEventListener("pointerup", this.onCloseUp);
@@ -346,9 +417,14 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
document.addEventListener("pointermove", this.onRadiusMove);
document.addEventListener("pointerup", this.onRadiusUp);
}
+ if (!this._isMoving) {
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+ map(d => d.borderRounding = "0%");
+ }
}
onRadiusMove = (e: PointerEvent): void => {
+ this._isMoving = true;
let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
map(d => d.borderRounding = `${Math.min(100, dist)}%`);
@@ -361,6 +437,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
e.preventDefault();
this._isPointerDown = false;
this._resizeUndo && this._resizeUndo.end();
+ this._isMoving = false;
document.removeEventListener("pointermove", this.onRadiusMove);
document.removeEventListener("pointerup", this.onRadiusUp);
}
@@ -523,7 +600,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
+ if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
let doc = PositionDocument(element.props.Document);
@@ -537,12 +614,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
- let fixedAspect = e.ctrlKey || (!BoolCast(proto.ignoreAspect) && nwidth && nheight);
+ let fixedAspect = e.ctrlKey || (!BoolCast(doc.ignoreAspect) && nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
proto.nativeWidth = nwidth = doc.width || 0;
proto.nativeHeight = nheight = doc.height || 0;
}
- if (nwidth > 0 && nheight > 0 && !BoolCast(proto.ignoreAspect)) {
+ if (nwidth > 0 && nheight > 0 && !BoolCast(doc.ignoreAspect)) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
@@ -618,6 +695,74 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
);
}
+ private get targetDoc() {
+ return SelectionManager.SelectedDocuments()[0].props.Document;
+ }
+
+ considerGoogleDocsPush = () => {
+ let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+ if (!canPush) return (null);
+ let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
+ let icon: IconProp = published ? (this.pushIcon as any) : cloud;
+ return (
+ <div className={"linkButtonWrapper"}>
+ <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+ DocumentDecorations.hasPushedHack = false;
+ this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
+ </div>
+ </div>
+ );
+ }
+
+ considerGoogleDocsPull = () => {
+ let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+ let dataDoc = Doc.GetProto(this.targetDoc);
+ if (!canPull || !dataDoc[GoogleRef]) return (null);
+ let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
+ icon = this.openHover ? "share" : icon;
+ let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
+ let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
+ return (
+ <div className={"linkButtonWrapper"}>
+ <div
+ title={title}
+ className="linkButton-linker"
+ style={{
+ backgroundColor: this.pullColor,
+ transition: "0.2s ease all"
+ }}
+ onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
+ onPointerLeave={() => runInAction(() => this.openHover = false)}
+ onClick={e => {
+ if (e.altKey) {
+ e.preventDefault();
+ window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
+ } else {
+ this.clearPullColor();
+ DocumentDecorations.hasPulledHack = false;
+ this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
+ dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+ }
+ }}>
+ <FontAwesomeIcon
+ style={{
+ WebkitAnimation: animation,
+ MozAnimation: animation
+ }}
+ className="documentdecorations-icon"
+ icon={icon}
+ size="sm"
+ />
+ </div>
+ </div>
+ );
+ }
+
+ public static hasPushedHack = false;
+ public static hasPulledHack = false;
+
considerTooltip = () => {
let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
@@ -685,11 +830,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template => {
- let sorted = SelectionManager.ViewsSortedVertically(); // slice().sort((doc1, doc2) => {
- // if (NumCast(doc1.props.Document.y) > NumCast(doc2.props.Document.x)) return 1;
- // if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
- // return 0;
- // });
+ let sorted = SelectionManager.ViewsSortedVertically();
let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
let temps = doc.props.Document.templates;
if (temps instanceof List) {
@@ -743,7 +884,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
{this._edtingTitle ?
<input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
<div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
- <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>X</div>
+ <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
+ </div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -768,6 +911,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
</div>
{this.metadataMenu}
{this.considerEmbed()}
+ {this.considerGoogleDocsPush()}
+ {this.considerGoogleDocsPull()}
{/* {this.considerTooltip()} */}
</div>
</div >
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index c3612fee9..dd5395802 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -3,6 +3,7 @@ import { observer } from 'mobx-react';
import { observable, action, trace } from 'mobx';
import "./EditableView.scss";
import * as Autosuggest from 'react-autosuggest';
+import { undoBatch } from '../util/UndoManager';
export interface EditableProps {
/**
@@ -70,14 +71,12 @@ export class EditableView extends React.Component<EditableProps> {
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Tab") {
e.stopPropagation();
+ this.finalizeEdit(e.currentTarget.value, e.shiftKey);
this.props.OnTab && this.props.OnTab();
} else if (e.key === "Enter") {
e.stopPropagation();
if (!e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value, e.shiftKey)) {
- this._editing = false;
- this.props.isEditingCallback && this.props.isEditingCallback(false);
- }
+ this.finalizeEdit(e.currentTarget.value, e.shiftKey);
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
this._editing = false;
@@ -100,6 +99,14 @@ export class EditableView extends React.Component<EditableProps> {
e.stopPropagation();
}
+ @action
+ private finalizeEdit(value: string, shiftDown: boolean) {
+ if (this.props.SetValue(value, shiftDown)) {
+ this._editing = false;
+ this.props.isEditingCallback && this.props.isEditingCallback(false);
+ }
+ }
+
stopPropagation(e: React.SyntheticEvent) {
e.stopPropagation();
}
@@ -118,7 +125,7 @@ export class EditableView extends React.Component<EditableProps> {
className: "editableView-input",
onKeyDown: this.onKeyDown,
autoFocus: true,
- onBlur: action(() => this._editing = false),
+ onBlur: e => this.finalizeEdit(e.currentTarget.value, false),
onPointerDown: this.stopPropagation,
onClick: this.stopPropagation,
onPointerUp: this.stopPropagation,
@@ -126,9 +133,14 @@ export class EditableView extends React.Component<EditableProps> {
onChange: this.props.autosuggestProps.onChange
}}
/>
- : <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus
- onBlur={action(() => { this._editing = false; this.props.isEditingCallback && this.props.isEditingCallback(false); })} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
- style={{ display: this.props.display, fontSize: this.props.fontSize }} />;
+ : <input className="editableView-input"
+ defaultValue={this.props.GetValue()}
+ onKeyDown={this.onKeyDown}
+ autoFocus={true}
+ onBlur={e => this.finalizeEdit(e.currentTarget.value, false)}
+ onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
+ style={{ display: this.props.display, fontSize: this.props.fontSize }}
+ />;
} else {
if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
return (
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index e773014e3..d0464bd5f 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -105,7 +105,7 @@ export default class KeyManager {
switch (keyname) {
case " ":
- DictationManager.Controls.listen({ tryExecute: true });
+ DictationManager.Controls.listen({ useOverlay: true, tryExecute: true });
stopPropagation = true;
preventDefault = true;
}
@@ -194,6 +194,10 @@ export default class KeyManager {
};
});
+ async printClipboard() {
+ let text: string = await navigator.clipboard.readText();
+ }
+
private ctrl_shift = action((keyname: string) => {
let stopPropagation = true;
let preventDefault = true;
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index d95398f17..5437b26d6 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -21,8 +21,7 @@
width: 8192px;
height: 8192px;
cursor: "crosshair";
- pointer-events: auto;
-
+ pointer-events: all;
}
.inkingCanvas-canSelect,
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 1c221e3df..1cfa8d644 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -165,14 +165,18 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
}
return paths;
}, [] as JSX.Element[]);
- return [<svg className={`inkingCanvas-paths-ink`} key="Pens"
- style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
- {paths.filter(path => path.props.tool !== InkTool.Highlighter)}
- </svg>,
- <svg className={`inkingCanvas-paths-markers`} key="Markers"
- style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
- {paths.filter(path => path.props.tool === InkTool.Highlighter)}
- </svg>];
+ let markerPaths = paths.filter(path => path.props.tool === InkTool.Highlighter);
+ let penPaths = paths.filter(path => path.props.tool !== InkTool.Highlighter);
+ return [!penPaths.length ? (null) :
+ <svg className={`inkingCanvas-paths-ink`} key="Pens"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
+ {penPaths}
+ </svg>,
+ !markerPaths.length ? (null) :
+ <svg className={`inkingCanvas-paths-markers`} key="Markers"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
+ {markerPaths}
+ </svg>];
}
render() {
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index f76abaff3..bc0975c86 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -140,6 +140,8 @@ button:hover {
// font-size: 8px;
// user-select: none;
// }
+ margin-top: -2.55px;
+ margin-left: -2.55px;
}
// add nodes menu. Note that the + button is actually an input label, not an actual button.
@@ -295,4 +297,15 @@ ul#add-options-list {
z-index: 999;
transition: 0.5s all ease;
pointer-events: none;
+}
+
+.webpage-input {
+ display: none;
+ height: 60px;
+ width: 600px;
+ position: absolute;
+
+ .url-input {
+ width: 80%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 72eb956e3..755e5de14 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -2,7 +2,7 @@ import { action, observable, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
-import { Doc } from '../../new_fields/Doc';
+import { Doc, DocListCast } from '../../new_fields/Doc';
import { BoolCast } from '../../new_fields/Types';
import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
import { DragManager } from '../util/DragManager';
@@ -42,13 +42,17 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
return this._textBox && this._textBox.setFontColor(color);
}
-
constructor(props: MainOverlayTextBoxProps) {
super(props);
this._textProxyDiv = React.createRef();
MainOverlayTextBox.Instance = this;
reaction(() => FormattedTextBox.InputBoxOverlay,
(box?: FormattedTextBox) => {
+ const tb = this._textBox;
+ const container = tb && tb.props.ContainingCollectionView;
+ if (tb && container) {
+ tb.rebuildEditor();// this forces the edited text box to completely recreate itself to avoid get out of sync -- e.g., bullet labels are only computed when the dom elements are created
+ }
this._textBox = box;
if (box) {
this.ChromeHeight = box.props.ChromeHeight;
@@ -59,7 +63,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
};
- this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
+ this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight) || box.props.height === "min-content");
}
else {
this.TextDoc = undefined;
@@ -130,21 +134,26 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
render() {
this.TextDoc; this.TextDataDoc;
if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
+ let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
+ let hgtx = FormattedTextBox.InputBoxOverlay.props.Document.height; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
let textRect = this._textTargetDiv.getBoundingClientRect();
let s = this._textXf().Scale;
let location = this._textBottom ? textRect.bottom : textRect.top;
- let hgt = this._textAutoHeight || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
+ let hgt = (this._textBox && this._textBox.props.Document.autoHeight) || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
- <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s},${1 / s})`, width: "auto", height: "0px" }} >
+ <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s})`, width: "auto", height: "0px" }} >
<div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
style={{ width: `${textRect.width * s}px`, height: "0px" }}>
<div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
<FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
Document={FormattedTextBox.InputBoxOverlay.props.Document}
DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
- isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
+ onClick={undefined}
+ ChromeHeight={this.ChromeHeight}
+ isSelected={returnTrue} select={emptyFunction} renderDepth={0}
ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction}
+ pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
</div>
</div>
</div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0b6fe3876..57eb30439 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,29 +1,32 @@
import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowDown, faCloudUploadAlt, faArrowUp, faClone, faCheck, faPlay, faPause, faCaretUp, faLongArrowAltRight, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faPortrait, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt, faCat, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, observable, runInAction, reaction, trace, autorun } from 'mobx';
+import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
import { SketchPicker } from 'react-color';
import Measure from 'react-measure';
import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc';
+import { List } from '../../new_fields/List';
import { Id } from '../../new_fields/FieldSymbols';
import { InkTool } from '../../new_fields/InkField';
-import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
-import { Cast, FieldValue, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
+import { BoolCast, Cast, FieldValue, StrCast } 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 { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
+import { ClientUtils } from '../util/ClientUtils';
+import { DictationManager } from '../util/DictationManager';
import { SetupDrag } from '../util/DragManager';
import { HistoryUtil } from '../util/History';
import { Transform } from '../util/Transform';
-import { UndoManager } from '../util/UndoManager';
+import { UndoManager, undoBatch } from '../util/UndoManager';
import { CollectionBaseView } from './collections/CollectionBaseView';
import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionTreeView } from './collections/CollectionTreeView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
import KeyManager from './GlobalKeyHandler';
@@ -33,21 +36,18 @@ import { MainOverlayTextBox } from './MainOverlayTextBox';
import { DocumentView } from './nodes/DocumentView';
import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
-import { PresentationView } from './presentationview/PresentationView';
import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
-import { CollectionTreeView } from './collections/CollectionTreeView';
-import { ClientUtils } from '../util/ClientUtils';
import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
//import { DocumentManager } from '../util/DocumentManager';
-import { DictationManager } from '../util/DictationManager';
import { Recommendations } from './Recommendations';
+import PresModeMenu from './presentationview/PresentationModeMenu';
+import { PresBox } from './nodes/PresBox';
@observer
export class MainView extends React.Component {
public static Instance: MainView;
@observable addMenuToggle = React.createRef<HTMLInputElement>();
- @observable private _workspacesShown: boolean = false;
@observable public pwidth: number = 0;
@observable public pheight: number = 0;
@@ -84,9 +84,6 @@ export class MainView extends React.Component {
public isPointerDown = false;
private set mainContainer(doc: Opt<Doc>) {
if (doc) {
- if (!("presentationView" in doc)) {
- doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
- }
CurrentUserUtils.UserDocument.activeWorkspace = doc;
}
}
@@ -151,6 +148,7 @@ export class MainView extends React.Component {
componentWillUnMount() {
window.removeEventListener("keydown", KeyManager.Instance.handle);
+ //close presentation
window.removeEventListener("pointerdown", this.globalPointerDown);
window.removeEventListener("pointerup", this.globalPointerUp);
}
@@ -176,7 +174,7 @@ export class MainView extends React.Component {
library.add(faCat);
library.add(faFilePdf);
library.add(faObjectGroup);
- library.add(faTable);
+ library.add(faTv);
library.add(faGlobeAsia);
library.add(faUndoAlt);
library.add(faRedoAlt);
@@ -314,7 +312,6 @@ export class MainView extends React.Component {
@computed get dockingContent() {
let flyoutWidth = this.flyoutWidth;
let mainCont = this.mainContainer;
- let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, listSpec(Doc))) : undefined;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
<div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
@@ -323,6 +320,8 @@ export class MainView extends React.Component {
DataDoc={undefined}
addDocument={undefined}
addDocTab={emptyFunction}
+ pinToPres={emptyFunction}
+ onClick={undefined}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
@@ -330,7 +329,6 @@ export class MainView extends React.Component {
PanelHeight={this.getPHeight}
renderDepth={0}
backgroundColor={returnEmptyString}
- selectOnLoad={false}
focus={emptyFunction}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
@@ -339,7 +337,6 @@ export class MainView extends React.Component {
zoomToScale={emptyFunction}
getScale={returnOne}
/>}
- {castRes ? <PresentationView Documents={castRes} key="presentation" /> : null}
</div>
}
</Measure>;
@@ -386,13 +383,14 @@ export class MainView extends React.Component {
DataDoc={undefined}
addDocument={undefined}
addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
removeDocument={undefined}
+ onClick={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
PanelWidth={this.flyoutWidthFunc}
PanelHeight={this.getPHeight}
renderDepth={0}
- selectOnLoad={false}
focus={emptyFunction}
backgroundColor={returnEmptyString}
parentActive={returnTrue}
@@ -407,7 +405,7 @@ export class MainView extends React.Component {
get mainContent() {
let sidebar = CurrentUserUtils.UserDocument.sidebar;
if (!(sidebar instanceof Doc)) return (null);
- return <div>
+ return <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
<div className="mainView-libraryHandle"
style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
onPointerDown={this.onPointerDown}>
@@ -436,56 +434,41 @@ export class MainView extends React.Component {
}
}
-
@observable private _colorPickerDisplay = false;
/* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
nodesMenu() {
let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- // let addDockingNode = action(() => Docs.Create.StandardCollectionDockingDocument([{ doc: addColNode(), initialWidth: 200 }], { width: 200, height: 200, title: "a nested docking freeform collection" }));
- let addSchemaNode = action(() => Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], [], { width: 200, height: 200, title: "a schema collection" }));
- //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
- // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
- let addTreeNode = action(() => CurrentUserUtils.UserDocument);
+ let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
+ let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
+ let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" }));
let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
- let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
- let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
+ // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
+ // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
[React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
+ [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode],
+ [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
[React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
- // [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode],
- [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode],
- [React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher]
+ [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
+ [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
+ //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
];
if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
- const setWriteMode = (mode: DocServer.WriteMode) => {
- console.log(DocServer.WriteMode[mode]);
- const mode1 = mode;
- const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
- DocServer.setFieldWriteMode("x", mode1);
- DocServer.setFieldWriteMode("y", mode1);
- DocServer.setFieldWriteMode("width", mode1);
- DocServer.setFieldWriteMode("height", mode1);
-
- DocServer.setFieldWriteMode("panX", mode2);
- DocServer.setFieldWriteMode("panY", mode2);
- DocServer.setFieldWriteMode("scale", mode2);
- DocServer.setFieldWriteMode("viewType", mode2);
- };
return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >
+
<input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
- <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Add Node"><p>+</p></label>
+ <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Close Menu"><p>+</p></label>
<div id="add-options-content">
<ul id="add-options-list">
<li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li>
- <li key="presentation"><button className="add-button round-button" title="Open Presentation View" onClick={() => PresentationView.Instance.toggle(undefined)}><FontAwesomeIcon icon="table" size="sm" /></button></li>
<li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
<li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
{btns.map(btn =>
@@ -494,13 +477,6 @@ export class MainView extends React.Component {
<FontAwesomeIcon icon={btn[1]} size="sm" />
</button>
</div></li>)}
- <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
- {ClientUtils.RELEASE ? [] : [
- <li key="test"><button className="add-button round-button" title="Default" onClick={() => setWriteMode(DocServer.WriteMode.Default)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
- <li key="test1"><button className="add-button round-button" title="Playground" onClick={() => setWriteMode(DocServer.WriteMode.Playground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
- <li key="test2"><button className="add-button round-button" title="Live Playground" onClick={() => setWriteMode(DocServer.WriteMode.LivePlayground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
- <li key="test3"><button className="add-button round-button" title="Live Readonly" onClick={() => setWriteMode(DocServer.WriteMode.LiveReadonly)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
- ]}
<li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
<div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
<SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
@@ -543,9 +519,8 @@ export class MainView extends React.Component {
}
@observable isSearchVisible = false;
- @action
+ @action.bound
toggleSearch = () => {
- // console.log("search toggling")
this.isSearchVisible = !this.isSearchVisible;
}
@@ -574,12 +549,21 @@ export class MainView extends React.Component {
);
}
+ @computed get miniPresentation() {
+ let next = () => PresBox.CurrentPresentation.next();
+ let back = () => PresBox.CurrentPresentation.back();
+ let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
+ let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document); });
+ return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
+ }
+
render() {
return (
<div id="main-div">
{this.dictationOverlay}
<DocumentDecorations />
{this.mainContent}
+ {this.miniPresentation}
<PreviewCursor />
<ContextMenu />
<Recommendations />
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index bcfc9a82d..7da55fd1c 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -1,6 +1,22 @@
.metadataEntry-outerDiv {
display: flex;
- width: 300px;
+ width: 310px;
+ flex-direction: column;
+
+ input[type=checkbox] {
+ margin-left: 5px;
+ }
+}
+
+.metadataEntry-keys {
+ max-height: 80;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+}
+.metadataEntry-inputArea {
+ display:flex;
+ flex-direction: row;
}
.react-autosuggest__container {
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 36c240dd8..f1b101b8e 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -1,11 +1,12 @@
import * as React from 'react';
import "./MetadataEntryMenu.scss";
import { observer } from 'mobx-react';
-import { observable, action, runInAction, trace } from 'mobx';
+import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc, Field } from '../../new_fields/Doc';
+import { Doc, Field, DocListCast, DocListCastAsync } from '../../new_fields/Doc';
import * as Autosuggest from 'react-autosuggest';
import { undoBatch } from '../util/UndoManager';
+import { emptyFunction } from '../../Utils';
export type DocLike = Doc | Doc[] | Promise<Doc> | Promise<Doc[]>;
export interface MetadataEntryProps {
@@ -19,6 +20,9 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
@observable private _currentKey: string = "";
@observable private _currentValue: string = "";
@observable private suggestions: string[] = [];
+ private _addChildren: boolean = false;
+ @observable _allSuggestions: string[] = [];
+ _suggestionDispser: IReactionDisposer | undefined;
private userModified = false;
private autosuggestRef = React.createRef<Autosuggest>();
@@ -82,16 +86,27 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
e.stopPropagation();
const script = KeyValueBox.CompileKVPScript(this._currentValue);
if (!script) return;
+
let doc = this.props.docs;
if (typeof doc === "function") {
doc = doc();
}
doc = await doc;
+
let success: boolean;
if (doc instanceof Doc) {
success = KeyValueBox.ApplyKVPScript(doc, this._currentKey, script);
} else {
- success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script));
+ let childSuccess = true;
+ if (this._addChildren) {
+ for (let document of doc) {
+ let collectionChildren = await DocListCastAsync(document.data);
+ if (collectionChildren) {
+ childSuccess = collectionChildren.every(c => KeyValueBox.ApplyKVPScript(c, this._currentKey, script));
+ }
+ }
+ }
+ success = doc.every(d => KeyValueBox.ApplyKVPScript(d, this._currentKey, script)) && childSuccess;
}
if (!success) {
if (this.props.onError) {
@@ -140,35 +155,67 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
getSuggestionValue = (suggestion: string) => suggestion;
renderSuggestion = (suggestion: string) => {
- return <p>{suggestion}</p>;
+ return (null);
}
+ componentDidMount() {
- onSuggestionFetch = async ({ value }: { value: string }) => {
- const sugg = await this.getKeySuggestions(value);
- runInAction(() => {
- this.suggestions = sugg;
- });
+ this._suggestionDispser = reaction(() => this._currentKey,
+ () => this.getKeySuggestions(this._currentKey).then(action((s: string[]) => this._allSuggestions = s)),
+ { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._suggestionDispser && this._suggestionDispser();
}
- @action
- onSuggestionClear = () => {
- this.suggestions = [];
+ onClick = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._addChildren = !this._addChildren;
+ }
+
+ private get considerChildOptions() {
+ let docSource = this.props.docs;
+ if (typeof docSource === "function") {
+ docSource = docSource();
+ }
+ docSource = docSource as Doc[] | Doc;
+ if (docSource instanceof Doc) {
+ if (docSource.viewType === undefined) {
+ return (null);
+ }
+ } else if (Array.isArray(docSource)) {
+ if (!docSource.every(doc => doc.viewType !== undefined)) {
+ return null;
+ }
+ }
+ return (
+ <div style={{ display: "flex" }}>
+ Children:
+ <input type="checkbox" onChange={this.onClick} ></input>
+ </div>
+ );
}
render() {
return (
<div className="metadataEntry-outerDiv">
- Key:
- <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
- getSuggestionValue={this.getSuggestionValue}
- suggestions={this.suggestions}
- alwaysRenderSuggestions
- renderSuggestion={this.renderSuggestion}
- onSuggestionsFetchRequested={this.onSuggestionFetch}
- onSuggestionsClearRequested={this.onSuggestionClear}
- ref={this.autosuggestRef} />
- Value:
- <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ <div className="metadataEntry-inputArea">
+ Key:
+ <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
+ getSuggestionValue={this.getSuggestionValue}
+ suggestions={[]}
+ alwaysRenderSuggestions={false}
+ renderSuggestion={this.renderSuggestion}
+ onSuggestionsFetchRequested={emptyFunction}
+ onSuggestionsClearRequested={emptyFunction}
+ ref={this.autosuggestRef} />
+ Value:
+ <input className="metadataEntry-input" value={this._currentValue} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ {this.considerChildOptions}
+ </div>
+ <div className="metadataEntry-keys" >
+ <ul>
+ {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)}
+ </ul>
+ </div>
</div>
);
}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 2f2579057..538614089 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,9 +1,15 @@
import * as React from "react";
import { observer } from "mobx-react";
import { observable, action } from "mobx";
-import { Utils } from "../../Utils";
+import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString } from "../../Utils";
import './OverlayView.scss';
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { DocListCast, Doc } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
+import { DocumentView } from "./nodes/DocumentView";
+import { Transform } from "../util/Transform";
+import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView";
export type OverlayDisposer = () => void;
@@ -134,8 +140,28 @@ export class OverlayView extends React.Component {
render() {
return (
- <div>
+ <div className="overlayView">
{this._elements}
+ {CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => (
+ <CollectionFreeFormDocumentView key={d[Id]}
+ Document={d}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={returnOne}
+ PanelWidth={returnOne}
+ PanelHeight={returnOne}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={1}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ addDocTab={emptyFunction}
+ pinToPres={emptyFunction}
+ ContainingCollectionView={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne} />))}
</div>
);
}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index e7a5475ed..1329dc02c 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -1,13 +1,20 @@
-import { action, observable } from 'mobx';
+import { action, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
import "./PreviewCursor.scss";
+import { Docs } from '../documents/Documents';
+// import { Transform } from 'prosemirror-transform';
+import { Doc } from '../../new_fields/Doc';
+import { Transform } from "../util/Transform";
@observer
export class PreviewCursor extends React.Component<{}> {
private _prompt = React.createRef<HTMLDivElement>();
static _onKeyPress?: (e: KeyboardEvent) => void;
+ static _getTransform: () => Transform;
+ static _addLiveTextDoc: (doc: Doc) => void;
+ static _addDocument: (doc: Doc, allowDuplicates: false) => boolean;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
//when focus is lost, this will remove the preview cursor
@@ -20,12 +27,67 @@ export class PreviewCursor extends React.Component<{}> {
document.addEventListener("keydown", this.onKeyPress);
document.addEventListener("paste", this.paste);
}
+
paste = (e: ClipboardEvent) => {
- console.log(e.clipboardData);
- if (e.clipboardData) {
- console.log(e.clipboardData.getData("text/html"));
- console.log(e.clipboardData.getData("text/csv"));
- console.log(e.clipboardData.getData("text/plain"));
+ if (PreviewCursor.Visible) {
+ if (e.clipboardData) {
+ let newPoint = PreviewCursor._getTransform().transformPoint(PreviewCursor._clickPoint[0], PreviewCursor._clickPoint[1]);
+ runInAction(() => { PreviewCursor.Visible = false; });
+
+
+ if (e.clipboardData.getData("text/plain") !== "") {
+
+ // tests for youtube and makes video document
+ if (e.clipboardData.getData("text/plain").indexOf("www.youtube.com/watch") !== -1) {
+ const url = e.clipboardData.getData("text/plain").replace("youtube.com/watch?v=", "youtube.com/embed/");
+ PreviewCursor._addDocument(Docs.Create.VideoDocument(url, {
+ title: url, width: 400, height: 315,
+ nativeWidth: 600, nativeHeight: 472.5,
+ x: newPoint[0], y: newPoint[1]
+ }), false);
+ return;
+ }
+
+ // tests for URL and makes web document
+ let re: any = /^https?:\/\//g;
+ if (re.test(e.clipboardData.getData("text/plain"))) {
+ const url = e.clipboardData.getData("text/plain");
+ PreviewCursor._addDocument(Docs.Create.WebDocument(url, {
+ title: url, width: 300, height: 300,
+ // nativeWidth: 300, nativeHeight: 472.5,
+ x: newPoint[0], y: newPoint[1]
+ }), false);
+ return;
+ }
+
+ // creates text document
+ let newBox = Docs.Create.TextDocument({
+ width: 200, height: 100,
+ x: newPoint[0],
+ y: newPoint[1],
+ title: "-pasted text-"
+ });
+
+ newBox.proto!.autoHeight = true;
+ PreviewCursor._addLiveTextDoc(newBox);
+ return;
+ }
+ //pasting in images
+ if (e.clipboardData.getData("text/html") !== "" && e.clipboardData.getData("text/html").includes("<img src=")) {
+ let re: any = /<img src="(.*?)"/g;
+ let arr: any[] = re.exec(e.clipboardData.getData("text/html"));
+
+ let img: Doc = Docs.Create.ImageDocument(
+ arr[1], {
+ width: 300, title: arr[1],
+ x: newPoint[0],
+ y: newPoint[1],
+ });
+ PreviewCursor._addDocument(img, false);
+ return;
+ }
+
+ }
}
}
@@ -49,9 +111,16 @@ export class PreviewCursor extends React.Component<{}> {
}
}
@action
- public static Show(x: number, y: number, onKeyPress: (e: KeyboardEvent) => void) {
+ public static Show(x: number, y: number,
+ onKeyPress: (e: KeyboardEvent) => void,
+ addLiveText: (doc: Doc) => void,
+ getTransform: () => Transform,
+ addDocument: (doc: Doc, allowDuplicates: false) => boolean) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
+ this._addLiveTextDoc = addLiveText;
+ this._getTransform = getTransform;
+ this._addDocument = addDocument;
setTimeout(action(() => this.Visible = true), (1));
}
render() {
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index d073945e5..2b862a81e 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -5,7 +5,11 @@ import { observable, action } from "mobx";
import "./ScriptBox.scss";
import { OverlayView } from "./OverlayView";
import { DocumentIconContainer } from "./nodes/DocumentIcon";
-import { Opt } from "../../new_fields/Doc";
+import { Opt, Doc } from "../../new_fields/Doc";
+import { emptyFunction } from "../../Utils";
+import { ScriptCast } from "../../new_fields/Types";
+import { CompileScript } from "../util/Scripting";
+import { ScriptField } from "../../new_fields/ScriptField";
export interface ScriptBoxProps {
onSave: (text: string, onError: (error: string) => void) => void;
@@ -62,4 +66,26 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
</div>
);
}
-} \ No newline at end of file
+ public static EditClickScript(doc: Doc, fieldKey: string) {
+ let overlayDisposer: () => void = emptyFunction;
+ const script = ScriptCast(doc[fieldKey]);
+ let originalText: string | undefined = undefined;
+ if (script) originalText = script.script.originalScript;
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
+ let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+ const script = CompileScript(text, {
+ params: { this: Doc.name },
+ typecheck: false,
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer()
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
+ doc[fieldKey] = new ScriptField(script);
+ overlayDisposer();
+ }} showDocumentIcons />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${doc.title || ""} OnClick` });
+ }
+}
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
deleted file mode 100644
index 17f99fa05..000000000
--- a/src/client/views/SearchBox.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faObjectGroup, faSearch } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import * as rp from 'request-promise';
-import { Doc } from '../../new_fields/Doc';
-import { Id } from '../../new_fields/FieldSymbols';
-import { NumCast } from '../../new_fields/Types';
-import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
-import { SetupDrag } from '../util/DragManager';
-import { SearchItem } from './search/SearchItem';
-import "./SearchBox.scss";
-import { Utils } from '../../Utils';
-
-library.add(faSearch);
-library.add(faObjectGroup);
-
-@observer
-export class SearchBox extends React.Component {
- @observable
- searchString: string = "";
-
- @observable private _open: boolean = false;
- @observable private _resultsOpen: boolean = false;
-
- @observable
- private _results: Doc[] = [];
-
- @action.bound
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- this.searchString = e.target.value;
- }
-
- @action
- submitSearch = async () => {
- let query = this.searchString;
- //gets json result into a list of documents that can be used
- const results = await this.getResults(query);
-
- runInAction(() => {
- this._resultsOpen = true;
- this._results = results;
- });
- }
-
- @action
- public getResults = async (query: string) => {
- let response = await rp.get(Utils.prepend('/search'), {
- qs: {
- query
- }
- });
- let res: string[] = JSON.parse(response);
- const fields = await DocServer.GetRefFields(res);
- const docs: Doc[] = [];
- for (const id of res) {
- const field = fields[id];
- if (field instanceof Doc) {
- docs.push(field);
- }
- }
- return docs;
- }
-
- @action
- handleClickFilter = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (className !== "filter-button" && className !== "filter-form") {
- this._open = false;
- }
-
- }
-
- @action
- handleClickResults = (e: Event): void => {
- var className = (e.target as any).className;
- var id = (e.target as any).id;
- if (id !== "result") {
- this._resultsOpen = false;
- this._results = [];
- }
-
- }
-
- componentWillMount() {
- document.addEventListener('mousedown', this.handleClickFilter, false);
- document.addEventListener('mousedown', this.handleClickResults, false);
- }
-
- componentWillUnmount() {
- document.removeEventListener('mousedown', this.handleClickFilter, false);
- document.removeEventListener('mousedown', this.handleClickResults, false);
- }
-
- @action
- toggleFilterDisplay = () => {
- this._open = !this._open;
- }
-
- enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === "Enter") {
- this.submitSearch();
- }
- }
-
- collectionRef = React.createRef<HTMLSpanElement>();
- startDragCollection = async () => {
- const results = await this.getResults(this.searchString);
- const docs = results.map(doc => {
- const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
- if (isProto) {
- return Doc.MakeDelegate(doc);
- } else {
- return Doc.MakeAlias(doc);
- }
- });
- let x = 0;
- let y = 0;
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
- const size = 200;
- const aspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1);
- if (aspect > 1) {
- doc.height = size;
- doc.width = size / aspect;
- } else if (aspect > 0) {
- doc.width = size;
- doc.height = size * aspect;
- } else {
- doc.width = size;
- doc.height = size;
- }
- doc.zoomBasis = 1;
- x += 250;
- if (x > 1000) {
- x = 0;
- y += 300;
- }
- }
- return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this.searchString}"` });
- }
-
- // Useful queries:
- // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
- // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
- render() {
- return (
- <div>
- <div className="searchBox-container">
- <div className="searchBox-bar">
- <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
- <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
- </span>
- <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
- className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
- style={{ width: this._resultsOpen ? "500px" : undefined }} />
- {/* <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button> */}
- {/* <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" /> */}
- </div>
- {this._resultsOpen ? (
- <div className="searchBox-results">
- {this._results.map(result => <SearchItem doc={result} key={result[Id]} highlighting={[]} />)}
- </div>
- ) : null}
- </div>
- {this._open ? (
- <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
- <div className="filter-form" id="header">Filter Search Results</div>
- <div className="filter-form" id="option">
- filter by collection, key, type of node
- </div>
-
- </div>
- ) : null}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index cad87ebcc..b6ed6aaa0 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -11,6 +11,7 @@ import { SelectionManager } from '../../util/SelectionManager';
import { ContextMenu } from '../ContextMenu';
import { FieldViewProps } from '../nodes/FieldView';
import './CollectionBaseView.scss';
+import { DateField } from '../../../new_fields/DateField';
export enum CollectionViewType {
Invalid,
@@ -83,7 +84,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
active = (): boolean => {
var isSelected = this.props.isSelected();
- return isSelected || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary);
+ return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary);
}
//TODO should this be observable?
@@ -113,6 +114,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
} else {
Doc.GetProto(targetDataDoc)[targetField] = new List([doc]);
}
+ Doc.GetProto(doc).lastOpened = new DateField;
return true;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 77b698a07..95f94875c 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -28,6 +28,9 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { Docs } from '../../documents/Documents';
+import { DateField } from '../../../new_fields/DateField';
+import { List } from '../../../new_fields/List';
+import { DocumentType } from '../../documents/DocumentTypes';
library.add(faFile);
@observer
@@ -161,6 +164,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this.stateChanged();
}
+ public Has = (document: Doc) => {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (!docs) {
+ return false;
+ }
+ return docs.includes(document);
+ }
+
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@@ -204,6 +215,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
@action
public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
+ Doc.GetProto(document).lastOpened = new DateField;
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
@@ -389,7 +401,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
const stack = tab.contentItem.parent;
// shifts the focus to this tab when another tab is dragged over it
tab.element[0].onmouseenter = (e: any) => {
- if (!this._isPointerDown) return;
+ if (!this._isPointerDown || !SelectionManager.GetIsDragging()) return;
var activeContentItem = tab.header.parent.getActiveContentItem();
if (tab.contentItem !== activeContentItem) {
tab.header.parent.setActiveContentItem(tab.contentItem);
@@ -533,6 +545,27 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}));
}
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ const data = Cast(curPres.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ curPres.data = new List([doc]);
+ }
+ if (!DocumentManager.Instance.getDocumentView(curPres)) {
+ this.addDocTab(curPres, undefined, "onRight");
+ }
+ }
+ }
+
componentDidMount() {
this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
this.props.glContainer.on("tab", this.onActiveContentItemChanged);
@@ -552,11 +585,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
- panelWidth = () => Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
- panelHeight = () => Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight)));
+ panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
+ panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), NumCast(this._document!.nativeHeight, this._panelHeight)));
- nativeWidth = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeWidth, this._panelWidth) : 0;
- nativeHeight = () => !BoolCast(this._document!.ignoreAspect) ? NumCast(this._document!.nativeHeight, this._panelHeight) : 0;
+ nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
+ nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
contentScaling = () => {
const nativeH = this.nativeHeight();
@@ -567,7 +600,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
ScreenToLocalTransform = () => {
- if (this._mainCont && this._mainCont!.children) {
+ if (this._mainCont && this._mainCont.children) {
let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.children[0].firstChild as HTMLElement);
scale = Utils.GetScreenTransform(this._mainCont).scale;
return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / this.contentScaling() / scale);
@@ -581,6 +614,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
MainView.Instance.openWorkspace(doc);
} else if (location === "onRight") {
CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
+ } else if (location === "close") {
+ CollectionDockingView.Instance.CloseRightSplit(doc);
} else {
CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
}
@@ -601,12 +636,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.ScreenToLocalTransform}
renderDepth={0}
- selectOnLoad={false}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
backgroundColor={returnEmptyString}
addDocTab={this.addDocTab}
+ pinToPres={this.PinDoc}
ContainingCollectionView={undefined}
zoomToScale={emptyFunction}
getScale={returnOne} />;
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 7e3061354..9c26a08f0 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -40,6 +40,7 @@ export interface CellProps {
fieldKey: string;
renderDepth: number;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
isFocused: boolean;
changeFocusedCellByIndex: (row: number, col: number) => void;
@@ -152,7 +153,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
isSelected: returnFalse,
select: emptyFunction,
renderDepth: this.props.renderDepth + 1,
- selectOnLoad: false,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
active: returnFalse,
@@ -160,6 +160,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
PanelHeight: returnZero,
PanelWidth: returnZero,
addDocTab: this.props.addDocTab,
+ pinToPres: this.props.pinToPres,
ContentScaling: returnOne
};
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 01744fb34..e0cedc210 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -22,30 +22,6 @@
overflow: scroll;
}
- .collectionSchemaView-previewRegion {
- position: relative;
- background: $light-color;
- height: 100%;
-
- .collectionSchemaView-previewDoc {
- height: 100%;
- width: 100%;
- position: absolute;
- }
-
- .collectionSchemaView-input {
- position: absolute;
- max-width: 150px;
- width: 100%;
- bottom: 0px;
- }
-
- .documentView-node:first-child {
- position: relative;
- background: $light-color;
- }
- }
-
.collectionSchemaView-dividerDragger {
position: relative;
height: 100%;
@@ -62,6 +38,30 @@
}
}
+.collectionSchemaView-previewRegion {
+ position: relative;
+ background: $light-color;
+ height: auto !important;
+
+ .collectionSchemaView-previewDoc {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ }
+
+ .collectionSchemaView-input {
+ position: absolute;
+ max-width: 150px;
+ width: 100%;
+ bottom: 0px;
+ }
+
+ .documentView-node:first-child {
+ position: relative;
+ background: $light-color;
+ }
+}
+
.ReactTable {
width: 100%;
background: white;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 897796174..9d83aa6c1 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -30,7 +30,7 @@ import { undoBatch } from "../../util/UndoManager";
import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells";
import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
-import { ComputedField } from "../../../new_fields/ScriptField";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
@@ -171,6 +171,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
active={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
setPreviewScript={this.setPreviewScript}
previewScript={this.previewScript}
/>
@@ -200,6 +201,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
active={this.props.active}
onDrop={this.onDrop}
addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
isSelected={this.props.isSelected}
isFocused={this.isFocused}
setFocused={this.setFocused}
@@ -251,6 +253,7 @@ export interface SchemaTableProps {
active: () => boolean;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
isSelected: () => boolean;
isFocused: (document: Doc) => boolean;
setFocused: (document: Doc) => void;
@@ -377,6 +380,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
fieldKey: this.props.fieldKey,
renderDepth: this.props.renderDepth,
addDocTab: this.props.addDocTab,
+ pinToPres: this.props.pinToPres,
moveDocument: this.props.moveDocument,
setIsEditing: this.setCellIsEditing,
isEditable: isEditable,
@@ -899,6 +903,7 @@ interface CollectionSchemaPreviewProps {
height: () => number;
showOverlays?: (doc: Doc) => { title?: string, caption?: string };
CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
+ onClick?: ScriptField;
getTransform: () => Transform;
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
@@ -906,6 +911,7 @@ interface CollectionSchemaPreviewProps {
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
setPreviewScript: (script: string) => void;
previewScript?: string;
}
@@ -988,23 +994,24 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
DataDoc={this.props.DataDocument}
Document={this.props.Document}
fitToBox={this.props.fitToBox}
- renderDepth={this.props.renderDepth + 1}
- selectOnLoad={false}
+ onClick={this.props.onClick}
showOverlays={this.props.showOverlays}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
+ whenActiveChanged={this.props.whenActiveChanged}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ parentActive={this.props.active}
ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
ContentScaling={this.contentScaling}
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
- ContainingCollectionView={this.props.CollectionView}
focus={emptyFunction}
backgroundColor={returnEmptyString}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
bringToFront={emptyFunction}
- addDocTab={this.props.addDocTab}
zoomToScale={emptyFunction}
getScale={returnOne}
/>
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 271ad2d58..01d4ea2b6 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,10 +1,15 @@
@import "../globalCssVariables";
-.collectionStackingView {
+.collectionMasonryView {
+ display:inline;
+}
+.collectionStackingView{
+ display: flex;
+}
+.collectionStackingView, .collectionMasonryView{
height: 100%;
width: 100%;
position: absolute;
- display: flex;
top: 0;
overflow-y: auto;
flex-wrap: wrap;
@@ -31,14 +36,20 @@
.collectionStackingView-masonrySingle,
.collectionStackingView-masonryGrid {
width: 100%;
- height: 100%;
- position: absolute;
display: grid;
top: 0;
left: 0;
- width: 100%;
+ }
+ .collectionStackingView-masonrySingle {
+ height: 100%;
position: absolute;
}
+ .collectionStackingView-masonryGrid {
+ margin: auto;
+ height: max-content;
+ position: relative;
+ grid-auto-rows: 0px;
+ }
.collectionStackingView-masonrySingle {
width: 100%;
@@ -80,12 +91,17 @@
height: 100%;
margin: auto;
}
+
+ .collectionStackingView-masonrySection {
+ margin: auto;
+ }
.collectionStackingView-sectionHeader {
text-align: center;
margin-left: 2px;
margin-right: 2px;
margin-top: 10px;
+ background: gray;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
.editableView-input {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index b87be5d68..97b31bf2a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -9,9 +9,9 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction } from "../../../Utils";
-import { DocumentType } from "../../documents/Documents";
+import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types";
+import { emptyFunction, Utils, numberRange } from "../../../Utils";
+import { DocumentType } from "../../documents/DocumentTypes";
import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
@@ -20,24 +20,36 @@ import { CollectionSchemaPreview } from "./CollectionSchemaView";
import "./CollectionStackingView.scss";
import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
import { CollectionSubView } from "./CollectionSubView";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ScriptBox } from "../ScriptBox";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
+ _childLayoutDisposer?: IReactionDisposer;
_sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
@observable private cursor: CursorProperty = "grab";
- get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
+ @observable _scroll = 0; // used to force the document decoration to update when scrolling
+ @computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
+ @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
+ @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
@computed get xMargin() { return NumCast(this.props.Document.xMargin, 2 * this.gridGap); }
@computed get yMargin() { return NumCast(this.props.Document.yMargin, 2 * this.gridGap); }
@computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
- @computed get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
- @computed get columnWidth() { return this.singleColumn ? (this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin) : Math.min(this.props.PanelWidth() - 2 * this.xMargin, NumCast(this.props.Document.columnWidth, 250)); }
- @computed get filteredChildren() { return this.childDocs.filter(d => !d.isMinimized); }
- @computed get sectionFilter() { return this.singleColumn ? StrCast(this.props.Document.sectionFilter) : ""; }
+ @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
+ @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
+ @computed get showAddAGroup() { return (this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); }
+ @computed get columnWidth() {
+ return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
+ this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
+ }
+
+ childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
get layoutDoc() {
// if this document's layout field contains a document (ie, a rendering template), then we will use that
@@ -45,14 +57,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
}
-
get Sections() {
- if (!this.sectionFilter) return new Map<SchemaHeaderField, Doc[]>();
+ if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
if (this.sectionHeaders === undefined) {
- this.props.Document.sectionHeaders = new List<SchemaHeaderField>();
+ setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0);
+ return new Map<SchemaHeaderField, Doc[]>();
}
- const sectionHeaders = this.sectionHeaders!;
+ const sectionHeaders = this.sectionHeaders;
let fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
this.filteredChildren.map(d => {
let sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
@@ -75,18 +87,26 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
componentDidMount() {
- // is there any reason this needs to exist? -syip
- this._heightDisposer = reaction(() => [this.props.Document.autoHeight, this.yMargin, this.props.Document[WidthSym](), this.gridGap, this.columnWidth, this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized])],
- () => {
- if (this.singleColumn && BoolCast(this.props.Document.autoHeight)) {
- let hgt = this.Sections.size * 50 + this.filteredChildren.reduce((height, d, i) => {
- let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
- return height + this.getDocHeight(pair.layout) + (i === this.filteredChildren.length - 1 ? this.yMargin : this.gridGap);
- }, this.yMargin);
- (this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc)
- .height = hgt * (this.props as any).ContentScaling();
- }
- }, { fireImmediately: true });
+ this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
+ async (args) => args[1] instanceof Doc &&
+ this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined)));
+
+ // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
+ this._heightDisposer = reaction(() => {
+ if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) {
+ let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
+ return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght,
+ (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)
+ ), 0);
+ }
+ return -1;
+ },
+ (hgt: number) => {
+ let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
+ doc && (doc.height = hgt);
+ },
+ { fireImmediately: true }
+ );
// reset section headers when a new filter is inputted
this._sectionFilterDisposer = reaction(
@@ -95,6 +115,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
}
componentWillUnmount() {
+ this._childLayoutDisposer && this._childLayoutDisposer();
this._heightDisposer && this._heightDisposer();
this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@@ -109,9 +130,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
overlays = (doc: Doc) => {
- return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: "title", caption: "caption" } : {};
+ return doc.type === DocumentType.IMG || doc.type === DocumentType.VID ? { title: StrCast(this.props.Document.showTitles), caption: StrCast(this.props.Document.showCaptions) } : {};
}
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : ScriptCast(this.Document.onChildClick); }
+
getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
let height = () => this.getDocHeight(layoutDoc);
let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
@@ -120,7 +144,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
DataDocument={dataDoc}
showOverlays={this.overlays}
renderDepth={this.props.renderDepth}
- fitToBox={true}
+ fitToBox={this.props.fitToBox}
+ onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler}
width={width}
height={height}
getTransform={finalDxf}
@@ -131,16 +156,19 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
active={this.props.active}
whenActiveChanged={this.props.whenActiveChanged}
addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
setPreviewScript={emptyFunction}
previewScript={undefined}>
</CollectionSchemaPreview>;
}
- getDocHeight(d: Doc, columnScale: number = 1) {
+ getDocHeight(d?: Doc) {
+ if (!d) return 0;
let nw = NumCast(d.nativeWidth);
let nh = NumCast(d.nativeHeight);
- if (!BoolCast(d.ignoreAspect) && nw && nh) {
+ if (!d.ignoreAspect && nw && nh) {
let aspect = nw && nh ? nh / nw : 1;
- let wid = Math.min(d[WidthSym](), this.columnWidth / columnScale);
+ let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+ if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid);
return wid * aspect;
}
return d[HeightSym]();
@@ -226,14 +254,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
});
}
headings = () => Array.from(this.Sections.keys());
- section = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
let key = this.sectionFilter;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
type = types[0];
}
- let cols = () => this.singleColumn ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
+ let cols = () => this.isStackingView ? 1 : Math.max(1, Math.min(this.filteredChildren.length,
Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
return <CollectionStackingViewFieldColumn
key={heading ? heading.heading : ""}
@@ -249,6 +277,60 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
/>;
}
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ if (!dref) return Transform.Identity();
+ let y = this._scroll; // required for document decorations to update when the text box container is scrolled
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.ScreenToLocalTransform().
+ translate(offset[0], offset[1]).
+ scale(NumCast(doc.width, 1) / this.columnWidth);
+ }
+ masonryChildren(docs: Doc[]) {
+ this._docXfs.length = 0;
+ return docs.map((d, i) => {
+ const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+ if (!pair.layout || pair.data instanceof Promise) {
+ return (null);
+ }
+ let dref = React.createRef<HTMLDivElement>();
+ let width = () => (d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth);/// (uniqueHeadings.length + 1);
+ let height = () => this.getDocHeight(pair.layout);
+ let dxf = () => this.getDocTransform(pair.layout!, dref.current!);
+ let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
+ this._docXfs.push({ dxf: dxf, width: width, height: height });
+ return !pair.layout ? (null) : <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >
+ {this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
+ </div>;
+ });
+ }
+
+ @observable _headingsHack: number = 1;
+ sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) {
+ let cols = Math.max(1, Math.min(docList.length,
+ Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
+ return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection">
+ {!heading ? (null) :
+ <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }}
+ onClick={action(() => this._headingsHack++ && heading.setCollapsed(!heading.collapsed))} >
+ {heading.heading}
+ </div>}
+ {this._headingsHack && heading && heading.collapsed ? (null) :
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
+ style={{
+ padding: `${this.yMargin}px ${this.xMargin}px`,
+ width: `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
+ gridGap: this.gridGap,
+ gridTemplateColumns: numberRange(cols).reduce((list, i) => list + ` ${this.columnWidth}px`, ""),
+ }}>
+ {this.masonryChildren(docList)}
+ {this.columnDragger}
+ </div>
+ }
+ </div>;
+ }
+
@action
addGroup = (value: string) => {
if (value && this.sectionHeaders) {
@@ -266,30 +348,46 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.CollectionView.props.Document.chromeSatus = checked ? "collapsed" : "view-mode";
+ this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+ }
+
+ onContextMenu = (e: React.MouseEvent): void => {
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped()) {
+ let subItems: ContextMenuProps[] = [];
+ subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" });
+ subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
+ subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
+ subItems.push({ description: "Edit onChildClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onChildClick") });
+ ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
+ }
}
render() {
- let headings = Array.from(this.Sections.keys());
let editableViewProps = {
GetValue: () => "",
SetValue: this.addGroup,
contents: "+ ADD A GROUP"
};
Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
-
- // let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
+ let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
+ if (this.sectionFilter) {
+ let entries = Array.from(this.Sections.entries());
+ sections = entries.sort(this.sortFunc);
+ }
return (
- <div className="collectionStackingView"
- ref={this.createRef} onDrop={this.onDrop.bind(this)} onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
- {this.sectionFilter ? Array.from(this.Sections.entries()).sort(this.sortFunc).
- map((section: [SchemaHeaderField, Doc[]]) => this.section(section[0], section[1])) :
- this.section(undefined, this.filteredChildren)}
- {(this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')) ?
+ <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
+ ref={this.createRef}
+ onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
+ onDrop={this.onDrop.bind(this)}
+ onContextMenu={this.onContextMenu}
+ onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))}
+ {!this.showAddAGroup ? (null) :
<div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
- style={{ width: (this.columnWidth / (headings.length + ((this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0))) - 10, marginTop: 10 }}>
+ style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
<EditableView {...editableViewProps} />
- </div> : null}
+ </div>}
{this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch
onChange={this.onToggle}
onClick={this.onToggle}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index df03da376..bc4fe7dd7 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,28 +1,25 @@
import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faPalette } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
-import { number } from "prop-types";
import { Doc, WidthSym } from "../../../new_fields/Doc";
-import { CollectionStackingView } from "./CollectionStackingView";
import { Id } from "../../../new_fields/FieldSymbols";
+import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { NumCast, StrCast } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
-import { NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { EditableView } from "../EditableView";
-import { action, observable, computed } from "mobx";
-import { undoBatch } from "../../util/UndoManager";
-import { DragManager } from "../../util/DragManager";
-import { DocumentManager } from "../../util/DocumentManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import "./CollectionStackingView.scss";
import { Docs } from "../../documents/Documents";
-import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { DragManager } from "../../util/DragManager";
import { CompileScript } from "../../util/Scripting";
-import { RichTextField } from "../../../new_fields/RichTextField";
+import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
-import { Flyout, anchorPoints } from "../DocumentDecorations";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faPalette } from '@fortawesome/free-solid-svg-icons';
+import { undoBatch } from "../../util/UndoManager";
+import { anchorPoints, Flyout } from "../DocumentDecorations";
+import { EditableView } from "../EditableView";
+import { CollectionStackingView } from "./CollectionStackingView";
+import "./CollectionStackingView.scss";
library.add(faPalette);
@@ -81,50 +78,23 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
let parent = this.props.parent;
parent._docXfs.length = 0;
return docs.map((d, i) => {
- let headings = this.props.headings();
- let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
- let pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
- let width = () => (d.nativeWidth && !BoolCast(d.ignoreAspect) ? Math.min(pair.layout[WidthSym](), parent.columnWidth / (uniqueHeadings.length + 1)) : parent.columnWidth / (uniqueHeadings.length + 1));/// (uniqueHeadings.length + 1);
- let height = () => parent.getDocHeight(pair.layout, uniqueHeadings.length + 1);// / (d.nativeWidth && !BoolCast(d.ignoreAspect) ? uniqueHeadings.length + 1 : 1);
+ const pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
+ if (!pair.layout || pair.data instanceof Promise) {
+ return (null);
+ }
+ let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns);
+ let height = () => parent.getDocHeight(pair.layout);
let dref = React.createRef<HTMLDivElement>();
- // if (uniqueHeadings.length > 0) {
- let dxf = () => this.getDocTransform(pair.layout, dref.current!);
- this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
- // }
- // else {
- // //have to add the height of all previous single column sections or the doc decorations will be in the wrong place.
- // let dxf = () => this.getDocTransform(layoutDoc, i, width());
- // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
- // }
- let rowHgtPcnt = height();
+ let dxf = () => parent.getDocTransform(pair.layout!, dref.current!);
+ parent._docXfs.push({ dxf: dxf, width: width, height: height });
let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
- let style = parent.singleColumn ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
- return <div className={`collectionStackingView-${parent.singleColumn ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {this.props.parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
+ let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
+ {parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
</div>;
- // } else {
- // let dref = React.createRef<HTMLDivElement>();
- // let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
- // this.props.parent._docXfs.push({ dxf: dxf, width: width, height: height });
- // let rowHgtPcnt = height();
- // let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
- // let divStyle = parent.singleColumn ? { width: width(), marginTop: i === 0 ? 0 : parent.gridGap, height: `${rowHgtPcnt}` } : { gridRowEnd: `span ${rowSpan}` };
- // return <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={divStyle} >
- // {this.props.parent.getDisplayDoc(layoutDoc, d, dxf, width)}
- // </div>;
- // }
});
}
- getDocTransform(doc: Doc, dref: HTMLDivElement) {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
- let outerXf = Utils.GetScreenTransform(this.props.parent._masonryGridRef!);
- let offset = this.props.parent.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
- return this.props.parent.props.ScreenToLocalTransform().
- translate(offset[0], offset[1]).
- scale(NumCast(doc.width, 1) / this.props.parent.columnWidth);
- }
-
getValue = (value: string): any => {
let parsed = parseInt(value);
if (!isNaN(parsed)) {
@@ -183,7 +153,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
addDocument = (value: string, shiftDown?: boolean) => {
let key = StrCast(this.props.parent.props.Document.sectionFilter);
- let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
return this.props.parent.props.addDocument(newDoc);
}
@@ -278,7 +248,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
let headings = this.props.headings();
let heading = this._heading;
let style = this.props.parent;
- let singleColumn = style.singleColumn;
+ let singleColumn = style.isStackingView;
let uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
let headerEditableViewProps = {
@@ -326,9 +296,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</button>}
</div>
</div> : (null);
- for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth}px `;
+ for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
return (
- <div key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+ <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
{headingView}
<div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
@@ -348,7 +318,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</div>
{(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
<div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / (uniqueHeadings.length + 1) }}>
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
<EditableView {...newEditableViewProps} />
</div> : null}
</div>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 077f3f941..99e5ab7b3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -11,12 +11,13 @@ import { CurrentUserUtils } from "../../../server/authentication/models/current_
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { Docs, DocumentOptions, DocumentType } from "../../documents/Documents";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Docs, DocumentOptions } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
@@ -82,7 +83,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let ind;
let doc = this.props.Document;
let id = CurrentUserUtils.id;
- let email = CurrentUserUtils.email;
+ let email = Doc.CurrentUserEmail;
let pos = { x: position[0], y: position[1] };
if (id && email) {
const proto = Doc.GetProto(doc);
@@ -112,7 +113,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
- if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) {
if (de.mods === "AltKey" && de.data.draggedDocuments.length) {
this.childDocs.map(doc =>
Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined)
@@ -206,7 +207,17 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, width: 400, height: 315, nativeWidth: 600, nativeHeight: 472.5 }));
return;
}
-
+ let matches: RegExpExecArray | null;
+ if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
+ let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
+ let proto = newBox.proto!;
+ proto.autoHeight = true;
+ proto[GoogleRef] = matches[2];
+ proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
+ proto.backgroundColor = "#eeeeff";
+ this.props.addDocument(newBox);
+ return;
+ }
let batch = UndoManager.StartBatch("collection view drop");
let promises: Promise<void>[] = [];
// tslint:disable-next-line:prefer-for-of
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 990979109..197e57808 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -31,7 +31,7 @@
position: relative;
width: 15px;
color: $intermediate-color;
- margin-top: 4px;
+ margin-top: 3px;
transform: scale(1.3, 1.3);
}
@@ -81,6 +81,9 @@
.treeViewItem-openRight {
display: none;
+ height: 17px;
+ background: gray;
+ width: 15px;
}
.treeViewItem-border {
@@ -95,15 +98,15 @@
.treeViewItem-openRight {
display: inline-block;
- height: 13px;
- margin-top: 2px;
- margin-left: 5px;
+ height: 17px;
+ background: #a8a7a7;
+ width: 15px;
// display: inline;
svg {
display: block;
padding: 0px;
- margin: 0px;
+ margin-left: 3px;
}
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 24bd24d11..50f03005c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -9,7 +9,8 @@ import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { emptyFunction, Utils } from '../../../Utils';
-import { Docs, DocUtils, DocumentType } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
@@ -25,8 +26,9 @@ import { CollectionSchemaPreview } from './CollectionSchemaView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
-import { ComputedField } from '../../../new_fields/ScriptField';
+import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { KeyValueBox } from '../nodes/KeyValueBox';
+import { ContextMenuProps } from '../ContextMenuItem';
export interface TreeViewProps {
@@ -38,6 +40,7 @@ export interface TreeViewProps {
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
panelWidth: () => number;
panelHeight: () => number;
addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
@@ -47,6 +50,8 @@ export interface TreeViewProps {
treeViewId: string;
parentKey: string;
active: () => boolean;
+ showHeaderFields: () => boolean;
+ preventTreeViewOpen: boolean;
}
library.add(faTrashAlt);
@@ -63,7 +68,12 @@ library.add(faArrowsAltH);
library.add(faPlus, faMinus);
@observer
/**
- * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ * Renders a treeView of a collection of documents
+ *
+ * special fields:
+ * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden
+ * preventTreeViewOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+ * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
*/
class TreeView extends React.Component<TreeViewProps> {
static loadId = "";
@@ -71,7 +81,9 @@ class TreeView extends React.Component<TreeViewProps> {
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; }
- @observable _collapsed: boolean = true;
+ @observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
+ @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; }
+ set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; }
@computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
@computed get dataDoc() { return this.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }
@@ -144,7 +156,7 @@ class TreeView extends React.Component<TreeViewProps> {
let rect = this._header!.current!.getBoundingClientRect();
let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
let before = x[1] < bounds[1];
- let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ let inside = x[0] > bounds[0] + 75;
this._header!.current!.className = "treeViewItem-header";
if (inside) this._header!.current!.className += " treeViewItem-header-inside";
else if (before) this._header!.current!.className += " treeViewItem-header-above";
@@ -161,20 +173,21 @@ class TreeView extends React.Component<TreeViewProps> {
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={(value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true}
- OnFillDown={(value: string) => {
+ SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)}
+ OnFillDown={undoBatch((value: string) => {
Doc.GetProto(this.dataDoc)[key] = value;
let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined;
if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
return this.props.addDocument(doc);
- }}
- OnTab={() => this.props.indentDocument && this.props.indentDocument()}
+ })}
+ OnTab={() => { TreeView.loadId = ""; this.props.indentDocument && this.props.indentDocument(); }}
/>)
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" });
ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });
ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
@@ -198,7 +211,7 @@ class TreeView extends React.Component<TreeViewProps> {
let rect = this._header!.current!.getBoundingClientRect();
let bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
let before = x[1] < bounds[1];
- let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
+ let inside = x[0] > bounds[0] + 75 || (!before && this.treeViewOpen);
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.document;
@@ -252,16 +265,16 @@ class TreeView extends React.Component<TreeViewProps> {
doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
let rows: JSX.Element[] = [];
- for (let key of Object.keys(ids).sort()) {
+ for (let key of Object.keys(ids).slice().sort()) {
let contents = doc[key];
- let contentElement: JSX.Element[] | JSX.Element = [];
+ let contentElement: (JSX.Element | null)[] | JSX.Element = [];
if (contents instanceof Doc || Cast(contents, listSpec(Doc))) {
let remDoc = (doc: Doc) => this.remove(doc, key);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
DocListCast(contents), this.props.treeViewId, doc, undefined, key, addDoc, remDoc, this.move,
- this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth);
+ this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen);
} else {
contentElement = <EditableView
key="editableView"
@@ -286,14 +299,14 @@ class TreeView extends React.Component<TreeViewProps> {
const expandKey = this.treeViewExpandedView === this.fieldKey ? this.fieldKey : this.treeViewExpandedView === "links" ? "links" : undefined;
if (expandKey !== undefined) {
let remDoc = (doc: Doc) => this.remove(doc, expandKey);
- let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before);
+ let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, expandKey, doc, addBefore, before, false, true);
let docs = expandKey === "links" ? this.childLinks : this.childDocs;
return <ul key={expandKey + "more"}>
{!docs ? (null) :
TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc,
this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move,
- this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform,
- this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth)}
+ this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
+ this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen)}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
@@ -318,6 +331,7 @@ class TreeView extends React.Component<TreeViewProps> {
active={this.props.active}
whenActiveChanged={emptyFunction as any}
addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
setPreviewScript={emptyFunction}>
</CollectionSchemaPreview>
</div>;
@@ -326,8 +340,8 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderBullet() {
- return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
- {<FontAwesomeIcon icon={this._collapsed ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
+ return <div className="bullet" title="view inline" onClick={action(() => this.treeViewOpen = !this.treeViewOpen)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
+ {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
</div>;
}
/**
@@ -341,31 +355,30 @@ class TreeView extends React.Component<TreeViewProps> {
let headerElements = (
<span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
onPointerDown={action(() => {
- if (!this._collapsed) {
+ if (this.treeViewOpen) {
this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" :
this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" :
this.treeViewExpandedView === "layout" && this.props.document.links ? "links" :
this.childDocs ? this.fieldKey : "fields";
}
- this._collapsed = false;
+ this.treeViewOpen = true;
})}>
{this.treeViewExpandedView}
</span>);
- let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
- let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
- <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
- <FontAwesomeIcon icon="angle-right" size="lg" />
- </div>);
+ let openRight = (<div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" />
+ </div>);
return <>
- <div className="docContainer" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
+ <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
style={{
+ color: this.props.document.isMinimized ? "red" : "black",
background: Doc.IsBrushed(this.props.document) ? "#06121212" : "0",
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
}} >
{this.editableView("title")}
</div >
- {headerElements}
+ {this.props.showHeaderFields() ? headerElements : (null)}
{openRight}
</>;
}
@@ -378,13 +391,13 @@ class TreeView extends React.Component<TreeViewProps> {
{this.renderTitle}
</div>
<div className="treeViewItem-border">
- {this._collapsed ? (null) : this.renderContent}
+ {!this.treeViewOpen ? (null) : this.renderContent}
</div>
</li>
</div>;
}
public static GetChildElements(
- docs: Doc[],
+ docList: Doc[],
treeViewId: string,
containingCollection: Doc,
dataDoc: Doc | undefined,
@@ -394,27 +407,74 @@ class TreeView extends React.Component<TreeViewProps> {
move: DragManager.MoveFunction,
dropAction: dropActionType,
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void,
+ pinToPres: (document: Doc) => void,
screenToLocalXf: () => Transform,
outerXf: () => { translateX: number, translateY: number },
active: () => boolean,
panelWidth: () => number,
- renderDepth: number
+ renderDepth: number,
+ showHeaderFields: () => boolean,
+ preventTreeViewOpen: boolean
) {
- let docList = docs.filter(child => !child.excludeFromLibrary);
+ let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0);
+ let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
+ if (viewSpecScript) {
+ let script = viewSpecScript.script;
+ docs = docs.filter(d => {
+ let res = script.run({ doc: d });
+ if (res.success) {
+ return res.result;
+ }
+ else {
+ console.log(res.error);
+ }
+ });
+ }
+
+ let ascending = Cast(containingCollection.sortAscending, "boolean", null);
+ if (ascending !== undefined) {
+ docs.sort(function (a, b): 1 | -1 {
+ let descA = ascending ? b : a;
+ let descB = ascending ? a : b;
+ let first = descA.title;
+ let second = descB.title;
+ // TODO find better way to sort how to sort..................
+ if (typeof first === 'number' && typeof second === 'number') {
+ return (first - second) > 0 ? 1 : -1;
+ }
+ if (typeof first === 'string' && typeof second === 'string') {
+ return first > second ? 1 : -1;
+ }
+ if (typeof first === 'boolean' && typeof second === 'boolean') {
+ // if (first === second) { // bugfixing?: otherwise, the list "flickers" because the list is resorted during every load
+ // return Number(descA.x) > Number(descB.x) ? 1 : -1;
+ // }
+ return first > second ? 1 : -1;
+ }
+ return ascending ? 1 : -1;
+ });
+ }
+
let rowWidth = () => panelWidth() - 20;
- return docList.map((child, i) => {
+ return docs.map((child, i) => {
let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+ if (!pair.layout || pair.data instanceof Promise) {
+ return (null);
+ }
let indent = i === 0 ? undefined : () => {
- if (StrCast(docList[i - 1].layout).indexOf("CollectionView") !== -1) {
- let fieldKeysub = StrCast(docList[i - 1].layout).split("fieldKey")[1];
+ if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) {
+ let fieldKeysub = StrCast(docs[i - 1].layout).split("fieldKey")[1];
let fieldKey = fieldKeysub.split("\"")[1];
- Doc.AddDocToList(docList[i - 1], fieldKey, child);
- remove(child);
+ if (fieldKey && Cast(docs[i - 1][fieldKey], listSpec(Doc)) !== undefined) {
+ Doc.AddDocToList(docs[i - 1], fieldKey, child);
+ docs[i - 1].treeViewOpen = true;
+ remove(child);
+ }
}
};
let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
- return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false);
+ return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false);
};
let rowHeight = () => {
let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
@@ -435,10 +495,13 @@ class TreeView extends React.Component<TreeViewProps> {
moveDocument={move}
dropAction={dropAction}
addDocTab={addDocTab}
+ pinToPres={pinToPres}
ScreenToLocalTransform={screenToLocalXf}
outerXf={outerXf}
parentKey={key}
- active={active} />;
+ active={active}
+ showHeaderFields={showHeaderFields}
+ preventTreeViewOpen={preventTreeViewOpen} />;
});
}
}
@@ -480,6 +543,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
e.stopPropagation();
e.preventDefault();
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ } else {
+ let layoutItems: ContextMenuProps[] = [];
+ layoutItems.push({ description: this.props.Document.preventTreeViewOpen ? "Persist Treeview State" : "Abandon Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
+ ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
@@ -518,7 +585,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
render() {
Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
- let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
+ let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
@@ -532,20 +599,22 @@ export class CollectionTreeView extends CollectionSubView(Document) {
display={"block"}
height={72}
GetValue={() => StrCast(this.resolvedDataDoc.title)}
- SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
- OnFillDown={(value: string) => {
+ SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)}
+ OnFillDown={undoBatch((value: string) => {
Doc.GetProto(this.props.Document).title = value;
let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined;
if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
- Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true);
- }} />
+ Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
+ })} />
{this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove,
- moveDoc, dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth)
+ moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
+ this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled",
+ BoolCast(this.props.Document.preventTreeViewOpen))
}
</ul>
</div >
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7a402798e..6182e82f4 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -30,6 +30,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
+ constructor(props: any) {
+ super(props);
+ }
+
componentDidMount = () => {
this._reactionDisposer = reaction(() => StrCast(this.props.Document.chromeStatus),
() => {
@@ -65,7 +69,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
@action
private collapse = (value: boolean) => {
this._collapsed = value;
- this.props.Document.chromeStatus = value ? "collapsed" : "visible";
+ this.props.Document.chromeStatus = value ? "collapsed" : "enabled";
}
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
@@ -73,12 +77,10 @@ export class CollectionView extends React.Component<FieldViewProps> {
if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
return [(null), this.SubViewHelper(type, renderProps)];
}
- else {
- return [
- (<CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />),
- this.SubViewHelper(type, renderProps)
- ];
- }
+ return [
+ <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />,
+ this.SubViewHelper(type, renderProps)
+ ];
}
get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
@@ -86,12 +88,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
let subItems: ContextMenuProps[] = [];
- subItems.push({
- description: "Freeform", event: () => {
- this.props.Document.viewType = CollectionViewType.Freeform;
- delete this.props.Document.usePivotLayout;
- }, icon: "signature"
- });
+ subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; delete this.props.Document.usePivotLayout; }, icon: "signature" });
if (CollectionBaseView.InSafeMode()) {
ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
}
@@ -107,10 +104,15 @@ export class CollectionView extends React.Component<FieldViewProps> {
}
}
ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
- ContextMenu.Instance.addItem({ description: "Apply Template", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
- ContextMenu.Instance.addItem({
- description: this.props.Document.chromeStatus !== "disabled" ? "Hide Chrome" : "Show Chrome", event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram"
- });
+ let existing = ContextMenu.Instance.findByDescription("Layout...");
+ let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+ layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
+ !existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
+
+ let makes = ContextMenu.Instance.findByDescription("Make...");
+ let makeItems: ContextMenuProps[] = makes && "subitems" in makes ? makes.subitems : [];
+ makeItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
+ !makes && ContextMenu.Instance.addItem({ description: "Make...", subitems: makeItems, icon: "hand-point-right" });
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 793cb7a8b..64411b5fe 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -7,7 +7,6 @@
z-index: 9001;
transition: top .5s;
background: lightgrey;
- padding: 10px;
.collectionViewChrome {
display: grid;
@@ -42,6 +41,14 @@
transform-origin: top left;
// margin-top: 10px;
}
+ .collectionViewBaseChrome-template {
+ margin-left: 10px;
+ display: grid;
+ background: rgb(238, 238, 238);
+ color:grey;
+ margin-top:auto;
+ margin-bottom:auto;
+ }
.collectionViewBaseChrome-viewSpecs {
margin-left: 10px;
@@ -57,7 +64,7 @@
font-size: 75%;
background: rgb(238, 238, 238);
height: 100%;
- width: 150px;
+ width: 75px;
}
.collectionViewBaseChrome-viewSpecsMenu {
@@ -97,7 +104,7 @@
.collectionViewBaseChrome-viewSpecsMenu-lastRow {
display: grid;
- grid-template-columns: 1fr 1fr;
+ grid-template-columns: 1fr 1fr 1fr;
grid-gap: 10px;
margin: 10px;
}
@@ -106,17 +113,20 @@
}
- .collectionStackingViewChrome-cont {
+ .collectionStackingViewChrome-cont,
+ .collectionTreeViewChrome-cont {
display: flex;
justify-content: space-between;
}
- .collectionStackingViewChrome-sort {
+ .collectionStackingViewChrome-sort,
+ .collectionTreeViewChrome-sort {
display: flex;
align-items: center;
justify-content: space-between;
- .collectionStackingViewChrome-sortIcon {
+ .collectionStackingViewChrome-sortIcon,
+ .collectionTreeViewChrome-sortIcon {
transition: transform .5s;
margin-left: 10px;
}
@@ -127,18 +137,21 @@
}
- .collectionStackingViewChrome-sectionFilter-cont {
+ .collectionStackingViewChrome-sectionFilter-cont,
+ .collectionTreeViewChrome-sectionFilter-cont {
justify-self: right;
display: flex;
font-size: 75%;
letter-spacing: 2px;
- .collectionStackingViewChrome-sectionFilter-label {
+ .collectionStackingViewChrome-sectionFilter-label,
+ .collectionTreeViewChrome-sectionFilter-label {
vertical-align: center;
padding: 10px;
}
- .collectionStackingViewChrome-sectionFilter {
+ .collectionStackingViewChrome-sectionFilter,
+ .collectionTreeViewChrome-sectionFilter {
color: white;
width: 100px;
text-align: center;
@@ -165,7 +178,8 @@
}
}
- .collectionStackingViewChrome-sectionFilter:hover {
+ .collectionStackingViewChrome-sectionFilter:hover,
+ .collectionTreeViewChrome-sectionFilter:hover {
cursor: text;
}
}
@@ -220,4 +234,75 @@
margin-left: 50px;
}
}
+}
+
+
+.commandEntry-outerDiv {
+ display: flex;
+ flex-direction: column;
+ width: 165px;
+ height: 40px;
+}
+.commandEntry-inputArea {
+ display:flex;
+ flex-direction: row;
+ width: 150px;
+ margin: auto 0 auto auto;
+}
+
+.react-autosuggest__container {
+ position: relative;
+ width: 100%;
+ margin-left: 5px;
+ margin-right: 5px;
+}
+
+.react-autosuggest__input {
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ width: 100%;
+}
+
+.react-autosuggest__input--focused {
+ outline: none;
+}
+
+.react-autosuggest__input--open {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.react-autosuggest__suggestions-container {
+ display: none;
+}
+
+.react-autosuggest__suggestions-container--open {
+ display: block;
+ position: fixed;
+ overflow-y: auto;
+ max-height: 400px;
+ width: 180px;
+ border: 1px solid #aaa;
+ background-color: #fff;
+ font-family: Helvetica, sans-serif;
+ font-weight: 300;
+ font-size: 16px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ z-index: 2;
+}
+
+.react-autosuggest__suggestions-list {
+ margin: 0;
+ padding: 0;
+ list-style-type: none;
+}
+
+.react-autosuggest__suggestion {
+ cursor: pointer;
+ padding: 10px 20px;
+}
+
+.react-autosuggest__suggestion--highlighted {
+ background-color: #ddd;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 52c47e7e8..c897af17e 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -1,26 +1,25 @@
-import * as React from "react";
-import { CollectionView } from "./CollectionView";
-import "./CollectionViewChromes.scss";
-import { CollectionViewType } from "./CollectionBaseView";
-import { undoBatch } from "../../util/UndoManager";
-import { action, observable, runInAction, computed, IObservable, IObservableValue, reaction, autorun } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
+import * as React from "react";
import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils, emptyFunction } from "../../../Utils";
+import { DragManager } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
+import { undoBatch } from "../../util/UndoManager";
+import { EditableView } from "../EditableView";
+import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
import { DocLike } from "../MetadataEntryMenu";
+import { CollectionViewType } from "./CollectionBaseView";
+import { CollectionView } from "./CollectionView";
+import "./CollectionViewChromes.scss";
import * as Autosuggest from 'react-autosuggest';
-import { EditableView } from "../EditableView";
-import { StrCast, NumCast, BoolCast, Cast } from "../../../new_fields/Types";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Utils } from "../../../Utils";
import KeyRestrictionRow from "./KeyRestrictionRow";
-import { CompileScript } from "../../util/Scripting";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { CollectionSchemaView } from "./CollectionSchemaView";
-import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
-import { listSpec } from "../../../new_fields/Schema";
-import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { threadId } from "worker_threads";
const datepicker = require('js-datepicker');
interface CollectionViewChromeProps {
@@ -29,31 +28,105 @@ interface CollectionViewChromeProps {
collapse?: (value: boolean) => any;
}
+interface Filter {
+ key: string;
+ value: string;
+ contains: boolean;
+}
+
let stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
@observer
export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
+ //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
+
+ _templateCommand = {
+ title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"],
+ initialize: emptyFunction,
+ immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined
+ };
+ _contentCommand = {
+ // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting...
+ title: "set content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
+ initialize: emptyFunction,
+ immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
+ };
+ _viewCommand = {
+ title: "restore view", script: "this.target.panX = this.restoredPanX; this.target.panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
+ immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document.panX = 0; this.props.CollectionView.props.Document.panY = 0; this.props.CollectionView.props.Document.scale = 1; },
+ initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document.panX; button.restoredPanY = this.props.CollectionView.props.Document.panY; button.restoredScale = this.props.CollectionView.props.Document.scale; }
+ };
+ _freeform_commands = [this._contentCommand, this._templateCommand, this._viewCommand];
+ _stacking_commands = [this._contentCommand, this._templateCommand];
+ _masonry_commands = [this._contentCommand, this._templateCommand];
+ _tree_commands = [];
+ private get _buttonizableCommands() {
+ switch (this.props.type) {
+ case CollectionViewType.Tree: return this._tree_commands;
+ case CollectionViewType.Stacking: return this._stacking_commands;
+ case CollectionViewType.Masonry: return this._stacking_commands;
+ case CollectionViewType.Freeform: return this._freeform_commands;
+ }
+ return [];
+ }
+ private _picker: any;
+ private _commandRef = React.createRef<HTMLInputElement>();
+ private _autosuggestRef = React.createRef<Autosuggest>();
+ @observable private _currentKey: string = "";
@observable private _viewSpecsOpen: boolean = false;
@observable private _dateWithinValue: string = "";
@observable private _dateValue: Date | string = "";
@observable private _keyRestrictions: [JSX.Element, string][] = [];
- @observable private _collapsed: boolean = false;
+ @observable private suggestions: string[] = [];
@computed private get filterValue() { return Cast(this.props.CollectionView.props.Document.viewSpecScript, ScriptField); }
- private _picker: any;
- private _datePickerElGuid = Utils.GenerateGuid();
+ getFilters = (script: string) => {
+ let re: any = /(!)?\(\(\(doc\.(\w+)\s+&&\s+\(doc\.\w+\s+as\s+\w+\)\.includes\(\"(\w+)\"\)/g;
+ let arr: any[] = re.exec(script);
+ let toReturn: Filter[] = [];
+ if (arr !== null) {
+ let filter: Filter = {
+ key: arr[2],
+ value: arr[3],
+ contains: (arr[1] === "!") ? false : true,
+ };
+ toReturn.push(filter);
+ script = script.replace(arr[0], "");
+ if (re.exec(script) !== null) {
+ toReturn.push(...this.getFilters(script));
+ }
+ else { return toReturn; }
+ }
+ return toReturn;
+ }
+
+ addKeyRestrictions = (fields: Filter[]) => {
+
+ if (fields.length !== 0) {
+ for (let i = 0; i < fields.length; i++) {
+ this._keyRestrictions.push([<KeyRestrictionRow field={fields[i].key} value={fields[i].value} key={Utils.GenerateGuid()} contains={fields[i].contains} script={(value: string) => runInAction(() => this._keyRestrictions[i][1] = value)} />, ""]);
+
+ }
+ if (this._keyRestrictions.length === 1) {
+ this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+ }
+ }
+ else {
+ this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]);
+ this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+ }
+ }
componentDidMount = () => {
- setTimeout(() => this._picker = datepicker("#" + this._datePickerElGuid, {
- disabler: (date: Date) => date > new Date(),
- onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date),
- dateSelected: new Date()
- }), 1000);
- runInAction(() => {
- this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[0][1] = value)} />, ""]);
- this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={false} script={(value: string) => runInAction(() => this._keyRestrictions[1][1] = value)} />, ""]);
+ let fields: Filter[] = [];
+ if (this.filterValue) {
+ let string = this.filterValue.script.originalScript;
+ fields = this.getFilters(string);
+ }
+ runInAction(() => {
+ this.addKeyRestrictions(fields);
// chrome status is one of disabled, collapsed, or visible. this determines initial state from document
let chromeStatus = this.props.CollectionView.props.Document.chromeStatus;
if (chromeStatus) {
@@ -61,7 +134,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
throw new Error("how did you get here, if chrome status is 'disabled' on a collection, a chrome shouldn't even be instantiated!");
}
else if (chromeStatus === "collapsed") {
- this._collapsed = true;
if (this.props.collapse) {
this.props.collapse(true);
}
@@ -113,17 +185,17 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action
addKeyRestriction = (e: React.MouseEvent) => {
let index = this._keyRestrictions.length;
- this._keyRestrictions.push([<KeyRestrictionRow key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
+ this._keyRestrictions.push([<KeyRestrictionRow field="" value="" key={Utils.GenerateGuid()} contains={true} script={(value: string) => runInAction(() => this._keyRestrictions[index][1] = value)} />, ""]);
this.openViewSpecs(e);
}
- @action
+ @action.bound
applyFilter = (e: React.MouseEvent) => {
+
this.openViewSpecs(e);
- let keyRestrictionScript = `${this._keyRestrictions.map(i => i[1])
- .reduce((acc: string, value: string, i: number) => value ? `${acc} && ${value}` : acc)}`;
+ let keyRestrictionScript = "(" + this._keyRestrictions.map(i => i[1]).filter(i => i.length > 0).join(" && ") + ")";
let yearOffset = this._dateWithinValue[1] === 'y' ? 1 : 0;
let monthOffset = this._dateWithinValue[1] === 'm' ? parseInt(this._dateWithinValue[0]) : 0;
let weekOffset = this._dateWithinValue[1] === 'w' ? parseInt(this._dateWithinValue[0]) : 0;
@@ -143,9 +215,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
}
let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
- `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} ${keyRestrictionScript}` :
- `return ${keyRestrictionScript} ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
+ `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
+ `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
"return true";
+
let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false });
if (compiled.compiled) {
this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
@@ -163,27 +236,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action
toggleCollapse = () => {
- this._collapsed = !this._collapsed;
+ this.props.CollectionView.props.Document.chromeStatus = this.props.CollectionView.props.Document.chromeStatus === "enabled" ? "collapsed" : "enabled";
if (this.props.collapse) {
- this.props.collapse(this._collapsed);
+ this.props.collapse(this.props.CollectionView.props.Document.chromeStatus !== "enabled");
}
}
subChrome = () => {
switch (this.props.type) {
- case CollectionViewType.Stacking: return (
- <CollectionStackingViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type} />);
- case CollectionViewType.Schema: return (
- <CollectionSchemaViewChrome
- key="collchrome"
- CollectionView={this.props.CollectionView}
- type={this.props.type}
- />);
- default:
- return null;
+ case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
+ default: return null;
}
}
@@ -217,17 +281,116 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
})} />);
}
+ @action.bound
+ clearFilter = () => {
+ let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false });
+ if (compiled.compiled) {
+ this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
+ }
+
+ this._keyRestrictions = [];
+ this.addKeyRestrictions([]);
+ }
+
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this.dropDisposer && this.dropDisposer();
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+
+ @undoBatch
+ @action
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ if (de.data instanceof DragManager.DocumentDragData && de.data.draggedDocuments.length) {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.data.draggedDocuments));
+ e.stopPropagation();
+ }
+ return true;
+ }
+
+ datePickerRef = (node: HTMLInputElement) => {
+ if (node) {
+ try {
+ this._picker = datepicker("#" + node.id, {
+ disabler: (date: Date) => date > new Date(),
+ onSelect: (instance: any, date: Date) => runInAction(() => this._dateValue = date),
+ dateSelected: new Date()
+ });
+ } catch (e) {
+ console.log("date picker exception:" + e);
+ }
+ }
+ }
+
+ renderSuggestion = (suggestion: string) => {
+ return <p>{suggestion}</p>;
+ }
+ getSuggestionValue = (suggestion: string) => suggestion;
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ }
+ onSuggestionFetch = async ({ value }: { value: string }) => {
+ const sugg = await this.getKeySuggestions(value);
+ runInAction(() => this.suggestions = sugg);
+ }
+ @action
+ onSuggestionClear = () => {
+ this.suggestions = [];
+ }
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ return this._buttonizableCommands.filter(c => c.title.indexOf(value) !== -1).map(c => c.title);
+ }
+
+ autoSuggestDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ }
+
+ private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ private _sensitivity: number = 16;
+
+ dragCommandDown = (e: React.PointerEvent) => {
+
+ this._startDragPosition = { x: e.clientX, y: e.clientY };
+ document.addEventListener("pointermove", this.dragPointerMove);
+ document.addEventListener("pointerup", this.dragPointerUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ dragPointerMove = (e: PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ let [dx, dy] = [e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y];
+ if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c =>
+ DragManager.StartButtonDrag([this._commandRef.current!], c.script, c.title,
+ { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY));
+ document.removeEventListener("pointermove", this.dragPointerMove);
+ document.removeEventListener("pointerup", this.dragPointerUp);
+ }
+ }
+ dragPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragPointerMove);
+ document.removeEventListener("pointerup", this.dragPointerUp);
+
+ }
+
render() {
+ let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
return (
- <div className="collectionViewChrome-cont" style={{ top: this._collapsed ? -70 : 0 }}>
+ <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0 }}>
<div className="collectionViewChrome">
<div className="collectionViewBaseChrome">
<button className="collectionViewBaseChrome-collapse"
style={{
- top: this._collapsed ? 70 : 10,
- transform: `rotate(${this._collapsed ? 180 : 0}deg) scale(${this._collapsed ? 0.5 : 1}) translate(${this._collapsed ? "-100%, -100%" : "0, 0"})`,
- opacity: (this._collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9,
- left: (this._collapsed ? 0 : "unset"),
+ top: collapsed ? 70 : 10,
+ transform: `rotate(${collapsed ? 180 : 0}deg) scale(${collapsed ? 0.5 : 1}) translate(${collapsed ? "-100%, -100%" : "0, 0"})`,
+ opacity: (collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9,
+ left: (collapsed ? 0 : "unset"),
}}
title="Collapse collection chrome" onClick={this.toggleCollapse}>
<FontAwesomeIcon icon="caret-up" size="2x" />
@@ -243,12 +406,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
</select>
- <div className="collectionViewBaseChrome-viewSpecs" style={{ display: this._collapsed ? "none" : "grid" }}>
+ <div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
<input className="collectionViewBaseChrome-viewSpecsInput"
- placeholder="FILTER DOCUMENTS"
- value={this.filterValue ? this.filterValue.script.originalScript : ""}
+ placeholder="FILTER"
+ value={this.filterValue ? this.filterValue.script.originalScript === "return true" ? "" : this.filterValue.script.originalScript : ""}
onChange={(e) => { }}
- onPointerDown={this.openViewSpecs} />
+ onPointerDown={this.openViewSpecs}
+ id="viewSpecsInput" />
{this.getPivotInput()}
<div className="collectionViewBaseChrome-viewSpecsMenu"
onPointerDown={this.openViewSpecs}
@@ -260,7 +424,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<div className="collectionViewBaseChrome-viewSpecsMenu-row">
<div className="collectionViewBaseChrome-viewSpecsMenu-rowLeft">
CREATED WITHIN:
- </div>
+ </div>
<select className="collectionViewBaseChrome-viewSpecsMenu-rowMiddle"
style={{ textTransform: "uppercase", textAlign: "center" }}
value={this._dateWithinValue}
@@ -275,19 +439,31 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option value="1y">1 year of</option>
</select>
<input className="collectionViewBaseChrome-viewSpecsMenu-rowRight"
- id={this._datePickerElGuid}
+ id={Utils.GenerateGuid()}
+ ref={this.datePickerRef}
value={this._dateValue instanceof Date ? this._dateValue.toLocaleDateString() : this._dateValue}
onChange={(e) => runInAction(() => this._dateValue = e.target.value)}
onPointerDown={this.openDatePicker}
placeholder="Value" />
</div>
<div className="collectionViewBaseChrome-viewSpecsMenu-lastRow">
- <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}>
- ADD KEY RESTRICTION
- </button>
- <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}>
- APPLY FILTER
- </button>
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.addKeyRestriction}> ADD KEY RESTRICTION </button>
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.applyFilter}> APPLY FILTER </button>
+ <button className="collectonViewBaseChrome-viewSpecsMenu-lastRowButton" onClick={this.clearFilter}> CLEAR </button>
+ </div>
+ </div>
+ </div>
+ <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} >
+ <div className="commandEntry-outerDiv" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
+ <div className="commandEntry-inputArea" onPointerDown={this.autoSuggestDown} >
+ <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
+ getSuggestionValue={this.getSuggestionValue}
+ suggestions={this.suggestions}
+ alwaysRenderSuggestions={true}
+ renderSuggestion={this.renderSuggestion}
+ onSuggestionsFetchRequested={this.onSuggestionFetch}
+ onSuggestionsClearRequested={this.onSuggestionClear}
+ ref={this._autosuggestRef} />
</div>
</div>
</div>
@@ -358,18 +534,13 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
render() {
return (
<div className="collectionStackingViewChrome-cont">
- <button className="collectionStackingViewChrome-sort" onClick={this.toggleSort}>
- <div className="collectionStackingViewChrome-sortLabel">
- Sort
- </div>
- <div className="collectionStackingViewChrome-sortIcon" style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
- <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
- </div>
- </button>
<div className="collectionStackingViewChrome-sectionFilter-cont">
<div className="collectionStackingViewChrome-sectionFilter-label">
GROUP ITEMS BY:
- </div>
+ </div>
+ <div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
<div className="collectionStackingViewChrome-sectionFilter">
<EditableView
GetValue={() => this.sectionFilter}
@@ -468,4 +639,81 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
</div >
);
}
-} \ No newline at end of file
+}
+
+@observer
+export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
+ @observable private _currentKey: string = "";
+ @observable private suggestions: string[] = [];
+
+ @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); }
+ @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
+
+ getKeySuggestions = async (value: string): Promise<string[]> => {
+ value = value.toLowerCase();
+ let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
+ = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
+ if (typeof docs === "function") {
+ docs = docs();
+ }
+ docs = await docs;
+ if (docs instanceof Doc) {
+ return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
+ } else {
+ const keys = new Set<string>();
+ docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
+ return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
+ }
+ }
+
+ @action
+ onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
+ this._currentKey = newValue;
+ }
+
+ getSuggestionValue = (suggestion: string) => suggestion;
+
+ renderSuggestion = (suggestion: string) => {
+ return <p>{suggestion}</p>;
+ }
+
+ onSuggestionFetch = async ({ value }: { value: string }) => {
+ const sugg = await this.getKeySuggestions(value);
+ runInAction(() => {
+ this.suggestions = sugg;
+ });
+ }
+
+ @action
+ onSuggestionClear = () => {
+ this.suggestions = [];
+ }
+
+ setValue = (value: string) => {
+ this.props.CollectionView.props.Document.sectionFilter = value;
+ return true;
+ }
+
+ @action toggleSort = () => {
+ if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined;
+ else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false;
+ else this.props.CollectionView.props.Document.sortAscending = true;
+ }
+ @action resetValue = () => { this._currentKey = this.sectionFilter; };
+
+ render() {
+ return (
+ <div className="collectionTreeViewChrome-cont">
+ <button className="collectionTreeViewChrome-sort" onClick={this.toggleSort}>
+ <div className="collectionTreeViewChrome-sortLabel">
+ Sort
+ </div>
+ <div className="collectionTreeViewChrome-sortIcon" style={{ transform: `rotate(${this.descending === undefined ? "90" : this.descending ? "180" : "0"}deg)` }}>
+ <FontAwesomeIcon icon="caret-up" size="2x" color="white" />
+ </div>
+ </button>
+ </div>
+ );
+ }
+}
+
diff --git a/src/client/views/collections/KeyRestrictionRow.tsx b/src/client/views/collections/KeyRestrictionRow.tsx
index 1b59547d8..e35b7d7d3 100644
--- a/src/client/views/collections/KeyRestrictionRow.tsx
+++ b/src/client/views/collections/KeyRestrictionRow.tsx
@@ -7,12 +7,14 @@ import { Doc } from "../../../new_fields/Doc";
interface IKeyRestrictionProps {
contains: boolean;
script: (value: string) => void;
+ field: string;
+ value: string;
}
@observer
export default class KeyRestrictionRow extends React.Component<IKeyRestrictionProps> {
- @observable private _key = "";
- @observable private _value = "";
+ @observable private _key = this.props.field;
+ @observable private _value = this.props.value;
@observable private _contains = this.props.contains;
render() {
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index c3e55d825..17111af58 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -50,10 +50,10 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
render() {
return (
<>
- <p>Contexts:</p>
- {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
- {this._otherDocs.length ? <hr></hr> : null}
- {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ <p key="contexts">Contexts:</p>
+ {this._docs.map(doc => <p key={doc.col[Id] + doc.target[Id]}><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._otherDocs.length ? <hr key="hr" /> : null}
+ {this._otherDocs.map(doc => <p key="p"><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
</>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 3193f5624..b8148852d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -55,7 +55,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
ctx.stroke();
// ctx.font = "10px Arial";
- // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
+ // ctx.fillText(Doc.CurrentUserEmail[0].toUpperCase(), 10, 10);
}
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 50f7e2dc8..2d4775070 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,17 +1,18 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
-import { faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faChalkboard, faBraille } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, observable } from "mobx";
+import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
+import { action, computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCastAsync, HeightSym, WidthSym, DocListCast, FieldResult, Field, Opt } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Field, FieldResult, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
-import { emptyFunction, returnOne, Utils, returnFalse, returnEmptyString } from "../../../../Utils";
+import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { DocumentType } from "../../../documents/DocumentTypes";
import { DocumentManager } from "../../../util/DocumentManager";
import { DragManager } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
@@ -29,8 +30,8 @@ import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
import { pageSchema } from "../../nodes/ImageBox";
import { OverlayElementOptions, OverlayView } from "../../OverlayView";
import PDFMenu from "../../pdf/PDFMenu";
-import { CollectionSubView } from "../CollectionSubView";
import { ScriptBox } from "../../ScriptBox";
+import { CollectionSubView } from "../CollectionSubView";
import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
@@ -40,10 +41,10 @@ import v5 = require("uuid/v5");
import { ClientRecommender } from "../../../ClientRecommender";
import { SearchUtil } from "../../../util/SearchUtil";
import { SearchBox } from "../../SearchBox";
-
-import { DocumentType, Docs } from "../../../documents/Documents";
import { RouteStore } from "../../../../server/RouteStore";
import { string, number, elementType } from "prop-types";
+import { DocServer } from "../../../DocServer";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
@@ -84,7 +85,6 @@ export namespace PivotView {
let collection = target.Document;
const field = StrCast(collection.pivotField) || "title";
const width = NumCast(collection.pivotWidth) || 200;
-
const groups = new Map<FieldResult<Field>, Doc[]>();
for (const doc of target.childDocs) {
@@ -97,14 +97,11 @@ export namespace PivotView {
} else {
groups.set(val, [doc]);
}
-
}
let minSize = Infinity;
- groups.forEach((val, key) => {
- minSize = Math.min(minSize, val.length);
- });
+ groups.forEach((val, key) => minSize = Math.min(minSize, val.length));
const numCols = NumCast(collection.pivotNumColumns) || Math.ceil(Math.sqrt(minSize));
const fontSize = NumCast(collection.pivotFontSize);
@@ -141,48 +138,40 @@ export namespace PivotView {
});
let elements = target.viewDefsToJSX(groupNames);
- let curPage = FieldValue(target.Document.curPage, -1);
-
- let docViews = target.childDocs.filter(doc => doc instanceof Doc).reduce((prev, doc) => {
- var page = NumCast(doc.page, -1);
- if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
- let minim = BoolCast(doc.isMinimized);
- if (minim === undefined || !minim) {
- let defaultPosition = (): ViewDefBounds => {
- return {
- x: NumCast(doc.x),
- y: NumCast(doc.y),
- z: NumCast(doc.z),
- width: NumCast(doc.width),
- height: NumCast(doc.height)
- };
+ let docViews = target.childDocs.reduce((prev, doc) => {
+ let minim = BoolCast(doc.isMinimized);
+ if (minim === undefined || !minim) {
+ let defaultPosition = (): ViewDefBounds => {
+ return {
+ x: NumCast(doc.x),
+ y: NumCast(doc.y),
+ z: NumCast(doc.z),
+ width: NumCast(doc.width),
+ height: NumCast(doc.height)
};
- const pos = docMap.get(doc) || defaultPosition();
- prev.push({
- ele: (
- <CollectionFreeFormDocumentView
- key={doc[Id]}
- x={pos.x}
- y={pos.y}
- width={pos.width}
- height={pos.height}
- {...target.getChildDocumentViewProps(doc)}
- />),
- bounds: {
- x: pos.x,
- y: pos.y,
- z: pos.z,
- width: NumCast(pos.width),
- height: NumCast(pos.height)
- }
- });
- }
+ };
+ const pos = docMap.get(doc) || defaultPosition();
+ prev.push({
+ ele: <CollectionFreeFormDocumentView
+ key={doc[Id]}
+ x={pos.x}
+ y={pos.y}
+ width={pos.width}
+ height={pos.height}
+ {...target.getChildDocumentViewProps(doc)}
+ />,
+ bounds: {
+ x: pos.x,
+ y: pos.y,
+ z: pos.z,
+ width: NumCast(pos.width),
+ height: NumCast(pos.height)
+ }
+ });
}
return prev;
}, elements);
- target.resetSelectOnLoaded();
-
return docViews;
};
@@ -193,12 +182,26 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema)
@observer
export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
- private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
private _lastX: number = 0;
private _lastY: number = 0;
private get _pwidth() { return this.props.PanelWidth(); }
private get _pheight() { return this.props.PanelHeight(); }
private inkKey = "ink";
+ private _childLayoutDisposer?: IReactionDisposer;
+
+ componentDidMount() {
+ this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
+ async (args) => {
+ this.childDocs.filter(doc => args[1] instanceof Doc || doc.layout instanceof Doc).map(async doc => {
+ if (!Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc)) {
+ Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined);
+ }
+ });
+ });
+ }
+ componentWillUnmount() {
+ this._childLayoutDisposer && this._childLayoutDisposer();
+ }
get parentScaling() {
return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
@@ -242,7 +245,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
private addLiveTextBox = (newBox: Doc) => {
- this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
this.addDocument(newBox, false);
}
private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
@@ -297,7 +300,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (super.drop(e, de)) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
- let z = NumCast(de.data.draggedDocuments[0].z);
+ let z = NumCast(de.data.droppedDocuments[0].z);
let x = (z ? xpo : xp) - de.data.xOffset;
let y = (z ? ypo : yp) - de.data.yOffset;
let dropX = NumCast(de.data.droppedDocuments[0].x);
@@ -351,9 +354,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}, -1);
if (cluster !== -1) {
let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+
+ // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view
+ let prevSelected = SelectionManager.SelectedDocuments();
this.selectDocuments(eles);
let clusterDocs = SelectionManager.SelectedDocuments();
SelectionManager.DeselectAll();
+ prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
+
let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
de.moveDocument = this.props.moveDocument;
const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
@@ -628,19 +636,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getScale = () => this.Document.scale ? this.Document.scale : 1;
- getChildDocumentViewProps(childDocLayout: Doc): DocumentViewProps {
- let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, childDocLayout);
+ getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
- DataDoc: pair.data,
- Document: pair.layout,
+ DataDoc: childData,
+ Document: childLayout,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
- ScreenToLocalTransform: pair.layout.z ? this.getTransformOverlay : this.getTransform,
+ onClick: this.props.onClick,
+ ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
renderDepth: this.props.renderDepth + 1,
- selectOnLoad: pair.layout[Id] === this._selectOnLoaded,
- PanelWidth: pair.layout[WidthSym],
- PanelHeight: pair.layout[HeightSym],
+ PanelWidth: childLayout[WidthSym],
+ PanelHeight: childLayout[HeightSym],
ContentScaling: returnOne,
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
@@ -649,6 +656,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
addDocTab: this.props.addDocTab,
+ pinToPres: this.props.pinToPres,
zoomToScale: this.zoomToScale,
getScale: this.getScale
};
@@ -660,9 +668,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
+ onClick: this.props.onClick,
ScreenToLocalTransform: this.getTransform,
renderDepth: this.props.renderDepth,
- selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
PanelWidth: layoutDoc[WidthSym],
PanelHeight: layoutDoc[HeightSym],
ContentScaling: returnOne,
@@ -673,6 +681,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
addDocTab: this.props.addDocTab,
+ pinToPres: this.props.pinToPres,
zoomToScale: this.zoomToScale,
getScale: this.getScale
};
@@ -723,6 +732,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed.struct
get elements() {
+ if (this.Document.usePivotLayout) return PivotView.elements(this);
let curPage = FieldValue(this.Document.curPage, -1);
const initScript = this.Document.arrangeInit;
const script = this.Document.arrangeScript;
@@ -746,27 +756,26 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) :
{ x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") };
state = pos.state === undefined ? state : pos.state;
- prev.push({
- ele: <CollectionFreeFormDocumentView key={doc[Id]}
- x={script ? pos.x : undefined} y={script ? pos.y : undefined}
- width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(doc)} />,
- bounds: (pos.x !== undefined && pos.y !== undefined) ? { x: pos.x, y: pos.y, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) } : undefined
- });
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc);
+ if (pair.layout && !(pair.data instanceof Promise)) {
+ prev.push({
+ ele: <CollectionFreeFormDocumentView key={doc[Id]}
+ x={script ? pos.x : undefined} y={script ? pos.y : undefined}
+ width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+ bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) }
+ });
+ }
}
}
return prev;
}, elements);
- this.resetSelectOnLoaded();
-
return docviews;
}
- resetSelectOnLoaded = () => setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
-
@computed.struct
get views() {
- let source = this.Document.usePivotLayout === true ? PivotView.elements(this) : this.elements;
+ let source = this.elements;
return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
}
@computed.struct
@@ -819,46 +828,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
onContextMenu = (e: React.MouseEvent) => {
let layoutItems: ContextMenuProps[] = [];
layoutItems.push({
- description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`,
- event: this.fitToContainer,
- icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt"
- });
- layoutItems.push({
- description: "reset view", event: () => {
- this.props.Document.panX = this.props.Document.panY = 0;
- this.props.Document.scale = 1;
- }, icon: "compress-arrows-alt"
- });
- layoutItems.push({
- description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
- event: async () => {
- Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
- Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
- this.props.Document.useClusters = !this.props.Document.useClusters;
- },
- icon: !this.props.Document.useClusters ? "braille" : "braille"
- });
- layoutItems.push({
- description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
- event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
- icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
- });
- layoutItems.push({
- description: "Arrange contents in grid",
- event: this.arrangeContents,
- icon: "table"
- });
- ContextMenu.Instance.addItem({
- description: "Layout...",
- subitems: layoutItems,
- icon: "compass"
- });
- ContextMenu.Instance.addItem({
- description: "Analyze Strokes",
- event: this.analyzeStrokes,
- icon: "paint-brush"
- });
- ContextMenu.Instance.addItem({
description: "Import document", icon: "upload", event: () => {
const input = document.createElement("input");
input.type = "file";
@@ -882,7 +851,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY);
doc.x = x, doc.y = y;
- this.addDocument(doc, false);
+ this.props.addDocument &&
+ this.props.addDocument(doc, false);
};
input.click();
}
@@ -908,6 +878,25 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
},
icon: "brain"
});
+ layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
+ layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ layoutItems.push({
+ description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
+ event: async () => {
+ Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
+ Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
+ this.props.Document.useClusters = !this.props.Document.useClusters;
+ },
+ icon: !this.props.Document.useClusters ? "braille" : "braille"
+ });
+ layoutItems.push({
+ description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
+ event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
+ icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
+ });
+ layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
+ layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+ ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index aad26efa0..27eafd769 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -203,7 +203,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
onClick = (e: React.MouseEvent): void => {
if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress);
+ PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
// let the DocumentView stopPropagation of this event when it selects this document
} else { // why do we get a click event when the cursor have moved a big distance?
// let's cut it off here so no one else has to deal with it.
@@ -373,7 +373,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
marqueeSelect(selectBackgrounds: boolean = true) {
let selRect = this.Bounds;
let selection: Doc[] = [];
- this.props.activeDocuments().filter(doc => !doc.isBackground).map(doc => {
+ this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
var x = NumCast(doc.x);
var y = NumCast(doc.y);
var w = NumCast(doc.width);
@@ -383,7 +383,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
});
if (!selection.length && selectBackgrounds) {
- this.props.activeDocuments().map(doc => {
+ this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
var x = NumCast(doc.x);
var y = NumCast(doc.y);
var w = NumCast(doc.width);
@@ -393,6 +393,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
});
}
+ if (!selection.length) {
+ let left = this._downX < this._lastX ? this._downX : this._lastX;
+ let top = this._downY < this._lastY ? this._downY : this._lastY;
+ let topLeft = this.props.getContainerTransform().transformPoint(left, top);
+ let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width);
+ var h = NumCast(doc.height);
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
+ selection.push(doc);
+ }
+ });
+ }
return selection;
}
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss
index 92beafa15..75a790667 100644
--- a/src/client/views/nodes/ButtonBox.scss
+++ b/src/client/views/nodes/ButtonBox.scss
@@ -3,10 +3,29 @@
height: 100%;
pointer-events: all;
border-radius: inherit;
+ display:flex;
+ flex-direction: column;
}
.buttonBox-mainButton {
width: 100%;
height: 100%;
border-radius: inherit;
+ text-align: center;
+ display: table;
+}
+.buttonBox-mainButtonCenter {
+ height: 100%;
+ display:table-cell;
+ vertical-align: middle;
+}
+
+.buttonBox-params {
+ display:flex;
+ flex-direction: row;
+}
+
+.buttonBox-missingParam {
+ width:100%;
+ background: lightgray;
} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 640795789..54848344b 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -1,20 +1,19 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
import * as React from 'react';
-import { FieldViewProps, FieldView } from './FieldView';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
import { ScriptField } from '../../../new_fields/ScriptField';
+import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { DragManager } from '../../util/DragManager';
+import { undoBatch } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
-import { ContextMenu } from '../ContextMenu';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit } from '@fortawesome/free-regular-svg-icons';
-import { emptyFunction } from '../../../Utils';
-import { ScriptBox } from '../ScriptBox';
-import { CompileScript } from '../../util/Scripting';
-import { OverlayView } from '../OverlayView';
-import { Doc } from '../../../new_fields/Doc';
-
import './ButtonBox.scss';
-import { observer } from 'mobx-react';
-import { DocumentIconContainer } from './DocumentIcon';
+import { FieldView, FieldViewProps } from './FieldView';
+
library.add(faEdit as any);
@@ -29,48 +28,42 @@ const ButtonDocument = makeInterface(ButtonSchema);
@observer
export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+ private dropDisposer?: DragManager.DragDropDisposer;
+
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
- onClick = (e: React.MouseEvent) => {
- const onClick = this.Document.onClick;
- if (!onClick) {
- return;
+
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
- e.stopPropagation();
- e.preventDefault();
- onClick.script.run({ this: this.props.Document });
}
-
- onContextMenu = () => {
- ContextMenu.Instance.addItem({
- description: "Edit OnClick script", icon: "edit", event: () => {
- let overlayDisposer: () => void = emptyFunction;
- const script = this.Document.onClick;
- let originalText: string | undefined = undefined;
- if (script) originalText = script.script.originalScript;
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
- const script = CompileScript(text, {
- params: { this: Doc.name },
- typecheck: false,
- editable: true,
- transformer: DocumentIconContainer.getTransformer()
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
- this.Document.onClick = new ScriptField(script);
- overlayDisposer();
- }} showDocumentIcons />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnClick` });
- }
- });
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ Doc.GetProto(this.dataDoc).source = new List<Doc>(de.data.droppedDocuments);
+ e.stopPropagation();
+ }
}
-
+ // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
+ let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
+ params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ...
return (
- <div className="buttonBox-outerDiv" onContextMenu={this.onContextMenu}>
- <button className="buttonBox-mainButton" onClick={this.onClick}>{this.Document.text || this.Document.title || "Button"}</button>
+ <div className="buttonBox-outerDiv" ref={this.createDropTarget} >
+ <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+ <div className="buttonBox-mainButtonCenter">
+ {(this.Document.text || this.Document.title)}
+ </div>
+ </div>
+ <div className="buttonBox-params" >
+ {!missingParams || !missingParams.length ? (null) : missingParams.map(m => <div key={m} className="buttonBox-missingParam">{m}</div>)}
+ </div>
</div>
);
}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index ee596c841..7631ecc6c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -83,7 +83,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transformOrigin: "left top",
position: "absolute",
backgroundColor: "transparent",
- boxShadow: this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
+ boxShadow: this.props.Document.opacity === 0 ? undefined : this.props.Document.z ? `#9c9396 ${StrCast(this.props.Document.boxShadow, "10px 10px 0.9vw")}` :
this.clusterColor ? (
this.props.Document.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :
`${this.clusterColor} ${StrCast(this.props.Document.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) : undefined,
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 396233551..d77662355 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -11,7 +11,9 @@ import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
+import { DragBox } from "./DragBox";
import { ButtonBox } from "./ButtonBox";
+import { PresBox } from "./PresBox";
import { IconBox } from "./IconBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
@@ -27,7 +29,7 @@ import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { Doc } from "../../../new_fields/Doc";
import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { ScriptField } from "../../../new_fields/ScriptField";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -48,6 +50,7 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
+ onClick?: ScriptField,
layoutKey: string,
hideOnLeave?: boolean
}> {
@@ -65,7 +68,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get dataDoc() {
- if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
// if there is no dataDoc (ie, we're not rendering a template layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
@@ -80,7 +83,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
CreateBindings(): JsxBindings {
- return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: this.layoutDoc, DataDoc: this.dataDoc } };
+ let list = {
+ ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
+ Document: this.layoutDoc,
+ DataDoc: this.dataDoc
+ };
+ return { props: list };
}
@computed get templates(): List<string> {
@@ -99,10 +107,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
if (this.props.renderDepth > 7) return (null);
if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
return <ObserverJsxParser
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, YoutubeBox }}
+ blacklistedAttrs={[]}
+ components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox }}
bindings={this.CreateBindings()}
jsx={this.finalLayout}
showWarnings={true}
+
onError={(test: any) => { console.log(test); }}
/>;
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3ce4dbd4f..2a6e91272 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction, trace } from "mobx";
+import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
@@ -8,13 +8,15 @@ import { Copy, Id } from '../../../new_fields/FieldSymbols';
import { List } from "../../../new_fields/List";
import { ObjectField } from "../../../new_fields/ObjectField";
import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../new_fields/Types";
+import { ScriptField } from '../../../new_fields/ScriptField';
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { RouteStore } from '../../../server/RouteStore';
import { emptyFunction, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
import { ClientUtils } from '../../util/ClientUtils';
+import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
import { LinkManager } from '../../util/LinkManager';
@@ -29,21 +31,20 @@ import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
+import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
-import { PresentationView } from "../presentationview/PresentationView";
+import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
-import { DictationManager } from '../../util/DictationManager';
-import { MainView } from '../MainView';
import requestPromise = require('request-promise');
import { Recommendations } from '../Recommendations';
import { SearchUtil } from '../../util/SearchUtil';
import { ClientRecommender } from '../../ClientRecommender';
-import { DocumentType } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
library.add(fa.faBrain);
@@ -85,6 +86,7 @@ export interface DocumentViewProps {
Document: Doc;
DataDoc?: Doc;
fitToBox?: boolean;
+ onClick?: ScriptField;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
@@ -95,11 +97,11 @@ export interface DocumentViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
focus: (doc: Doc, willZoom: boolean, scale?: number) => void;
- selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
zoomToScale: (scale: number) => void;
backgroundColor: (doc: Doc) => string | undefined;
@@ -114,7 +116,8 @@ const schema = createSchema({
nativeHeight: "number",
backgroundColor: "string",
opacity: "number",
- hidden: "boolean"
+ hidden: "boolean",
+ onClick: ScriptField,
});
export const positionSchema = createSchema({
@@ -140,8 +143,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _lastTap: number = 0;
private _doubleTap = false;
private _hitExpander = false;
+ private _hitTemplateDrag = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
+ _animateToIconDisposer?: IReactionDisposer;
+ _reactionDisposer?: IReactionDisposer;
public get ContentDiv() { return this._mainCont.current; }
@computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@@ -156,8 +162,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
set templates(templates: List<string>) { this.props.Document.templates = templates; }
screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
- _animateToIconDisposer?: IReactionDisposer;
- _reactionDisposer?: IReactionDisposer;
@action
componentDidMount() {
if (this._mainCont.current) {
@@ -209,9 +213,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
componentDidUpdate() {
- if (this._dropDisposer) {
- this._dropDisposer();
- }
+ this._dropDisposer && this._dropDisposer();
if (this._mainCont.current) {
this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
handlers: { drop: this.drop.bind(this) }
@@ -220,9 +222,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
componentWillUnmount() {
- if (this._reactionDisposer) this._reactionDisposer();
- if (this._animateToIconDisposer) this._animateToIconDisposer();
- if (this._dropDisposer) this._dropDisposer();
+ this._reactionDisposer && this._reactionDisposer();
+ this._animateToIconDisposer && this._animateToIconDisposer();
+ this._dropDisposer && this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -231,7 +233,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
get dataDoc() {
- if (this.props.DataDoc === undefined && this.props.Document.layout instanceof Doc) {
+ if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
// if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
// has a template layout document, then we will render the template layout but use
// this document as the data document for the layout.
@@ -239,7 +241,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
}
- startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) {
if (this._mainCont.current) {
let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
@@ -250,6 +252,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
dragData.xOffset = xoff;
dragData.yOffset = yoff;
dragData.moveDocument = this.props.moveDocument;
+ dragData.applyAsTemplate = applyAsTemplate;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
dragComplete: action(emptyFunction)
@@ -296,10 +299,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onClick = async (e: React.MouseEvent) => {
if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
- e.stopPropagation();
+ if (this.onClickHandler && this.onClickHandler.script) {
+ e.stopPropagation();
+ this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document });
+ e.preventDefault();
+ return;
+ }
let altKey = e.altKey;
let ctrlKey = e.ctrlKey;
if (this._doubleTap && this.props.renderDepth) {
+ e.stopPropagation();
let fullScreenAlias = Doc.MakeAlias(this.props.Document);
fullScreenAlias.templates = new List<string>();
Doc.UseDetailLayout(fullScreenAlias);
@@ -311,10 +320,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
+ if (BoolCast(this.props.Document.ignoreClick)) {
+ return;
+ }
+ e.stopPropagation();
SelectionManager.SelectDoc(this, e.ctrlKey);
let isExpander = (e.target as any).id === "isExpander";
- if (BoolCast(this.props.Document.isButton) || isExpander) {
- SelectionManager.DeselectAll();
+ if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) {
let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
@@ -325,6 +337,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
// let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ SelectionManager.DeselectAll();
let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
if (altKey || ctrlKey) {
@@ -353,6 +366,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
else if (linkedDocs.length) {
+ SelectionManager.DeselectAll();
let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
@@ -375,28 +389,35 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
}
+
+
onPointerDown = (e: React.PointerEvent): void => {
+ if (e.nativeEvent.cancelBubble) return;
this._downX = e.clientX;
this._downY = e.clientY;
this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
- // if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
- // CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]);
- // e.stopPropagation();
- // } else {
+ this._hitTemplateDrag = false;
+ for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
+ if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
+ this._hitTemplateDrag = true;
+ }
+ }
if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointerup", this.onPointerUp);
- // }
}
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && this.active) {
+ if (e.cancelBubble && this.active) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
+ else if (!e.cancelBubble && this.active) {
if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -454,6 +475,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`);
}
+ if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc);
+ e.stopPropagation();
+ }
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
@@ -568,36 +593,32 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
- cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
- cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
- cm.addItem({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
- cm.addItem({ description: "Transcribe Speech", event: this.listen, icon: "microphone" });
- let makes: ContextMenuProps[] = [];
- makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Make Background", event: this.makeBackground, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
- makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
+ let existingMake = ContextMenu.Instance.findByDescription("Make...");
+ let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
+ makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" });
+ makes.push({ description: this.props.Document.isButton ? "Remove Button" : "Into Button", event: this.makeBtnClicked, icon: "concierge-bell" });
+ makes.push({ description: "OnClick script", icon: "edit", event: () => ScriptBox.EditClickScript(this.props.Document, "onClick") });
makes.push({
- description: "Make Portal", event: () => {
+ description: "Into Portal", event: () => {
let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
- //Doc.GetProto(this.props.Document).subBulletDocs = new List<Doc>([portal]);
- //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
- //Doc.GetProto(this.props.Document).templates = new List<string>([Templates.Bullet.Layout]);
- //let coll = Docs.Create.StackingDocument([this.props.Document, portal], { x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y), width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".cont" });
- //this.props.addDocument && this.props.addDocument(coll);
- //this.props.removeDocument && this.props.removeDocument(this.props.Document);
DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal");
this.makeBtnClicked();
-
}, icon: "window-restore"
});
- cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
- if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
- cm.addItem({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
- }
- cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
+ makes.push({ description: this.props.Document.ignoreClick ? "Selectable" : "Unselectable", event: () => this.props.Document.ignoreClick = !this.props.Document.ignoreClick, icon: this.props.Document.ignoreClick ? "unlock" : "lock" });
+ !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
let existing = ContextMenu.Instance.findByDescription("Layout...");
let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+
+ layoutItems.push({ description: `${this.props.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.props.Document.chromeStatus = (this.props.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+ layoutItems.push({ description: `${this.props.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.props.Document.autoHeight = !this.props.Document.autoHeight, icon: "plus" });
+ layoutItems.push({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
+ layoutItems.push({ description: BoolCast(this.props.Document.lockedPosition) ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.props.Document.lockedPosition) ? "unlock" : "lock" });
layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+ if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
+ layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
+ }
!existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
if (!ClientUtils.RELEASE) {
let copies: ContextMenuProps[] = [];
@@ -605,6 +626,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
}
+ let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
+ let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
+ analyzers.push({ description: "Transcribe Speech", event: this.listen, icon: "microphone" });
+ !existingAnalyze && cm.addItem({ description: "Analyzers...", subitems: analyzers, icon: "hand-point-right" });
+ cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle!
+ cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
+ cm.addItem({ description: "Move To Overlay", icon: "laptop-code", event: () => ((o: Doc) => o && Doc.AddDocToList(o, "data", this.props.Document))(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc) });
cm.addItem({
description: "Download document", icon: "download", event: () => {
const a = document.createElement("a");
@@ -647,7 +675,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
try {
let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
const users: User[] = JSON.parse(stuff);
- usersMenu = users.filter(({ email }) => email !== CurrentUserUtils.email).map(({ email, userDocumentId }) => ({
+ usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({
description: email, event: async () => {
const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
if (!userDocument) {
@@ -670,6 +698,30 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
runInAction(() => {
cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
+ if (!ClientUtils.RELEASE) {
+ let setWriteMode = (mode: DocServer.WriteMode) => {
+ DocServer.AclsMode = mode;
+ const mode1 = mode;
+ const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
+ DocServer.setFieldWriteMode("x", mode1);
+ DocServer.setFieldWriteMode("y", mode1);
+ DocServer.setFieldWriteMode("width", mode1);
+ DocServer.setFieldWriteMode("height", mode1);
+
+ DocServer.setFieldWriteMode("panX", mode2);
+ DocServer.setFieldWriteMode("panY", mode2);
+ DocServer.setFieldWriteMode("scale", mode2);
+ DocServer.setFieldWriteMode("viewType", mode2);
+ };
+ let aclsMenu: ContextMenuProps[] = [];
+ aclsMenu.push({ description: "Default (write/read all)", event: () => setWriteMode(DocServer.WriteMode.Default), icon: DocServer.AclsMode === DocServer.WriteMode.Default ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Playground (write own/no read)", event: () => setWriteMode(DocServer.WriteMode.Playground), icon: DocServer.AclsMode === DocServer.WriteMode.Playground ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Live Playground (write own/read others)", event: () => setWriteMode(DocServer.WriteMode.LivePlayground), icon: DocServer.AclsMode === DocServer.WriteMode.LivePlayground ? "check" : "exclamation" });
+ aclsMenu.push({ description: "Live Readonly (no write/read others)", event: () => setWriteMode(DocServer.WriteMode.LiveReadonly), icon: DocServer.AclsMode === DocServer.WriteMode.LiveReadonly ? "check" : "exclamation" });
+ cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" });
+ cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
+ }
+
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
@@ -686,14 +738,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
isSelected = () => SelectionManager.IsSelected(this);
@action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
-
@computed get nativeWidth() { return this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
@computed get contents() {
return (<DocumentContentsView {...this.props}
ChromeHeight={this.chromeHeight}
- isSelected={this.isSelected} select={this.select}
- selectOnLoad={this.props.selectOnLoad}
+ isSelected={this.isSelected}
+ select={this.select}
+ onClick={this.onClickHandler}
layoutKey={"layout"}
fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
DataDoc={this.dataDoc} />);
@@ -702,7 +755,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
chromeHeight = () => {
let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
- return showTitle ? 25 : 0;
+ let templates = Cast(this.layoutDoc.templates, listSpec("string"));
+ if (!showOverlays && templates instanceof List) {
+ templates.map(str => {
+ if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
+ });
+ }
+ return (showTitle ? 25 : 0) + 1;// bcz: why 8??
}
get layoutDoc() {
@@ -711,6 +770,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
}
+
render() {
let documents = [{ preview: "hi", similarity: 0 }];
let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ?
@@ -730,7 +790,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
- let brushDegree = Doc.IsBrushedDegree(this.layoutDoc);
+ let brushDegree = Doc.IsBrushedDegree(this.props.Document);
+ let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding);
+ let localScale = this.props.ScreenToLocalTransform().Scale * brushDegree;
return (
<div>
<div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/DragBox.scss
new file mode 100644
index 000000000..fbb9b9c1c
--- /dev/null
+++ b/src/client/views/nodes/DragBox.scss
@@ -0,0 +1,13 @@
+.dragBox-outerDiv {
+ width: 100%;
+ height: 100%;
+ pointer-events: all;
+ border-radius: inherit;
+ background: black;
+ border-radius: 100%;
+ svg {
+ margin:18%;
+ width:65% !important;
+ height:65%;
+ }
+}
diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx
new file mode 100644
index 000000000..1f2c88086
--- /dev/null
+++ b/src/client/views/nodes/DragBox.tsx
@@ -0,0 +1,101 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../../new_fields/Doc';
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { ScriptField } from '../../../new_fields/ScriptField';
+import { emptyFunction } from '../../../Utils';
+import { CompileScript } from '../../util/Scripting';
+import { ContextMenu } from '../ContextMenu';
+import { DocComponent } from '../DocComponent';
+import { OverlayView } from '../OverlayView';
+import { ScriptBox } from '../ScriptBox';
+import { DocumentIconContainer } from './DocumentIcon';
+import './DragBox.scss';
+import { FieldView, FieldViewProps } from './FieldView';
+import { DragManager } from '../../util/DragManager';
+import { Docs } from '../../documents/Documents';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+
+library.add(faEdit as any);
+
+const DragSchema = createSchema({
+ onDragStart: ScriptField,
+ text: "string"
+});
+
+type DragDocument = makeInterface<[typeof DragSchema]>;
+const DragDocument = makeInterface(DragSchema);
+@observer
+export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocument) {
+ _downX: number = 0;
+ _downY: number = 0;
+ public static LayoutString() { return FieldView.LayoutString(DragBox); }
+ _mainCont = React.createRef<HTMLDivElement>();
+ onDragStart = (e: React.PointerEvent) => {
+ if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) {
+ document.removeEventListener("pointermove", this.onDragMove);
+ document.addEventListener("pointermove", this.onDragMove);
+ document.removeEventListener("pointerup", this.onDragUp);
+ document.addEventListener("pointerup", this.onDragUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ onDragMove = (e: MouseEvent) => {
+ if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
+ document.removeEventListener("pointermove", this.onDragMove);
+ document.removeEventListener("pointerup", this.onDragUp);
+ const onDragStart = this.Document.onDragStart;
+ e.stopPropagation();
+ e.preventDefault();
+ let res = onDragStart ? onDragStart.script.run({ this: this.props.Document }) : undefined;
+ let doc = res !== undefined && res.success ?
+ res.result as Doc :
+ Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
+ doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY);
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onDragUp = (e: MouseEvent) => {
+ document.removeEventListener("pointermove", this.onDragMove);
+ document.removeEventListener("pointerup", this.onDragUp);
+ }
+
+ onContextMenu = () => {
+ ContextMenu.Instance.addItem({
+ description: "Edit OnClick script", icon: "edit", event: () => {
+ let overlayDisposer: () => void = emptyFunction;
+ const script = this.Document.onDragStart;
+ let originalText: string | undefined = undefined;
+ if (script) originalText = script.script.originalScript;
+ // tslint:disable-next-line: no-unnecessary-callback-wrapper
+ let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+ const script = CompileScript(text, {
+ params: { this: Doc.name },
+ typecheck: false,
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer()
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
+ this.Document.onClick = new ScriptField(script);
+ overlayDisposer();
+ }} showDocumentIcons />;
+ overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` });
+ }
+ });
+ }
+
+ render() {
+ return (<div className="dragBox-outerDiv" onContextMenu={this.onContextMenu} onPointerDown={this.onDragStart} ref={this._mainCont}>
+ <FontAwesomeIcon className="dragBox-icon" icon="folder" size="lg" color="white" />
+ </div>);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index da54ecc3a..d9774303b 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -17,7 +17,7 @@ import { IconBox } from "./IconBox";
import { ImageBox } from "./ImageBox";
import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { ScriptField } from "../../../new_fields/ScriptField";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -32,12 +32,13 @@ export interface FieldViewProps {
ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
Document: Doc;
DataDoc?: Doc;
+ onClick?: ScriptField;
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
- selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -56,6 +57,7 @@ export interface FieldViewProps {
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
+ //"<ImageBox {...props} />"
}
@computed
@@ -77,6 +79,9 @@ export class FieldView extends React.Component<FieldViewProps> {
else if (field instanceof ImageField) {
return <ImageBox {...this.props} leaveNativeSize={true} />;
}
+ // else if (field instaceof PresBox) {
+ // return <PresBox {...this.props} />;
+ // }
else if (field instanceof IconField) {
return <IconBox {...this.props} />;
}
@@ -102,7 +107,6 @@ export class FieldView extends React.Component<FieldViewProps> {
// PanelWidth={returnHundred}
// PanelHeight={returnHundred}
// renderDepth={0} //TODO Why is renderDepth reset?
- // selectOnLoad={false}
// focus={emptyFunction}
// isSelected={this.props.isSelected}
// select={returnFalse}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 247f7d1ea..03e81bfca 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -33,12 +33,12 @@
}
.formattedTextBox-inner-rounded {
- height: calc(100% - 25px);
- width: calc(100% - 40px);
+ height: 70%;
+ width: 85%;
position: absolute;
overflow: auto;
- top: 15;
- left: 20;
+ top: 15%;
+ left: 10%;
}
.formattedTextBox-inner-rounded div,
@@ -65,4 +65,37 @@
.em {
font-style: italic;
-} \ No newline at end of file
+}
+
+.userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+}
+.userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width:10px;
+ min-height:10px;
+ text-align:center;
+ align-content: center;
+}
+
+ol { counter-reset: deci 0;}
+.decimal-ol { counter-reset: deci 0; p { display: inline }; font-size: 24 }
+.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
+.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
+.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
+.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
+.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
+.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
+.decimal:before { content: counter(deci) " "; counter-increment: deci; display:inline-block; width: 30}
+.decimal2:before { content: counter(deci) "." counter(deci2) " "; counter-increment: deci2; display:inline-block; width: 35}
+.decimal3:before { content: counter(deci) "." counter(deci2) "." counter(deci3) " "; counter-increment: deci3; display:inline-block; width: 35}
+.decimal4:before { content: counter(deci) "." counter(deci2) "." counter(deci3) "." counter(deci4) " "; counter-increment: deci4; display:inline-block; width: 40}
+.upper-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) " "; counter-increment: ualph; display:inline-block; width: 35 }
+.lower-roman:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) " "; counter-increment: lroman;display:inline-block; width: 50 }
+.lower-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha)" "; counter-increment: lalpha; display:inline-block; width: 35}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 44b5d2c21..146281f2b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,20 +1,21 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faSmile, faTextHeight } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed, Lambda, trace } from "mobx";
+import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { Node as ProsNode } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, Selection } from "prosemirror-state";
-import { NodeType, Slice, Node, Fragment } from 'prosemirror-model';
+import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
+import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { Doc, Opt, DocListCast } from "../../../new_fields/Doc";
-import { Id, Copy } from '../../../new_fields/FieldSymbols';
+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 } from "../../../new_fields/RichTextField";
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { RichTextField, ToPlainText, FromPlainText } 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';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
@@ -26,24 +27,22 @@ import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { ContextMenu } from "../../views/ContextMenu";
-import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { InkingControl } from "../InkingControl";
-import { Templates } from '../Templates';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import React = require("react");
-import { For } from 'babel-types';
-import { DateField } from '../../../new_fields/DateField';
-import { Utils } from '../../../Utils';
-import { MainOverlayTextBox } from '../MainOverlayTextBox';
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { DictationManager } from '../../util/DictationManager';
+import { ReplaceStep } from 'prosemirror-transform';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { number } from 'prop-types';
library.add(faEdit);
-library.add(faSmile, faTextHeight);
+library.add(faSmile, faTextHeight, faUpload);
-// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
-//
+export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
export interface FormattedTextBoxProps {
isOverlay?: boolean;
@@ -58,31 +57,37 @@ const richTextSchema = createSchema({
documentText: "string"
});
+export const GoogleRef = "googleDocId";
+
type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
+type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, 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 Instance: FormattedTextBox;
- private _ref: React.RefObject<HTMLDivElement>;
- private _outerdiv?: (dominus: HTMLElement) => void;
+ private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
private _linkClicked = "";
+ private _undoTyping?: UndoManager.Batch;
private _reactionDisposer: Opt<IReactionDisposer>;
private _searchReactionDisposer?: Lambda;
private _textReactionDisposer: Opt<IReactionDisposer>;
+ private _heightReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ private _pullReactionDisposer: Opt<IReactionDisposer>;
+ private _pushReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @observable _entered = false;
+ @observable private _entered = false;
@observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static SelectOnLoad = "";
public static InputBoxOverlayScroll: number = 0;
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
@@ -111,7 +116,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@undoBatch
public setFontColor(color: string) {
- let self = this;
if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
this.props.Document.color = color;
@@ -124,21 +128,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
constructor(props: FieldViewProps) {
super(props);
- //if (this.props.firstinstance) {
- FormattedTextBox.Instance = this;
- //}
- if (this.props.outer_div) {
- this._outerdiv = this.props.outer_div;
- }
-
- this._ref = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
-
- document.addEventListener("paste", this.paste);
}
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
@computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
@computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
@@ -146,42 +142,45 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
paste = (e: ClipboardEvent) => {
if (e.clipboardData && this._editorView) {
- let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
- for (let i = 0; i < e.clipboardData.items.length; i++) {
- let item = e.clipboardData.items.item(i);
- if (item.type === "text/plain") {
- item.getAsString((text) => {
- let pdfPasteIndex = text.indexOf(pdfPasteText);
- if (pdfPasteIndex > -1) {
- let insertText = text.substr(0, pdfPasteIndex);
- const tx = this._editorView!.state.tr.insertText(insertText);
- // tx.setSelection(new Selection(tx.))
- const state = this._editorView!.state;
- this._editorView!.dispatch(tx);
- if (FormattedTextBox._toolTipTextMenu) {
- // this._toolTipTextMenu.makeLinkWithState(state)
- }
- e.stopPropagation();
- e.preventDefault();
- }
- });
- }
- }
+ // let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
+ // for (let i = 0; i < e.clipboardData.items.length; i++) {
+ // let item = e.clipboardData.items.item(i);
+ // console.log(item)
+ // if (item.type === "text/plain") {
+ // console.log("plain")
+ // item.getAsString((text) => {
+ // let pdfPasteIndex = text.indexOf(pdfPasteText);
+ // if (pdfPasteIndex > -1) {
+ // let insertText = text.substr(0, pdfPasteIndex);
+ // const tx = this._editorView!.state.tr.insertText(insertText);
+ // // tx.setSelection(new Selection(tx.))
+ // const state = this._editorView!.state;
+ // this._editorView!.dispatch(tx);
+ // if (FormattedTextBox._toolTipTextMenu) {
+ // // this._toolTipTextMenu.makeLinkWithState(state)
+ // }
+ // e.stopPropagation();
+ // e.preventDefault();
+ // }
+ // });
+ // }
+ // }
}
}
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
+ FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
this._editorView.updateState(state);
- if (state.selection.empty && FormattedTextBox._toolTipTextMenu) {
- const marks = tx.storedMarks;
- if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
+ FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
+ if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
+ FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
+
this._applyingChange = true;
- const fieldkey = "preview";
- if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
- if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
+ this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
let title = StrCast(this.dataDoc.title);
@@ -195,7 +194,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
public highlightSearchTerms = (terms: String[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const fieldkey = "preview";
const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
@@ -206,9 +204,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
tokens.forEach((word) => {
if (terms.includes(word) && this._editorView) {
this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
- // else {
- // this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark);
- // }
}
start += word.length + 1;
});
@@ -239,12 +234,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
+ this.dropDisposer && this.dropDisposer();
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@undoBatch
@@ -257,36 +248,71 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
e.stopPropagation();
- } else {
- if (de.data instanceof DragManager.DocumentDragData) {
- this.props.Document.layout = de.data.draggedDocuments[0];
- de.data.draggedDocuments[0].isTemplate = true;
+ } else if (de.data instanceof DragManager.DocumentDragData) {
+ const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
+ if (draggedDoc && draggedDoc.type === DocumentType.TEXT && StrCast(draggedDoc.layout) !== "") {
+ this.props.Document.layout = draggedDoc;
+ draggedDoc.isTemplate = true;
e.stopPropagation();
- // let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
- // if (!ldocs) {
- // this.dataDoc.subBulletDocs = new List<Doc>([]);
- // }
- // ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
- // if (!ldocs) return;
- // if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) {
- // ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
- // this.props.addDocument && this.props.addDocument(ldocs[0] as Doc);
- // this.props.Document.templates = new List<string>([Templates.Bullet.Layout]);
- // this.props.Document.isBullet = true;
- // }
- // let stackDoc = (ldocs[0] as Doc);
- // if (de.data.moveDocument) {
- // de.data.moveDocument(de.data.draggedDocuments[0], stackDoc, (doc) => {
- // Cast(stackDoc.data, listSpec(Doc))!.push(doc);
- // return true;
- // });
- // }
}
}
}
- componentDidMount() {
- const config = {
+ recordKeyHandler = (e: KeyboardEvent) => {
+ if (this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
+ if (e.key === "R" && e.altKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.recordBullet();
+ }
+ }
+ }
+
+ recordBullet = async () => {
+ let completedCue = "end session";
+ let results = await DictationManager.Controls.listen({
+ interimHandler: this.setCurrentBulletContent,
+ continuous: { indefinite: false },
+ terminators: [completedCue, "bullet", "next"]
+ });
+ if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) {
+ DictationManager.Controls.stop();
+ return;
+ }
+ this.nextBullet(this._editorView!.state.selection.to);
+ setTimeout(this.recordBullet, 2000);
+ }
+
+ setCurrentBulletContent = (value: string) => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let from = state.selection.from;
+ let to = state.selection.to;
+ this._editorView.dispatch(state.tr.insertText(value, from, to));
+ state = this._editorView.state;
+ let updated = TextSelection.create(state.doc, from, from + value.length);
+ this._editorView.dispatch(state.tr.setSelection(updated));
+ }
+ }
+
+ nextBullet = (pos: number) => {
+ if (this._editorView) {
+ let frag = Fragment.fromArray(this.newListItems(2));
+ let slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
+ }
+
+ private newListItems = (count: number) => {
+ return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
+ }
+
+ @computed get config() {
+ return {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
plugins: this.props.isOverlay ? [
@@ -306,6 +332,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
keymap(baseKeymap),
]
};
+ }
+
+ @action
+ rebuildEditor() {
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
+ }
+
+ componentDidMount() {
+ document.addEventListener("paste", this.paste);
if (!this.props.isOverlay) {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
@@ -317,17 +352,49 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, { fireImmediately: true });
}
+ this.pullFromGoogleDoc(this.checkState);
+ this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
+
this._reactionDisposer = reaction(
() => {
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}}`;
+ return field ? field.Data : Blank;
},
- 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(this.config, updatedState));
+ this.tryUpdateHeight();
+ }
+ }
+ );
+
+ this._pullReactionDisposer = reaction(
+ () => this.props.Document[Pulls],
+ () => {
+ if (!DocumentDecorations.hasPulledHack) {
+ DocumentDecorations.hasPulledHack = true;
+ let unchanged = this.dataDoc.unchanged;
+ this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
+ }
+ }
+ );
+
+ this._pushReactionDisposer = reaction(
+ () => this.props.Document[Pushes],
+ () => {
+ if (!DocumentDecorations.hasPushedHack) {
+ DocumentDecorations.hasPushedHack = true;
+ this.pushToGoogleDoc();
+ }
}
);
+ this._heightReactionDisposer = reaction(
+ () => this.props.Document[WidthSym](),
+ () => this.tryUpdateHeight()
+ );
+
this._textReactionDisposer = reaction(
() => this.extensionDoc,
() => {
@@ -338,7 +405,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc.lastModified = undefined;
}
}, { fireImmediately: true });
- this.setupEditor(config, this.dataDoc, this.props.fieldKey);
+
+
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.search_string);
@@ -355,6 +424,87 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.unhighlightSearchTerms();
}
}, { fireImmediately: true });
+ setTimeout(() => this.tryUpdateHeight(), 0);
+ }
+
+ pushToGoogleDoc = async () => {
+ this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ let modes = GoogleApiClientUtils.WriteMode;
+ let mode = modes.Replace;
+ let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ if (!reference) {
+ mode = modes.Insert;
+ 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]();
+ let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ response && (this.dataDoc[GoogleRef] = response.documentId);
+ let pushSuccess = response !== undefined && !("errors" in response);
+ dataDoc.unchanged = pushSuccess;
+ DocumentDecorations.Instance.startPushOutcome(pushSuccess);
+ }
+ };
+ let undo = () => {
+ let content = exportState.body;
+ if (reference && content) {
+ GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ }
+ };
+ UndoManager.AddEvent({ undo, redo });
+ redo();
+ });
+ }
+
+ pullFromGoogleDoc = async (handler: PullHandler) => {
+ let dataDoc = this.dataDoc;
+ let documentId = StrCast(dataDoc[GoogleRef]);
+ let exportState: GoogleApiClientUtils.ReadResult = {};
+ if (documentId) {
+ exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+ }
+ UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
+ }
+
+ updateState = (exportState: 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));
+ 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);
+ }
+ }
}
clipboardTextSerializer = (slice: Slice): string => {
@@ -456,12 +606,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
if (this._proseRef) {
+ this._editorView && this._editorView.destroy();
this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
- star(node, view, getPos) { return new SummarizedView(node, view, getPos); }
+ star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
@@ -472,21 +623,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- if (this.props.selectOnLoad) {
- if (!this.props.isOverlay) this.props.select(false);
- else this._editorView!.focus();
- this.tryUpdateHeight();
+ if (this.props.Document[Id] === FormattedTextBox.SelectOnLoad) {
+ FormattedTextBox.SelectOnLoad = "";
+ this.props.select(false);
}
+ else if (this.props.isOverlay) this._editorView!.focus();
+ var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
+ let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })];
+ this._editorView!.state.storedMarks = newMarks;
+
}
componentWillUnmount() {
- this._editorView && this._editorView.destroy();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
this._textReactionDisposer && this._textReactionDisposer();
+ this._pushReactionDisposer && this._pushReactionDisposer();
+ this._pullReactionDisposer && this._pullReactionDisposer();
+ this._heightReactionDisposer && this._heightReactionDisposer();
+ this._searchReactionDisposer && this._searchReactionDisposer();
+ document.removeEventListener("paste", this.paste);
}
onPointerDown = (e: React.PointerEvent): void => {
+ if (this.props.onClick && e.button === 0) {
+ e.preventDefault();
+ }
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
@@ -544,9 +706,47 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
e.preventDefault();
}
}
+
+ findUserMark(marks: Mark[]) {
+ return marks.find(m => m.attrs && m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+ }
+ findStartOfMark(rpos: ResolvedPos) {
+ let before = 0;
+ let nbef = rpos.nodeBefore;
+ while (nbef && this.findUserMark(nbef.marks)) {
+ before += nbef.nodeSize;
+ rpos = this._editorView!.state.doc.resolve(rpos.pos - nbef.nodeSize);
+ rpos && (nbef = rpos.nodeBefore);
+ }
+ return before;
+ }
+ findEndOfMark(rpos: ResolvedPos) {
+ let after = 0;
+ let naft = rpos.nodeAfter;
+ while (naft && this.findUserMark(naft.marks)) {
+ after += naft.nodeSize;
+ rpos = this._editorView!.state.doc.resolve(rpos.pos + naft.nodeSize);
+ rpos && (naft = rpos.nodeAfter);
+ }
+ return after;
+ }
+
onPointerUp = (e: React.PointerEvent): void => {
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "1";
+ let view = this._editorView!;
+ const pos = view.posAtCoords({ left: e.clientX, top: e.clientY });
+ const rpos = pos && view.state.doc.resolve(pos.pos);
+ if (pos && rpos && view.state.selection.$from === view.state.selection.$to) {
+ let nbef = this.findStartOfMark(rpos);
+ let naft = this.findEndOfMark(rpos);
+ const spos = view.state.doc.resolve(pos.pos - nbef);
+ const epos = view.state.doc.resolve(pos.pos + naft);
+ let ts = new TextSelection(spos, epos);
+ let child = rpos.nodeBefore;
+ let mark = child && this.findUserMark(child.marks);
+ if (mark && child && nbef && naft) {
+ let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: e.button === 2 ? Doc.CurrentUserEmail : mark.attrs.userid, opened: e.button === 2 ? false : !mark.attrs.opened });
+ view.dispatch(view.state.tr.setSelection(ts).removeMark(ts.from, ts.to, nmark).addMark(ts.from, ts.to, nmark).setSelection(new TextSelection(epos, epos)));
+ }
}
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
@@ -555,6 +755,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@action
onFocused = (e: React.FocusEvent): void => {
+ document.removeEventListener("keypress", this.recordKeyHandler);
+ document.addEventListener("keypress", this.recordKeyHandler);
+ this.tryUpdateHeight();
if (!this.props.isOverlay) {
FormattedTextBox.InputBoxOverlay = this;
} else {
@@ -592,7 +795,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
- //this.props.Document.tooltip = self._toolTipTextMenu;
}
tooltipLinkingMenuPlugin() {
@@ -604,18 +806,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
onBlur = (e: any) => {
+ document.removeEventListener("keypress", this.recordKeyHandler);
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
}
}
- public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
}
e.stopPropagation();
- if (e.key === "Tab") e.preventDefault();
+ if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used. sometimes backspcae, so maybe that should be added.
+ e.preventDefault();
+ setTimeout(() => { // force re-rendering of bullet numbers that may have had their bullet labels change. (Our prosemirrior code re-"marks" the changed bullets, but nothing causes the Dom to be re-rendered which is where the nubering takes place)
+ SelectionManager.DeselectAll();
+ SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
+ }, 0);
+ }
+ var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
+ let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })];
+ this._editorView!.state.storedMarks = newMarks;
+
// stop propagation doesn't seem to stop propagation of native keyboard events.
// so we set a flag on the native event that marks that the event's been handled.
(e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
@@ -632,36 +844,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@action
tryUpdateHeight() {
- if (this.props.isOverlay && this.props.Document.autoHeight) {
- let self = this;
- let xf = this._ref.current!.getBoundingClientRect();
- let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height);
- let nh = NumCast(this.dataDoc.nativeHeight, 0);
+ const ChromeHeight = this.props.ChromeHeight;
+ let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
+ if (!this.props.isOverlay && this.props.Document.autoHeight && sh !== 0) {
+ let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
let dh = NumCast(this.props.Document.height, 0);
- let sh = scrBounds.height;
- const ChromeHeight = MainOverlayTextBox.Instance.ChromeHeight;
- this.props.Document.height = (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0);
+ this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
this.dataDoc.nativeHeight = nh ? sh : undefined;
}
}
- @action
- onPointerEnter = (e: React.PointerEvent) => {
- this._entered = true;
- }
- @action
- onPointerLeave = (e: React.PointerEvent) => {
- this._entered = false;
- }
- specificContextMenu = (e: React.MouseEvent): void => {
- let subitems: ContextMenuProps[] = [];
- subitems.push({
- description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height",
- 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" });
- }
render() {
let self = this;
let style = this.props.isOverlay ? "scroll" : "hidden";
@@ -672,8 +865,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
- height: this.props.height ? this.props.height : undefined,
- background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
+ overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
+ height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
+ background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive,
@@ -682,14 +876,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
- onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
onWheel={this.onPointerWheel}
- onPointerEnter={this.onPointerEnter}
- onPointerLeave={this.onPointerLeave}
+ onPointerEnter={action(() => this._entered = true)}
+ onPointerLeave={action(() => this._entered = false)}
>
<div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
</div>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 697f19f0d..00c069e1f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -56,4 +56,47 @@
left: 5%;
top: 15%;
}
-} \ No newline at end of file
+}
+
+#cf {
+ position:relative;
+ width:100%;
+ margin:0 auto;
+ display:flex;
+ align-items: center;
+ height:100%;
+ overflow:hidden;
+ .imageBox-fadeBlocker {
+ width:100%;
+ height:100%;
+ background: black;
+ display:flex;
+ flex-direction: row;
+ align-items: center;
+ z-index: 1;
+ .imageBox-fadeaway {
+ object-fit: contain;
+ width:100%;
+ height:100%;
+ }
+ }
+ }
+
+ #cf img {
+ position:absolute;
+ left:0;
+ }
+
+ .imageBox-fadeBlocker {
+ -webkit-transition: opacity 1s ease-in-out;
+ -moz-transition: opacity 1s ease-in-out;
+ -o-transition: opacity 1s ease-in-out;
+ transition: opacity 1s ease-in-out;
+ }
+ .imageBox-fadeBlocker:hover {
+ -webkit-transition: opacity 1s ease-in-out;
+ -moz-transition: opacity 1s ease-in-out;
+ -o-transition: opacity 1s ease-in-out;
+ transition: opacity 1s ease-in-out;
+ opacity:0;
+ } \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 45d389ba6..ec35465eb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -68,7 +68,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
private dropDisposer?: DragManager.DragDropDisposer;
- @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : this.props.Document; }
+ @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -92,13 +92,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData) {
de.data.droppedDocuments.forEach(action((drop: Doc) => {
- if (de.mods === "CtrlKey") {
- Doc.ApplyTemplateTo(drop, this.props.Document, this.props.DataDoc);
- e.stopPropagation();
- } else if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
+ if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
e.stopPropagation();
- } else if (de.mods === "CtrlKey") {
+ } else if (de.mods === "MetaKey") {
if (this.extensionDoc !== this.dataDoc) {
let layout = StrCast(drop.backgroundLayout);
if (layout.indexOf(ImageBox.name) !== -1) {
@@ -341,7 +338,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let rotation = NumCast(this.dataDoc.rotation) % 180;
let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
let aspect = realsize.height / realsize.width;
- if (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1) {
+ if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) {
setTimeout(action(() => {
layoutdoc.height = layoutdoc[WidthSym]() * aspect;
layoutdoc.nativeHeight = realsize.height;
@@ -424,6 +421,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];
+ let fadepath = paths[Math.min(paths.length - 1, 1)];
if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
@@ -431,13 +429,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
<div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
onPointerDown={this.onPointerDown}
onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <img id={id}
- key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
- src={srcpath}
- style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
- width={nativeWidth}
- ref={this._imgRef}
- onError={this.onError} />
+ <div id="cf">
+ <img id={id}
+ key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
+ src={srcpath}
+ style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
+ width={nativeWidth}
+ ref={this._imgRef}
+ onError={this.onError} />
+ {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker"> <img className="imageBox-fadeaway"
+ key={"fadeaway" + this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
+ src={fadepath}
+ style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
+ width={nativeWidth}
+ ref={this._imgRef}
+ onError={this.onError} /></div>}
+ </div>
{paths.length > 1 ? this.dots(paths) : (null)}
<div className="imageBox-audioBackground"
onPointerDown={this.audioDown}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 0d4b377dd..653c5c27f 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -128,7 +128,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
const self = this;
- for (let key of Object.keys(ids).sort()) {
+ for (let key of Object.keys(ids).slice().sort()) {
rows.push(<KeyValuePair doc={realDoc} ref={(function () {
let oldEl: KeyValuePair | undefined;
return (el: KeyValuePair) => {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 534a42efc..5afd4d834 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -60,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
isSelected: returnFalse,
select: emptyFunction,
renderDepth: 1,
- selectOnLoad: false,
active: returnFalse,
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
@@ -68,6 +67,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
PanelWidth: returnZero,
PanelHeight: returnZero,
addDocTab: returnZero,
+ pinToPres: returnZero,
ContentScaling: returnOne
};
let contents = <FieldView {...props} />;
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 1a4af04f8..1908889e9 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -12,9 +12,6 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
library.add(faTrash);
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { DocumentType } from "../../documents/Documents";
interface Props {
docView: DocumentView;
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index a119eb39b..90b335933 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -52,7 +52,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
}
if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext!);
+ DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext);
}
else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!));
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 6450cb826..18f82ff47 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -198,7 +198,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
<PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)}
Document={this.props.Document} DataDoc={this.props.DataDoc}
addDocTab={this.props.addDocTab} setPanY={this.setPanY}
- addDocument={this.props.addDocument}
+ pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} />
{this.settingsPanel()}
</div>);
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
new file mode 100644
index 000000000..e376fbddb
--- /dev/null
+++ b/src/client/views/nodes/PresBox.tsx
@@ -0,0 +1,529 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from "../../../Utils";
+import { DocumentManager } from "../../util/DocumentManager";
+import { undoBatch } from "../../util/UndoManager";
+import PresentationElement from "../presentationview/PresentationElement";
+import PresentationViewList from "../presentationview/PresentationList";
+import "../presentationview/PresentationView.scss";
+import { FieldView, FieldViewProps } from './FieldView';
+import { ContextMenu } from "../ContextMenu";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+
+export interface PresViewProps {
+ Documents: List<Doc>;
+}
+
+const expandedWidth = 450;
+
+@observer
+export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
+
+
+ public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+
+ public static Instance: PresBox;
+
+ //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed. Let users create them, store them, as they see fit.
+ @computed get curPresentation() { return this.props.Document; }
+
+ //mapping from docs to their rendered component
+ @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+ //variable that holds all the docs in the presentation
+ @observable childrenDocs: Doc[] = [];
+ //variable to hold if presentation is started
+ @observable presStatus: boolean = false;
+ //Mapping of guids to presentations.
+ @observable presentationsMapping: Map<String, Doc> = new Map();
+ //Mapping of presentations to guid, so that select option values can be given.
+ @observable presentationsKeyMapping: Map<Doc, String> = new Map();
+ //Variable to keep track of guid of the current presentation
+ @observable currentSelectedPresValue: string | undefined;
+ //A flag to keep track if title input is open, which is used in rendering.
+ @observable PresTitleInputOpen: boolean = false;
+ //Variable that holds reference to title input, so that new presentations get titles assigned.
+ @observable titleInputElement: HTMLInputElement | undefined;
+ @observable PresTitleChangeOpen: boolean = false;
+
+ @observable opacity = 1;
+ @observable persistOpacity = true;
+ @observable labelOpacity = 0;
+ @observable presMode = false;
+
+ @observable public static CurrentPresentation: PresBox;
+
+ //initilize class variables
+ constructor(props: FieldViewProps) {
+ super(props);
+ runInAction(() => PresBox.CurrentPresentation = this);
+ }
+
+ @action
+ toggle = (forcedValue: boolean | undefined) => {
+ if (forcedValue !== undefined) {
+ this.curPresentation.width = forcedValue ? expandedWidth : 0;
+ } else {
+ this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
+ }
+ }
+
+ //Second lifecycle function that gets called when component mounts. It makes sure toS
+ //get the back-up information from previous session for the current presentation.
+ async componentDidMount() {
+ this.setPresentationBackUps();
+ }
+
+
+ /**
+ * The function that retrieves the backUps for the current Presentation if present,
+ * otherwise initializes.
+ */
+ setPresentationBackUps = async () => {
+ //storing the presentation status,ie. whether it was stopped or playing
+ let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
+ runInAction(() => this.presStatus = presStatusBackUp);
+ }
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ next = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //asking to get document at current index
+ let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+ if (docAtCurrentNext === undefined) {
+ return;
+ }
+ let nextSelected = current + 1;
+
+ let presDocs = DocListCast(this.curPresentation.data);
+ for (; nextSelected < presDocs.length - 1; nextSelected++) {
+ if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+ break;
+ }
+ }
+
+ this.gotoDocument(nextSelected, current);
+
+ }
+ back = async () => {
+ const current = NumCast(this.curPresentation.selectedDoc);
+ //requesting for the doc at current index
+ let docAtCurrent = await this.getDocAtIndex(current);
+ if (docAtCurrent === undefined) {
+ return;
+ }
+
+ //asking for its presentation id.
+ let curPresId = StrCast(docAtCurrent.presentId);
+ let prevSelected = current;
+ let zoomOut: boolean = false;
+
+ //checking if this presentation id is mapped to a group, if so chosing the first element in group
+ let presDocs = DocListCast(this.curPresentation.data);
+ let currentsArray: Doc[] = [];
+ for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
+ currentsArray.push(presDocs[prevSelected]);
+ }
+ prevSelected = Math.max(0, prevSelected - 1);
+
+ //checking if any of the group members had used zooming in
+ currentsArray.forEach((doc: Doc) => {
+ //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
+ if (this.presElementsMappings.get(doc)!.props.document.showButton) {
+ zoomOut = true;
+ return;
+ }
+ });
+
+
+ // if a group set that flag to zero or a single element
+ //If so making sure to zoom out, which goes back to state before zooming action
+ if (current > 0) {
+ if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) {
+ let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
+ if (prevScale !== undefined) {
+ if (prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ }
+ }
+ }
+ }
+ this.gotoDocument(prevSelected, current);
+
+ }
+
+ /**
+ * This is the method that checks for the actions that need to be performed
+ * after the document has been presented, which involves 3 button options:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ showAfterPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ //the order of cases is aligned based on priority
+ if (presElem.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(key) <= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0;
+ }
+ }
+ if (presElem.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(key) < index) {
+ key.opacity = 0.5;
+ }
+ }
+ });
+ }
+
+ /**
+ * This is the method that checks for the actions that need to be performed
+ * before the document has been presented, which involves 3 button options:
+ * Hide Until Presented, Hide After Presented, Fade After Presented
+ */
+ hideIfNotPresented = (index: number) => {
+ this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ //the order of cases is aligned based on priority
+
+ if (presElem.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(key) >= index) {
+ key.opacity = 1;
+ }
+ }
+ if (presElem.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(key) > index) {
+ key.opacity = 0;
+ }
+ }
+ });
+ }
+
+ /**
+ * This method makes sure that cursor navigates to the element that
+ * has the option open and last in the group. If not in the group, and it has
+ * te option open, navigates to that element.
+ */
+ navigateToElement = async (curDoc: Doc, fromDoc: number) => {
+ let docToJump: Doc = curDoc;
+ let willZoom: boolean = false;
+
+
+ let presDocs = DocListCast(this.curPresentation.data);
+ let nextSelected = presDocs.indexOf(curDoc);
+ let currentDocGroups: Doc[] = [];
+ for (; nextSelected < presDocs.length - 1; nextSelected++) {
+ if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+ break;
+ }
+ currentDocGroups.push(presDocs[nextSelected]);
+ }
+
+ currentDocGroups.forEach((doc: Doc, index: number) => {
+ if (this.presElementsMappings.get(doc)!.navButton) {
+ docToJump = doc;
+ willZoom = false;
+ }
+ if (this.presElementsMappings.get(doc)!.showButton) {
+ docToJump = doc;
+ willZoom = true;
+ }
+ });
+
+ //docToJump stayed same meaning, it was not in the group or was the last element in the group
+ if (docToJump === curDoc) {
+ //checking if curDoc has navigation open
+ if (this.presElementsMappings.get(curDoc)!.navButton) {
+ DocumentManager.Instance.jumpToDocument(curDoc, false);
+ } else if (this.presElementsMappings.get(curDoc)!.showButton) {
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(curDoc, true);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+
+ //saving the scale user was on before zooming in
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+ return;
+ }
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ curDoc.viewScale = newScale;
+ //saving the scale that user was on
+ if (curScale !== 1) {
+ this.childrenDocs[fromDoc].viewScale = curScale;
+ }
+
+ }
+
+ /**
+ * Async function that supposedly return the doc that is located at given index.
+ */
+ getDocAtIndex = async (index: number) => {
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return undefined;
+ }
+ if (index < 0 || index >= list.length) {
+ return undefined;
+ }
+
+ this.curPresentation.selectedDoc = index;
+ //awaiting async call to finish to get Doc instance
+ const doc = await list[index];
+ return doc;
+ }
+
+ /**
+ * The function that removes a doc from a presentation. It also makes sure to
+ * do necessary updates to backUps and mappings stored locally.
+ */
+ @action
+ public RemoveDoc = async (index: number) => {
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ let removedDoc = await value.splice(index, 1)[0];
+
+ //removing the Presentation Element stored for it
+ this.presElementsMappings.delete(removedDoc);
+
+ }
+ }
+
+ public removeDocByRef = (doc: Doc) => {
+ let indexOfDoc = this.childrenDocs.indexOf(doc);
+ const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (value) {
+ value.splice(indexOfDoc, 1)[0];
+ }
+ //this.RemoveDoc(indexOfDoc, true);
+ if (indexOfDoc !== - 1) {
+ return true;
+ }
+ return false;
+ }
+
+ //The function that is called when a document is clicked or reached through next or back.
+ //it'll also execute the necessary actions if presentation is playing.
+ @action
+ public gotoDocument = async (index: number, fromDoc: number) => {
+ Doc.UnBrushAllDocs();
+ const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+ this.curPresentation.selectedDoc = index;
+
+ if (!this.presStatus) {
+ this.presStatus = true;
+ this.startPresentation(index);
+ }
+
+ const doc = await list[index];
+ if (this.presStatus) {
+ this.navigateToElement(doc, fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
+ }
+ }
+ //Function that sets the store of the children docs.
+ @action
+ setChildrenDocs = (docList: Doc[]) => {
+ this.childrenDocs = docList;
+ }
+
+ //The function that is called to render the play or pause button depending on
+ //if presentation is running or not.
+ renderPlayPauseButton = () => {
+ if (this.presStatus) {
+ return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+ } else {
+ return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ }
+ }
+
+ //The function that starts or resets presentaton functionally, depending on status flag.
+ @action
+ startOrResetPres = () => {
+ if (this.presStatus) {
+ this.resetPresentation();
+ } else {
+ this.presStatus = true;
+ this.startPresentation(0);
+ const current = NumCast(this.curPresentation.selectedDoc);
+ this.gotoDocument(0, current);
+ }
+ this.curPresentation.presStatus = this.presStatus;
+ }
+
+ //The function that resets the presentation by removing every action done by it. It also
+ //stops the presentaton.
+ @action
+ resetPresentation = () => {
+ this.childrenDocs.forEach((doc: Doc) => {
+ doc.opacity = 1;
+ doc.viewScale = 1;
+ });
+ this.curPresentation.selectedDoc = 0;
+ this.presStatus = false;
+ this.curPresentation.presStatus = this.presStatus;
+ if (this.childrenDocs.length === 0) {
+ return;
+ }
+ DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+ }
+
+
+ //The function that starts the presentation, also checking if actions should be applied
+ //directly at start.
+ startPresentation = (startIndex: number) => {
+ this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+ if (component.props.document.hideTillShownButton) {
+ if (this.childrenDocs.indexOf(doc) > startIndex) {
+ doc.opacity = 0;
+ }
+
+ }
+ if (component.props.document.hideAfterButton) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0;
+ }
+ }
+ if (component.props.document.fadeButton) {
+ if (this.childrenDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0.5;
+ }
+ }
+
+ });
+
+ }
+
+
+ /**
+ * The function that is called to render either select for presentations, or title inputting.
+ */
+ renderSelectOrPresSelection = () => {
+ if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
+ return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+ } else {
+ return (null);
+ }
+ }
+
+ /**
+ * The function that is called on enter press of title input. It gives the
+ * new presentation the title user entered. If nothing is entered, gives a default title.
+ */
+ @action
+ submitPresentationTitle = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ let presTitle = this.titleInputElement!.value;
+ this.titleInputElement!.value = "";
+ if (this.PresTitleChangeOpen) {
+ this.PresTitleChangeOpen = false;
+ this.changePresentationTitle(presTitle);
+ }
+ }
+ }
+ /**
+ * The function that is called to change title of presentation to what user entered.
+ */
+ @undoBatch
+ changePresentationTitle = (newTitle: string) => {
+ if (newTitle === "") {
+ return;
+ }
+ this.curPresentation.title = newTitle;
+ }
+
+ addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
+ this.presElementsMappings.set(keyDoc, elem);
+ }
+
+ minimize = undoBatch(action(() => {
+ this.presMode = true;
+ this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
+ }));
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
+ }
+
+ render() {
+
+ let width = "100%"; //NumCast(this.curPresentation.width)
+ return (
+ <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu}
+ onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))}
+ style={{ width: width, opacity: this.opacity, }}>
+ <div className="presentation-buttons">
+ <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ {this.renderPlayPauseButton()}
+ <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button>
+ </div>
+ <input
+ type="checkbox"
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.persistOpacity = e.target.checked;
+ this.opacity = this.persistOpacity ? 1 : 0.4;
+ })}
+ checked={this.persistOpacity}
+ style={{ position: "absolute", bottom: 5, left: 5 }}
+ onPointerEnter={action(() => this.labelOpacity = 1)}
+ onPointerLeave={action(() => this.labelOpacity = 0)}
+ />
+ <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
+ <PresentationViewList
+ mainDocument={this.curPresentation}
+ deleteDocument={this.RemoveDoc}
+ gotoDocument={this.gotoDocument}
+ PresElementsMappings={this.presElementsMappings}
+ setChildrenDocs={this.setChildrenDocs}
+ presStatus={this.presStatus}
+ removeDocByRef={this.removeDocByRef}
+ clearElemMap={() => this.presElementsMappings.clear()}
+ />
+ </div>
+ );
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 704030d85..3f4ee8960 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -34,7 +34,7 @@ library.add(faVideo);
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
private _reactionDisposer?: IReactionDisposer;
private _youtubeReactionDisposer?: IReactionDisposer;
- private _youtubePlayer: any = undefined;
+ private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
@@ -78,7 +78,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
@action public Pause = (update: boolean = true) => {
this.Playing = false;
update && this.player && this.player.pause();
- update && this._youtubePlayer && this._youtubePlayer.pauseVideo();
+ update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo();
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._playTimer = undefined;
this.updateTimecode();
@@ -244,7 +244,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
let onYoutubePlayerStateChange = (event: any) => runInAction(() => {
if (started && event.data === YT.PlayerState.PLAYING) {
started = false;
- this._youtubePlayer.unMute();
+ this._youtubePlayer && this._youtubePlayer.unMute();
this.Pause();
return;
}
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index eb09b0693..43220df71 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,16 +1,20 @@
+@import "../globalCssVariables.scss";
-.webBox-cont, .webBox-cont-interactive{
+.webBox-cont,
+.webBox-cont-interactive {
padding: 0vw;
position: absolute;
top: 0;
- left:0;
+ left: 0;
width: 100%;
height: 100%;
overflow: auto;
- pointer-events: none ;
+ pointer-events: none;
}
+
.webBox-cont-interactive {
pointer-events: all;
+
span {
user-select: text !important;
}
@@ -18,8 +22,8 @@
#webBox-htmlSpan {
position: absolute;
- top:0;
- left:0;
+ top: 0;
+ left: 0;
}
.webBox-overlay {
@@ -29,8 +33,60 @@
}
.webBox-button {
- padding : 0vw;
+ padding: 0vw;
border: none;
- width : 100%;
+ width: 100%;
+ height: 100%;
+}
+
+.webView-urlEditor {
+ position: relative;
+ opacity: 0.9;
+ z-index: 9001;
+ transition: top .5s;
+ background: lightgrey;
+ padding: 10px;
+
+
+ .urlEditor {
+ display: grid;
+ grid-template-columns: 1fr auto;
+ padding-bottom: 10px;
+ overflow: hidden;
+
+ .editorBase {
+ display: flex;
+
+ .editor-collapse {
+ transition: all .5s, opacity 0.3s;
+ position: absolute;
+ width: 40px;
+ transform-origin: top left;
+ }
+
+ .switchToText {
+ color: $main-accent;
+ }
+
+ .switchToText:hover {
+ color: $dark-color;
+ }
+ }
+
+ button:hover {
+ transform: scale(1);
+ }
+ }
+}
+
+.webpage-urlInput {
+ padding: 12px 10px 11px 10px;
+ border: 0px;
+ color: grey;
+ letter-spacing: 2px;
+ outline-color: black;
+ background: rgb(238, 238, 238);
+ width: 100%;
+ margin-right: 10px;
height: 100%;
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c8749b7cd..642f58daf 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,19 +1,31 @@
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
+import { FieldResult, Doc } from "../../../new_fields/Doc";
import { HtmlField } from "../../../new_fields/HtmlField";
+import { InkTool } from "../../../new_fields/InkField";
+import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
+import { Utils } from "../../../Utils";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
+import { KeyValueBox } from "./KeyValueBox";
import "./WebBox.scss";
import React = require("react");
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Docs } from "../../documents/Documents";
+import { faStickyNote } from "@fortawesome/free-solid-svg-icons";
+import { library } from "@fortawesome/fontawesome-svg-core";
+
+library.add(faStickyNote);
@observer
export class WebBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(WebBox); }
+ @observable private collapsed: boolean = true;
+ @observable private url: string = "";
componentWillMount() {
@@ -28,6 +40,104 @@ export class WebBox extends React.Component<FieldViewProps> {
this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect;
}
}
+
+ this.setURL();
+ }
+
+ @action
+ onURLChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.url = e.target.value;
+ }
+
+ @action
+ submitURL = () => {
+ const script = KeyValueBox.CompileKVPScript(`new WebField("${this.url}")`);
+ if (!script) return;
+ KeyValueBox.ApplyKVPScript(this.props.Document, "data", script);
+ }
+
+ @action
+ setURL() {
+ let urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField);
+ if (urlField) this.url = urlField.url.toString();
+ else this.url = "";
+ }
+
+ onValueKeyDown = async (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation();
+ this.submitURL();
+ }
+ }
+
+
+ switchToText = () => {
+ let url: string = "";
+ let field = Cast(this.props.Document[this.props.fieldKey], WebField);
+ if (field) url = field.url.href;
+
+ let newBox = Docs.Create.TextDocument({
+ x: NumCast(this.props.Document.x),
+ y: NumCast(this.props.Document.y),
+ title: url,
+ width: 200,
+ height: 70,
+ documentText: "@@@" + url
+ });
+
+ SelectionManager.SelectedDocuments().map(dv => {
+ dv.props.addDocument && dv.props.addDocument(newBox, false);
+ dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
+ });
+
+ Doc.BrushDoc(newBox);
+ }
+
+ urlEditor() {
+ return (
+ <div className="webView-urlEditor" style={{ top: this.collapsed ? -70 : 0 }}>
+ <div className="urlEditor">
+ <div className="editorBase">
+ <button className="editor-collapse"
+ style={{
+ top: this.collapsed ? 70 : 10,
+ transform: `rotate(${this.collapsed ? 180 : 0}deg) scale(${this.collapsed ? 0.5 : 1}) translate(${this.collapsed ? "-100%, -100%" : "0, 0"})`,
+ opacity: (this.collapsed && !this.props.isSelected()) ? 0 : 0.9,
+ left: (this.collapsed ? 0 : "unset"),
+ }}
+ title="Collapse Url Editor" onClick={this.toggleCollapse}>
+ <FontAwesomeIcon icon="caret-up" size="2x" />
+ </button>
+ <div style={{ marginLeft: 54, width: "100%", display: this.collapsed ? "none" : "flex" }}>
+ <input className="webpage-urlInput"
+ placeholder="ENTER URL"
+ value={this.url}
+ onChange={this.onURLChange}
+ onKeyDown={this.onValueKeyDown}
+ />
+ <div style={{
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ minWidth: "100px",
+ }}>
+ <button className="submitUrl" onClick={this.submitURL}>
+ SUBMIT
+ </button>
+ <div className="switchToText" title="Convert web to text doc" onClick={this.switchToText} style={{ display: "flex", alignItems: "center", justifyContent: "center" }} >
+ <FontAwesomeIcon icon={faStickyNote} size={"lg"} />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+
+ @action
+ toggleCollapse = () => {
+ this.collapsed = !this.collapsed;
}
_ignore = 0;
@@ -53,12 +163,13 @@ export class WebBox extends React.Component<FieldViewProps> {
if (field instanceof HtmlField) {
view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
} else {
- view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
}
let content =
<div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ {this.urlEditor()}
{view}
</div>;
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 7ba7b6d14..6f77a0a5b 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -6,10 +6,10 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { DocumentManager } from "../../util/DocumentManager";
-import { PresentationView } from "../presentationview/PresentationView";
import PDFMenu from "./PDFMenu";
import "./Annotation.scss";
import { scale } from "./PDFViewer";
+import { PresBox } from "../nodes/PresBox";
interface IAnnotationProps {
anno: Doc;
@@ -18,6 +18,7 @@ interface IAnnotationProps {
fieldExtensionDoc: Doc;
scrollTo?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
}
export default class Annotation extends React.Component<IAnnotationProps> {
@@ -37,6 +38,7 @@ interface IRegionAnnotationProps {
fieldExtensionDoc: Doc;
scrollTo?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
document: Doc;
}
@@ -81,7 +83,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
pinToPres = () => {
let group = FieldValue(Cast(this.props.document.group, Doc));
- group && PresentationView.Instance.PinDoc(group);
+ group && this.props.pinToPres(group);
}
@action
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 08674720d..e5917fefc 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -36,6 +36,7 @@ interface IViewerProps {
active: () => boolean;
setPanY?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+ pinToPres: (document: Doc) => void;
addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
}
@@ -151,12 +152,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (this._pageSizes.length === 0) {
this._isPage = Array<string>(this.props.pdf.numPages);
this._pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
+ this._visibleElements = Array<JSX.Element>(this.props.pdf.numPages);
await Promise.all(this._pageSizes.map<Pdfjs.PDFPromise<any>>((val, i) =>
this.props.pdf.getPage(i + 1).then(action((page: Pdfjs.PDFPageProxy) => {
this._pageSizes.splice(i, 1, {
width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale,
height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale
});
+ this._visibleElements.splice(i, 1,
+ <div key={`${this.props.url}-placeholder-${i + 1}`} className="pdfviewer-placeholder"
+ style={{ width: this._pageSizes[i].width, height: this._pageSizes[i].height }}>
+ "PAGE IS LOADING... "
+ </div>);
this.getPlaceholderPage(i);
}))));
this.props.loaded(Math.max(...this._pageSizes.map(i => i.width)), this._pageSizes[0].height, this.props.pdf.numPages);
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 7ca9d2d7d..8df2dce29 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -19,8 +19,8 @@ interface IPageProps {
numPages: number;
page: number;
pageLoaded: (page: Pdfjs.PDFPageViewport) => void;
- fieldExtensionDoc: Doc,
- Document: Doc,
+ fieldExtensionDoc: Doc;
+ Document: Doc;
renderAnnotations: (annotations: Doc[], removeOld: boolean) => void;
sendAnnotations: (annotations: HTMLDivElement[], page: number) => void;
createAnnotation: (div: HTMLDivElement, page: number) => void;
@@ -64,13 +64,14 @@ export default class Page extends React.Component<IPageProps> {
// lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) {
this._state = "rendering";
- let viewport = page.getViewport(scale);
+ let viewport = page.getViewport({ scale: scale });
this._canvas.current.width = this._width = viewport.width;
this._canvas.current.height = this._height = viewport.height;
this.props.pageLoaded(viewport);
let ctx = this._canvas.current.getContext("2d");
if (ctx) {
- page.render({ canvasContext: ctx, viewport: viewport }); // renders the page onto the canvas context
+ //@ts-ignore
+ page.render({ canvasContext: ctx, viewport: viewport, enableWebGL: true }); // renders the page onto the canvas context
page.getTextContent().then(res => // renders text onto the text container
//@ts-ignore
Pdfjs.renderTextLayer({
@@ -112,7 +113,7 @@ export default class Page extends React.Component<IPageProps> {
if (!BoolCast(annotationDoc.linkedToDoc)) {
let annotations = await DocListCastAsync(annotationDoc.annotations);
annotations && annotations.forEach(anno => anno.target = targetDoc);
- DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`)
+ DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
}
}
},
@@ -151,6 +152,9 @@ export default class Page extends React.Component<IPageProps> {
PDFMenu.Instance.fadeOut(true);
if (e.target && (e.target as any).parentElement === this._textLayer.current) {
e.stopPropagation();
+ if (!e.ctrlKey) {
+ this.props.sendAnnotations([], -1);
+ }
}
else {
// set marquee x and y positions to the spatially transformed position
@@ -161,14 +165,12 @@ export default class Page extends React.Component<IPageProps> {
}
this._marqueeing = true;
this._marquee.current && (this._marquee.current.style.opacity = "0.2");
+ this.props.sendAnnotations([], -1);
}
document.removeEventListener("pointermove", this.onSelectStart);
document.addEventListener("pointermove", this.onSelectStart);
document.removeEventListener("pointerup", this.onSelectEnd);
document.addEventListener("pointerup", this.onSelectEnd);
- if (!e.ctrlKey) {
- this.props.sendAnnotations([], -1);
- }
}
}
@@ -257,7 +259,7 @@ export default class Page extends React.Component<IPageProps> {
}
}
}
- let text = selRange.extractContents().textContent;
+ let text = selRange.cloneContents().textContent;
text && this.props.setSelectionText(text);
// clear selection
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index d98b66324..80aa25f48 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -1,21 +1,19 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
-import { faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch, faArrowRight } from '@fortawesome/free-solid-svg-icons';
+import { faArrowRight, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, returnFalse, emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
+import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils";
+import { DocumentType } from "../../documents/DocumentTypes";
import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
-import { ContextMenu } from "../ContextMenu";
import { Transform } from "../../util/Transform";
+import { ContextMenu } from "../ContextMenu";
import { DocumentView } from "../nodes/DocumentView";
-import { DocumentType } from "../../documents/Documents";
import React = require("react");
@@ -33,26 +31,9 @@ interface PresentationElementProps {
deleteDocument(index: number): void;
gotoDocument(index: number, fromDoc: number): Promise<void>;
allListElements: Doc[];
- groupMappings: Map<String, Doc[]>;
presStatus: boolean;
- presButtonBackUp: Doc;
- presGroupBackUp: Doc;
removeDocByRef(doc: Doc): boolean;
PresElementsMappings: Map<Doc, PresentationElement>;
-
-
-}
-
-//enum for the all kinds of buttons a doc in presentation can have
-export enum buttonIndex {
- Show = 0,
- Navigate = 1,
- HideTillPressed = 2,
- FadeAfter = 3,
- HideAfter = 4,
- Group = 5,
- OpenRight = 6
-
}
/**
@@ -62,37 +43,33 @@ export enum buttonIndex {
@observer
export default class PresentationElement extends React.Component<PresentationElementProps> {
- @observable private selectedButtons: boolean[];
private header?: HTMLDivElement | undefined;
private listdropDisposer?: DragManager.DragDropDisposer;
- private presElRef: React.RefObject<HTMLDivElement>;
- private backUpDoc: Doc | undefined;
-
-
- constructor(props: PresentationElementProps) {
- super(props);
- this.selectedButtons = new Array(7);
-
- this.presElRef = React.createRef();
- }
-
+ private presElRef: React.RefObject<HTMLDivElement> = React.createRef();
componentWillUnmount() {
this.listdropDisposer && this.listdropDisposer();
}
+ @computed get currentIndex() { return NumCast(this.props.mainDocument.selectedDoc); }
- /**
- * Getter to get the status of the buttons.
- */
- @computed
- get selected() {
- return this.selectedButtons;
- }
+ @computed get showButton() { return BoolCast(this.props.document.showButton); }
+ @computed get navButton() { return BoolCast(this.props.document.navButton); }
+ @computed get hideTillShownButton() { return BoolCast(this.props.document.hideTillShownButton); }
+ @computed get fadeButton() { return BoolCast(this.props.document.fadeButton); }
+ @computed get hideAfterButton() { return BoolCast(this.props.document.hideAfterButton); }
+ @computed get groupButton() { return BoolCast(this.props.document.groupButton); }
+ @computed get openRightButton() { return BoolCast(this.props.document.openRightButton); }
+ set showButton(val: boolean) { this.props.document.showButton = val; }
+ set navButton(val: boolean) { this.props.document.navButton = val; }
+ set hideTillShownButton(val: boolean) { this.props.document.hideTillShownButton = val; }
+ set fadeButton(val: boolean) { this.props.document.fadeButton = val; }
+ set hideAfterButton(val: boolean) { this.props.document.hideAfterButton = val; }
+ set groupButton(val: boolean) { this.props.document.groupButton = val; }
+ set openRightButton(val: boolean) { this.props.document.openRightButton = val; }
//Lifecycle function that makes sure that button BackUp is received when mounted.
async componentDidMount() {
- this.receiveButtonBackUp();
if (this.presElRef.current) {
this.header = this.presElRef.current;
this.createListDropTarget(this.presElRef.current);
@@ -107,156 +84,9 @@ export default class PresentationElement extends React.Component<PresentationEle
}
}
- /**
- * Function that will be called to receive stored backUp for buttons
- */
- receiveButtonBackUp = async () => {
-
- //get the list that stores docs that keep track of buttons
- let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
- if (!castedList) {
- this.props.presButtonBackUp.selectedButtonDocs = castedList = new List<Doc>();
- }
-
- let foundDoc: boolean = false;
-
- //if this is the first time this doc mounts, push a doc for it to store
-
- for (let doc of castedList) {
- let curDoc = await doc;
- let curDocId = StrCast(curDoc.docId);
- if (curDocId === this.props.document[Id]) {
- let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
- if (selectedButtonOfDoc !== undefined) {
- runInAction(() => this.selectedButtons = selectedButtonOfDoc);
- foundDoc = true;
- this.backUpDoc = curDoc;
- break;
- }
- }
- }
-
- if (!foundDoc) {
- let newDoc = new Doc();
- let defaultBooleanArray: boolean[] = new Array(7);
- newDoc.selectedButtons = new List(defaultBooleanArray);
- newDoc.docId = this.props.document[Id];
- castedList.push(newDoc);
- this.backUpDoc = newDoc;
- }
-
- }
-
- /**
- * The function that is called to group docs together. It tries to group a doc
- * that turned grouping option with the above document. If that doc is grouped with
- * other documents. Those other documents will be grouped with doc's above document as well.
- */
- @action
- onGroupClick = (document: Doc, index: number, buttonStatus: boolean) => {
- let p = this.props;
- if (index >= 1) {
- //checking if options was turned true
- if (buttonStatus) {
- //getting the id of the above-doc and the doc
- let aboveGuid = StrCast(p.allListElements[index - 1].presentId, null);
- let docGuid = StrCast(document.presentId, null);
- //the case where above-doc is already in group
- if (p.groupMappings.has(aboveGuid)) {
- let aboveArray = p.groupMappings.get(aboveGuid)!;
- //case where doc is already in group
- if (p.groupMappings.has(docGuid)) {
- let docsArray = p.groupMappings.get(docGuid)!;
- docsArray.forEach((doc: Doc) => {
- if (!aboveArray.includes(doc)) {
- aboveArray.push(doc);
- }
- doc.presentId = aboveGuid;
- });
- p.groupMappings.delete(docGuid);
- //the case where doc was not in group
- } else {
- if (!aboveArray.includes(document)) {
- aboveArray.push(document);
-
- }
-
- }
- //the case where above-doc was not in group
- } else {
- let newAboveArray: Doc[] = [];
- newAboveArray.push(p.allListElements[index - 1]);
-
- //the case where doc is in group
- if (p.groupMappings.has(docGuid)) {
- let docsArray = p.groupMappings.get(docGuid)!;
- docsArray.forEach((doc: Doc) => {
- newAboveArray.push(doc);
- doc.presentId = aboveGuid;
- });
- p.groupMappings.delete(docGuid);
-
- //the case where doc is not in a group
- } else {
- newAboveArray.push(document);
-
- }
- p.groupMappings.set(aboveGuid, newAboveArray);
-
- }
- document.presentId = aboveGuid;
-
- //when grouping is turned off
- } else {
- let curArray = p.groupMappings.get(StrCast(document.presentId, Utils.GenerateGuid()))!;
- let targetIndex = curArray.indexOf(document);
- let firstPart = curArray.slice(0, targetIndex);
- let firstPartNewGuid = Utils.GenerateGuid();
- firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
- let secondPart = curArray.slice(targetIndex);
- p.groupMappings.set(StrCast(p.allListElements[index - 1].presentId, Utils.GenerateGuid()), firstPart);
- p.groupMappings.set(StrCast(document.presentId, Utils.GenerateGuid()), secondPart);
-
-
- }
-
- }
- this.autoSaveGroupChanges();
-
- }
-
-
- /**
- * This function is called at the end of each group update to update the group updates.
- */
- @action
- autoSaveGroupChanges = () => {
- let castedList: List<Doc> = new List<Doc>();
- this.props.presGroupBackUp.groupDocs = castedList;
- this.props.groupMappings.forEach((docArray: Doc[], id: String) => {
- //create a new doc for each group
- let newGroupDoc = new Doc();
- castedList.push(newGroupDoc);
- //store the id of the group in the doc
- newGroupDoc.presentIdStore = id.toString();
- //store the doc array which represents the group in the doc
- newGroupDoc.grouping = new List(docArray);
- });
-
- }
-
- /**
- * Function that is called on click to change the group status of a docus, by turning the option on/off.
- */
@action
- changeGroupStatus = () => {
- if (this.selectedButtons[buttonIndex.Group]) {
- this.selectedButtons[buttonIndex.Group] = false;
- } else {
- this.selectedButtons[buttonIndex.Group] = true;
- }
- this.autoSaveButtonChange(buttonIndex.Group);
-
+ onGroupClick = (e: React.MouseEvent) => {
+ this.groupButton = !this.groupButton;
}
/**
@@ -266,31 +96,18 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
e.stopPropagation();
- const current = NumCast(this.props.mainDocument.selectedDoc);
- if (this.selectedButtons[buttonIndex.HideTillPressed]) {
- this.selectedButtons[buttonIndex.HideTillPressed] = false;
- if (this.props.index >= current) {
+ this.hideTillShownButton = !this.hideTillShownButton;
+ if (!this.hideTillShownButton) {
+ if (this.props.index >= this.currentIndex) {
this.props.document.opacity = 1;
}
} else {
- this.selectedButtons[buttonIndex.HideTillPressed] = true;
if (this.props.presStatus) {
- if (this.props.index > current) {
+ if (this.props.index > this.currentIndex) {
this.props.document.opacity = 0;
}
}
}
- this.autoSaveButtonChange(buttonIndex.HideTillPressed);
- }
-
- /**
- * This function is called to get the updates for the changed buttons.
- */
- @action
- autoSaveButtonChange = async (index: buttonIndex) => {
- if (this.backUpDoc) {
- this.backUpDoc.selectedButtons = new List(this.selectedButtons);
- }
}
/**
@@ -301,25 +118,19 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- const current = NumCast(this.props.mainDocument.selectedDoc);
- if (this.selectedButtons[buttonIndex.HideAfter]) {
- this.selectedButtons[buttonIndex.HideAfter] = false;
- if (this.props.index <= current) {
+ this.hideAfterButton = !this.hideAfterButton;
+ if (!this.hideAfterButton) {
+ if (this.props.index <= this.currentIndex) {
this.props.document.opacity = 1;
}
} else {
- if (this.selectedButtons[buttonIndex.FadeAfter]) {
- this.selectedButtons[buttonIndex.FadeAfter] = false;
- }
- this.selectedButtons[buttonIndex.HideAfter] = true;
+ if (this.fadeButton) this.fadeButton = false;
if (this.props.presStatus) {
- if (this.props.index < current) {
+ if (this.props.index < this.currentIndex) {
this.props.document.opacity = 0;
}
}
}
- this.autoSaveButtonChange(buttonIndex.HideAfter);
-
}
/**
@@ -330,25 +141,19 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- const current = NumCast(this.props.mainDocument.selectedDoc);
- if (this.selectedButtons[buttonIndex.FadeAfter]) {
- this.selectedButtons[buttonIndex.FadeAfter] = false;
- if (this.props.index <= current) {
+ this.fadeButton = !this.fadeButton;
+ if (!this.fadeButton) {
+ if (this.props.index <= this.currentIndex) {
this.props.document.opacity = 1;
}
} else {
- if (this.selectedButtons[buttonIndex.HideAfter]) {
- this.selectedButtons[buttonIndex.HideAfter] = false;
- }
- this.selectedButtons[buttonIndex.FadeAfter] = true;
+ this.hideAfterButton = false;
if (this.props.presStatus) {
- if (this.props.index < current) {
+ if (this.props.index < this.currentIndex) {
this.props.document.opacity = 0.5;
}
}
}
- this.autoSaveButtonChange(buttonIndex.FadeAfter);
-
}
/**
@@ -357,22 +162,13 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onNavigateDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- if (this.selectedButtons[buttonIndex.Navigate]) {
- this.selectedButtons[buttonIndex.Navigate] = false;
-
- } else {
- if (this.selectedButtons[buttonIndex.Show]) {
- this.selectedButtons[buttonIndex.Show] = false;
- }
- this.selectedButtons[buttonIndex.Navigate] = true;
- const current = NumCast(this.props.mainDocument.selectedDoc);
- if (current === this.props.index) {
+ this.navButton = !this.navButton;
+ if (this.navButton) {
+ this.showButton = false;
+ if (this.currentIndex === this.props.index) {
this.props.gotoDocument(this.props.index, this.props.index);
}
}
-
- this.autoSaveButtonChange(buttonIndex.Navigate);
-
}
/**
@@ -381,23 +177,16 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onZoomDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- if (this.selectedButtons[buttonIndex.Show]) {
- this.selectedButtons[buttonIndex.Show] = false;
- this.props.document.viewScale = 1;
+ this.showButton = !this.showButton;
+ if (!this.showButton) {
+ this.props.document.viewScale = 1;
} else {
- if (this.selectedButtons[buttonIndex.Navigate]) {
- this.selectedButtons[buttonIndex.Navigate] = false;
- }
- this.selectedButtons[buttonIndex.Show] = true;
- const current = NumCast(this.props.mainDocument.selectedDoc);
- if (current === this.props.index) {
+ this.navButton = false;
+ if (this.currentIndex === this.props.index) {
this.props.gotoDocument(this.props.index, this.props.index);
}
}
-
- this.autoSaveButtonChange(buttonIndex.Show);
-
}
/**
@@ -407,13 +196,8 @@ export default class PresentationElement extends React.Component<PresentationEle
@action
onRightTabClick = (e: React.MouseEvent) => {
e.stopPropagation();
- if (this.selectedButtons[buttonIndex.OpenRight]) {
- this.selectedButtons[buttonIndex.OpenRight] = false;
- // action maybe
- } else {
- this.selectedButtons[buttonIndex.OpenRight] = true;
- }
- this.autoSaveButtonChange(buttonIndex.OpenRight);
+
+ this.openRightButton = !this.openRightButton;
}
/**
@@ -449,8 +233,6 @@ export default class PresentationElement extends React.Component<PresentationEle
//where does treeViewId come from
let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments);
//console.log("How is this causing an issue");
- let droppedDoc: Doc = de.data.droppedDocuments[0];
- await this.updateGroupsOnDrop(droppedDoc, de);
document.removeEventListener("pointermove", this.onDragMove, true);
return (de.data.dropAction || de.data.userDropAction) ?
de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false)
@@ -463,221 +245,13 @@ export default class PresentationElement extends React.Component<PresentationEle
return false;
}
- /**
- * This method is called to update groups when the user drags and drops an
- * element to a different place. It follows the default behaviour and reconstructs
- * the groups in the way they would appear if clicked by user.
- */
- updateGroupsOnDrop = async (droppedDoc: Doc, de: DragManager.DropEvent) => {
-
- let x = this.ScreenToLocalListTransform(de.x, de.y);
- let rect = this.header!.getBoundingClientRect();
- let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
- let before = x[1] < bounds[1];
-
- let droppedDocIndex = this.props.allListElements.indexOf(droppedDoc);
-
- let dropIndexDiff = droppedDocIndex - this.props.index;
-
- //checking if the position it's dropped corresponds to current location with 3 cases.
- if (droppedDocIndex === this.props.index) {
- return;
- }
-
- if (dropIndexDiff === 1 && !before) {
- return;
- }
- if (dropIndexDiff === -1 && before) {
- return;
- }
-
- let p = this.props;
- let droppedDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(droppedDoc);
- let curDocGuid = StrCast(droppedDoc.presentId, null);
-
- //Splicing the doc from its current group, since it's moved
- if (p.groupMappings.has(curDocGuid)) {
- let groupArray = this.props.groupMappings.get(curDocGuid)!;
-
- if (droppedDocSelectedButtons[buttonIndex.Group]) {
- let groupIndexOfDrop = groupArray.indexOf(droppedDoc);
- let firstPart = groupArray.splice(0, groupIndexOfDrop);
-
- if (firstPart.length > 1) {
- let newGroupGuid = Utils.GenerateGuid();
- firstPart.forEach((doc: Doc) => doc.presentId = newGroupGuid);
- this.props.groupMappings.set(newGroupGuid, firstPart);
- }
- }
-
- groupArray.splice(groupArray.indexOf(droppedDoc), 1);
- if (groupArray.length === 0) {
- this.props.groupMappings.delete(curDocGuid);
- }
- droppedDoc.presentId = Utils.GenerateGuid();
-
- //making sure to correct to groups after splicing, in case the dragged element
- //had the grouping on.
- let indexOfBelow = droppedDocIndex + 1;
- if (indexOfBelow < this.props.allListElements.length && indexOfBelow > 1) {
- let selectedButtonsOrigBelow: boolean[] = await this.getSelectedButtonsOfDoc(this.props.allListElements[indexOfBelow]);
- let aboveBelowDoc: Doc = this.props.allListElements[droppedDocIndex - 1];
- let aboveBelowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveBelowDoc);
- let belowDoc: Doc = this.props.allListElements[indexOfBelow];
- let belowDocPresId = StrCast(belowDoc.presentId);
-
- if (selectedButtonsOrigBelow[buttonIndex.Group]) {
- let belowDocGroup: Doc[] = this.props.groupMappings.get(belowDocPresId)!;
- if (aboveBelowDocSelectedButtons[buttonIndex.Group]) {
- let aboveBelowDocPresId = StrCast(aboveBelowDoc.presentId);
- if (this.props.groupMappings.has(aboveBelowDocPresId)) {
- let aboveBelowDocGroup: Doc[] = this.props.groupMappings.get(aboveBelowDocPresId)!;
- aboveBelowDocGroup.push(...belowDocGroup);
- this.props.groupMappings.delete(belowDocPresId);
- belowDocGroup.forEach((doc: Doc) => doc.presentId = aboveBelowDocPresId);
-
- }
- } else {
- belowDocGroup.unshift(aboveBelowDoc);
- aboveBelowDoc.presentId = belowDocPresId;
- }
-
-
- }
- }
-
- }
-
- //Case, when the dropped doc had the group button clicked.
- if (droppedDocSelectedButtons[buttonIndex.Group]) {
- if (before) {
- if (this.props.index > 0) {
- let aboveDoc = this.props.allListElements[this.props.index - 1];
- let aboveDocGuid = StrCast(aboveDoc.presentId);
- if (this.props.groupMappings.has(aboveDocGuid)) {
- this.protectOrderAndPush(aboveDocGuid, aboveDoc, droppedDoc);
- } else {
- this.createNewGroup(aboveDoc, droppedDoc, aboveDocGuid);
- }
- } else {
- let propsPresId = StrCast(this.props.document.presentId);
- if (this.selectedButtons[buttonIndex.Group]) {
- let propsArray = this.props.groupMappings.get(propsPresId)!;
- propsArray.unshift(droppedDoc);
- droppedDoc.presentId = propsPresId;
- }
- }
- } else {
- let propsDocGuid = StrCast(this.props.document.presentId);
- if (this.props.groupMappings.has(propsDocGuid)) {
- this.protectOrderAndPush(propsDocGuid, this.props.document, droppedDoc);
-
- } else {
- this.createNewGroup(this.props.document, droppedDoc, propsDocGuid);
- }
- }
-
-
- //if the group button of the element was not clicked.
- } else {
- if (before) {
- if (this.props.index > 0) {
-
- let aboveDoc = this.props.allListElements[this.props.index - 1];
- let aboveDocGuid = StrCast(aboveDoc.presentId);
- let aboveDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(aboveDoc);
-
-
- if (this.selectedButtons[buttonIndex.Group]) {
- if (aboveDocSelectedButtons[buttonIndex.Group]) {
- let aboveGroupArray = this.props.groupMappings.get(aboveDocGuid)!;
- let propsDocPresId = StrCast(this.props.document.presentId);
-
- this.halveGroupArray(aboveDoc, aboveGroupArray, droppedDoc, propsDocPresId);
-
- } else {
- let belowPresentId = StrCast(this.props.document.presentId);
- let belowGroup = this.props.groupMappings.get(belowPresentId)!;
- belowGroup.splice(belowGroup.indexOf(aboveDoc), 1);
- belowGroup.unshift(droppedDoc);
- droppedDoc.presentId = belowPresentId;
- aboveDoc.presentId = Utils.GenerateGuid();
- }
-
-
- }
- } else {
- let propsPresId = StrCast(this.props.document.presentId);
- if (this.selectedButtons[buttonIndex.Group]) {
- let propsArray = this.props.groupMappings.get(propsPresId)!;
- propsArray.unshift(droppedDoc);
- droppedDoc.presentId = propsPresId;
- }
- }
- } else {
- if (this.props.index < this.props.allListElements.length - 1) {
- let belowDoc = this.props.allListElements[this.props.index + 1];
- let belowDocGuid = StrCast(belowDoc.presentId);
- let belowDocSelectedButtons: boolean[] = await this.getSelectedButtonsOfDoc(belowDoc);
-
- let propsDocGuid = StrCast(this.props.document.presentId);
-
- if (belowDocSelectedButtons[buttonIndex.Group]) {
- let belowGroupArray = this.props.groupMappings.get(belowDocGuid)!;
- if (this.selectedButtons[buttonIndex.Group]) {
-
- let propsGroupArray = this.props.groupMappings.get(propsDocGuid)!;
-
- this.halveGroupArray(this.props.document, propsGroupArray, droppedDoc, belowDocGuid);
-
- } else {
- belowGroupArray.splice(belowGroupArray.indexOf(this.props.document), 1);
- this.props.document.presentId = Utils.GenerateGuid();
- belowGroupArray.unshift(droppedDoc);
- droppedDoc.presentId = belowDocGuid;
- }
- }
-
- }
- }
- }
- this.autoSaveGroupChanges();
-
- }
-
- /**
- * This method returns the selectedButtons boolean array of the passed in doc,
- * retrieving it from the back-up.
- */
- getSelectedButtonsOfDoc = async (paramDoc: Doc) => {
- let castedList = Cast(this.props.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
- let foundSelectedButtons: boolean[] = new Array(7);
-
- //if this is the first time this doc mounts, push a doc for it to store
- for (let doc of castedList!) {
- let curDoc = await doc;
- let curDocId = StrCast(curDoc.docId);
- if (curDocId === paramDoc[Id]) {
- let selectedButtonOfDoc = Cast(curDoc.selectedButtons, listSpec("boolean"), null);
- if (selectedButtonOfDoc !== undefined) {
- return selectedButtonOfDoc;
- }
- }
- }
-
- return foundSelectedButtons;
-
- }
-
//This is used to add dragging as an event.
onPointerEnter = (e: React.PointerEvent): void => {
if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
- let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
this.header!.className = "presentationView-item";
-
- if (selected === this.props.index) {
+ if (this.currentIndex === this.props.index) {
//this doc is selected
this.header!.className = "presentationView-item presentationView-selected";
}
@@ -687,13 +261,9 @@ export default class PresentationElement extends React.Component<PresentationEle
//This is used to remove the dragging when dropped.
onPointerLeave = (e: React.PointerEvent): void => {
- //to get currently selected presentation doc
- let selected = NumCast(this.props.mainDocument.selectedDoc, 0);
-
this.header!.className = "presentationView-item";
-
- if (selected === this.props.index) {
+ if (this.currentIndex === this.props.index) {
//this doc is selected
this.header!.className = "presentationView-item presentationView-selected";
@@ -729,62 +299,6 @@ export default class PresentationElement extends React.Component<PresentationEle
move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc);
}
-
- /**
- * Helper method that gets called to divide a group array into two different groups
- * including the targetDoc in first part.
- * @param targetDoc document that is targeted as slicing point
- * @param propsGroupArray the array that gets divided into 2
- * @param droppedDoc the dropped document
- * @param belowDocGuid presentId of the belowGroup
- */
- private halveGroupArray(targetDoc: Doc, propsGroupArray: Doc[], droppedDoc: Doc, belowDocGuid: string) {
- let targetIndex = propsGroupArray.indexOf(targetDoc);
- let firstPart = propsGroupArray.slice(0, targetIndex + 1);
- let firstPartNewGuid = Utils.GenerateGuid();
- firstPart.forEach((doc: Doc) => doc.presentId = firstPartNewGuid);
- let secondPart = propsGroupArray.slice(targetIndex + 1);
- secondPart.unshift(droppedDoc);
- droppedDoc.presentId = belowDocGuid;
- this.props.groupMappings.set(firstPartNewGuid, firstPart);
- this.props.groupMappings.set(belowDocGuid, secondPart);
- }
-
- /**
- * Helper method that creates a new group, pushing above document first,
- * and dropped document second.
- * @param aboveDoc the document above dropped document
- * @param droppedDoc the dropped document itself
- * @param aboveDocGuid above document's presentId
- */
- private createNewGroup(aboveDoc: Doc, droppedDoc: Doc, aboveDocGuid: string) {
- let newGroup: Doc[] = [];
- newGroup.push(aboveDoc);
- newGroup.push(droppedDoc);
- droppedDoc.presentId = aboveDocGuid;
- this.props.groupMappings.set(aboveDocGuid, newGroup);
- }
-
- /**
- * Helper method that finds the above document's group, and pushes the
- * dropped document into that group, protecting the visual order of the
- * presentation elements.
- * @param aboveDoc the document above dropped document
- * @param droppedDoc the dropped document itself
- * @param aboveDocGuid above document's presentId
- */
- private protectOrderAndPush(aboveDocGuid: string, aboveDoc: Doc, droppedDoc: Doc) {
- let groupArray = this.props.groupMappings.get(aboveDocGuid)!;
- let tempStack: Doc[] = [];
- while (groupArray[groupArray.length - 1] !== aboveDoc) {
- tempStack.push(groupArray.pop()!);
- }
- groupArray.push(droppedDoc);
- droppedDoc.presentId = aboveDocGuid;
- while (tempStack.length !== 0) {
- groupArray.push(tempStack.pop()!);
- }
- }
/**
* This function is a getter to get if a document is in previewMode.
*/
@@ -839,12 +353,12 @@ export default class PresentationElement extends React.Component<PresentationEle
removeDocument={returnFalse}
ScreenToLocalTransform={Transform.Identity}
addDocTab={returnFalse}
+ pinToPres={returnFalse}
renderDepth={1}
PanelWidth={() => 350}
PanelHeight={() => 90}
focus={emptyFunction}
backgroundColor={returnEmptyString}
- selectOnLoad={false}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
bringToFront={emptyFunction}
@@ -871,11 +385,8 @@ export default class PresentationElement extends React.Component<PresentationEle
let p = this.props;
let title = p.document.title;
- //to get currently selected presentation doc
- let selected = NumCast(p.mainDocument.selectedDoc, 0);
-
let className = " presentationView-item";
- if (selected === p.index) {
+ if (this.currentIndex === p.index) {
//this doc is selected
className += " presentationView-selected";
}
@@ -891,23 +402,19 @@ export default class PresentationElement extends React.Component<PresentationEle
outlineStyle: "dashed",
outlineWidth: Doc.IsBrushed(p.document) ? `1px` : "0px",
}}
- onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}>
+ onClick={e => { p.gotoDocument(p.index, this.currentIndex); e.stopPropagation(); }}>
<strong className="presentationView-name">
{`${p.index + 1}. ${title}`}
</strong>
<button className="presentation-icon" onPointerDown={(e) => e.stopPropagation()} onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
<br></br>
- <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} color={"gray"} /></button>
- <button title="Hide Document After Presented" className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={(e) => {
- e.stopPropagation();
- this.changeGroupStatus();
- this.onGroupClick(p.document, p.index, this.selectedButtons[buttonIndex.Group]);
- }}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
- <button title="Open Right" className={this.selectedButtons[buttonIndex.OpenRight] ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ <button title="Zoom" className={this.showButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={this.navButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Document Till Presented" className={this.hideTillShownButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade Document After Presented" className={this.fadeButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Hide Document After Presented" className={this.hideAfterButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={this.groupButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onGroupClick}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
+ <button title="Open Right" className={this.openRightButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button>
<br />
{this.renderEmbeddedInline()}
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index e853c4070..930ce202e 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -1,27 +1,20 @@
-import { observer } from "mobx-react";
-import React = require("react");
import { action } from "mobx";
-import "./PresentationView.scss";
-import { Utils } from "../../../Utils";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { NumCast, StrCast } from "../../../new_fields/Types";
+import { observer } from "mobx-react";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import PresentationElement, { buttonIndex } from "./PresentationElement";
-import "../../../new_fields/Doc";
-
-
+import { NumCast } from "../../../new_fields/Types";
+import PresentationElement from "./PresentationElement";
+import "./PresentationView.scss";
+import React = require("react");
interface PresListProps {
mainDocument: Doc;
deleteDocument(index: number): void;
gotoDocument(index: number, fromDoc: number): Promise<void>;
- groupMappings: Map<String, Doc[]>;
PresElementsMappings: Map<Doc, PresentationElement>;
setChildrenDocs: (docList: Doc[]) => void;
presStatus: boolean;
- presButtonBackUp: Doc;
- presGroupBackUp: Doc;
removeDocByRef(doc: Doc): boolean;
clearElemMap(): void;
@@ -34,35 +27,6 @@ interface PresListProps {
*/
export default class PresentationViewList extends React.Component<PresListProps> {
- /**
- * Method that initializes presentation ids for the
- * docs that is in the presentation, when presentation list
- * gets re-rendered. It makes sure to not assign ids to the
- * docs that are in the group, so that mapping won't be disrupted.
- */
-
- @action
- initializeGroupIds = async (docList: Doc[]) => {
- docList.forEach(async (doc: Doc, index: number) => {
- let docGuid = StrCast(doc.presentId, null);
- //checking if part of group
- let storedGuids: string[] = [];
- let castedGroupDocs = await DocListCastAsync(this.props.presGroupBackUp.groupDocs);
- //making sure the docs that were in groups, which were stored, to not get new guids.
- if (castedGroupDocs !== undefined) {
- castedGroupDocs.forEach((doc: Doc) => {
- let storedGuid = StrCast(doc.presentIdStore, null);
- if (storedGuid) {
- storedGuids.push(storedGuid);
- }
-
- });
- }
- if (!this.props.groupMappings.has(docGuid) && !storedGuids.includes(docGuid)) {
- doc.presentId = Utils.GenerateGuid();
- }
- });
- }
/**
* Initially every document starts with a viewScale 1, which means
@@ -80,7 +44,6 @@ export default class PresentationViewList extends React.Component<PresListProps>
render() {
const children = DocListCast(this.props.mainDocument.data);
- this.initializeGroupIds(children);
this.initializeScaleViews(children);
this.props.setChildrenDocs(children);
this.props.clearElemMap();
@@ -99,11 +62,8 @@ export default class PresentationViewList extends React.Component<PresListProps>
index={index}
deleteDocument={this.props.deleteDocument}
gotoDocument={this.props.gotoDocument}
- groupMappings={this.props.groupMappings}
allListElements={children}
presStatus={this.props.presStatus}
- presButtonBackUp={this.props.presButtonBackUp}
- presGroupBackUp={this.props.presGroupBackUp}
removeDocByRef={this.props.removeDocByRef}
PresElementsMappings={this.props.PresElementsMappings}
/>
diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
index 65b09c833..5c40a8808 100644
--- a/src/client/views/presentationview/PresentationView.scss
+++ b/src/client/views/presentationview/PresentationView.scss
@@ -1,13 +1,14 @@
.presentationView-cont {
position: absolute;
- background: white;
z-index: 2;
box-shadow: #AAAAAA .2vw .2vw .4vw;
right: 0;
top: 0;
bottom: 0;
letter-spacing: 2px;
-
+ overflow: hidden;
+ transition: 0.7s opacity ease;
+ pointer-events: all;
}
.presentationView-item {
@@ -22,14 +23,11 @@
user-select: none;
transition: all .1s;
-
-
.documentView-node {
position: absolute;
z-index: 1;
}
-
}
.presentationView-item-above {
@@ -47,12 +45,15 @@
.presentationView-item:hover {
transition: all .1s;
- background: #AAAAAA
+ background: #AAAAAA;
+ border-radius: 12px;
}
.presentationView-selected {
background: gray;
color: black;
+ border-radius: 12px;
+ box-shadow: black 2px 2px 5px;
}
.presentationView-heading {
@@ -69,7 +70,6 @@
display: inline-block;
width: calc(100% - 200px);
letter-spacing: 2px;
-
}
.presentation-icon {
@@ -77,11 +77,12 @@
}
.presentation-interaction {
+ color: gray;
float: left;
}
.presentation-interaction-selected {
- background: #505050;
+ color: white;
float: left;
}
@@ -93,7 +94,8 @@
.presentation-button {
margin-right: 2.5%;
margin-left: 2.5%;
- width: 25%;
+ width: 20%;
+ border-radius: 5px;
}
.presentation-buttons {
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
deleted file mode 100644
index bea70f00b..000000000
--- a/src/client/views/presentationview/PresentationView.tsx
+++ /dev/null
@@ -1,994 +0,0 @@
-import { observer } from "mobx-react";
-import React = require("react");
-import { observable, action, runInAction, reaction, autorun } from "mobx";
-import "./PresentationView.scss";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Utils } from "../../../Utils";
-import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc";
-import { listSpec } from "../../../new_fields/Schema";
-import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { List } from "../../../new_fields/List";
-import PresentationElement, { buttonIndex } from "./PresentationElement";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons';
-import { Docs } from "../../documents/Documents";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import PresentationViewList from "./PresentationList";
-import PresModeMenu from "./PresentationModeMenu";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-
-library.add(faArrowLeft);
-library.add(faArrowRight);
-library.add(faPlay);
-library.add(faStop);
-library.add(faPlus);
-library.add(faTimes);
-library.add(faMinus);
-library.add(faEdit);
-library.add(faEye);
-
-
-export interface PresViewProps {
- Documents: List<Doc>;
-}
-
-const expandedWidth = 400;
-const presMinWidth = 300;
-
-@observer
-export class PresentationView extends React.Component<PresViewProps> {
- public static Instance: PresentationView;
-
- //Mapping from presentation ids to a list of doc that represent a group
- @observable groupMappings: Map<String, Doc[]> = new Map();
- //mapping from docs to their rendered component
- @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
- //variable that holds all the docs in the presentation
- @observable childrenDocs: Doc[] = [];
- //variable to hold if presentation is started
- @observable presStatus: boolean = false;
- //back-up so that presentation stays the way it's when refreshed
- @observable presGroupBackUp: Doc = new Doc();
- @observable presButtonBackUp: Doc = new Doc();
-
- //Keeping track of the doc for the current presentation
- @observable curPresentation: Doc = new Doc();
- //Mapping of guids to presentations.
- @observable presentationsMapping: Map<String, Doc> = new Map();
- //Mapping of presentations to guid, so that select option values can be given.
- @observable presentationsKeyMapping: Map<Doc, String> = new Map();
- //Variable to keep track of guid of the current presentation
- @observable currentSelectedPresValue: string | undefined;
- //A flag to keep track if title input is open, which is used in rendering.
- @observable PresTitleInputOpen: boolean = false;
- //Variable that holds reference to title input, so that new presentations get titles assigned.
- @observable titleInputElement: HTMLInputElement | undefined;
- @observable PresTitleChangeOpen: boolean = false;
- @observable presMode: boolean = false;
-
-
- @observable opacity = 1;
- @observable persistOpacity = true;
- @observable labelOpacity = 0;
-
- //initilize class variables
- constructor(props: PresViewProps) {
- super(props);
- PresentationView.Instance = this;
- }
-
- @action
- toggle = (forcedValue: boolean | undefined) => {
- if (forcedValue !== undefined) {
- this.curPresentation.width = forcedValue ? expandedWidth : 0;
- } else {
- this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
- }
- }
-
- //The first lifecycle function that gets called to set up the current presentation.
- async componentWillMount() {
-
- this.props.Documents.forEach(async (doc, index: number) => {
-
- //For each presentation received from mainContainer, a mapping is created.
- let curDoc: Doc = await doc;
- let newGuid = Utils.GenerateGuid();
- this.presentationsKeyMapping.set(curDoc, newGuid);
- this.presentationsMapping.set(newGuid, curDoc);
-
- //The Presentation at first index gets set as default start presentation
- if (index === 0) {
- runInAction(() => this.currentSelectedPresValue = newGuid);
- runInAction(() => this.curPresentation = curDoc);
- }
- });
- }
-
- //Second lifecycle function that gets called when component mounts. It makes sure to
- //get the back-up information from previous session for the current presentation.
- async componentDidMount() {
- let docAtZero = await this.props.Documents[0];
- runInAction(() => this.curPresentation = docAtZero);
-
- this.setPresentationBackUps();
-
- }
-
-
- /**
- * The function that retrieves the backUps for the current Presentation if present,
- * otherwise initializes.
- */
- setPresentationBackUps = async () => {
- //getting both backUp documents
-
- let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc);
- let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc);
- //if instantiated before
- if (castedGroupBackUp instanceof Promise) {
- castedGroupBackUp.then(doc => {
- let toAssign = doc ? doc : new Doc();
- this.curPresentation.presGroupBackUp = toAssign;
- runInAction(() => this.presGroupBackUp = toAssign);
- if (doc) {
- if (toAssign[Id] === doc[Id]) {
- this.retrieveGroupMappings();
- }
- }
- });
-
- //if never instantiated a store doc yet
- } else if (castedGroupBackUp instanceof Doc) {
- let castedDoc: Doc = await castedGroupBackUp;
- runInAction(() => this.presGroupBackUp = castedDoc);
- this.retrieveGroupMappings();
- } else {
- runInAction(() => {
- let toAssign = new Doc();
- this.presGroupBackUp = toAssign;
- this.curPresentation.presGroupBackUp = toAssign;
-
- });
-
- }
- //if instantiated before
- if (castedButtonBackUp instanceof Promise) {
- castedButtonBackUp.then(doc => {
- let toAssign = doc ? doc : new Doc();
- this.curPresentation.presButtonBackUp = toAssign;
- runInAction(() => this.presButtonBackUp = toAssign);
- });
-
- //if never instantiated a store doc yet
- } else if (castedButtonBackUp instanceof Doc) {
- let castedDoc: Doc = await castedButtonBackUp;
- runInAction(() => this.presButtonBackUp = castedDoc);
-
- } else {
- runInAction(() => {
- let toAssign = new Doc();
- this.presButtonBackUp = toAssign;
- this.curPresentation.presButtonBackUp = toAssign;
- });
-
- }
-
-
- //storing the presentation status,ie. whether it was stopped or playing
- let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
- runInAction(() => this.presStatus = presStatusBackUp);
- }
-
- /**
- * This is the function that is called to retrieve the groups that have been stored and
- * push them to the groupMappings.
- */
- retrieveGroupMappings = async () => {
- let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
- if (castedGroupDocs !== undefined) {
- castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
- let castedGrouping = await DocListCastAsync(groupDoc.grouping);
- let castedKey = StrCast(groupDoc.presentIdStore, null);
- if (castedGrouping) {
- castedGrouping.forEach((doc: Doc) => {
- doc.presentId = castedKey;
- });
- }
- if (castedGrouping !== undefined && castedKey !== undefined) {
- this.groupMappings.set(castedKey, castedGrouping);
- }
- });
- }
- }
-
- //observable means render is re-called every time variable is changed
- @observable
- collapsed: boolean = false;
- closePresentation = action(() => this.curPresentation.width = 0);
- next = async () => {
- const current = NumCast(this.curPresentation.selectedDoc);
- //asking to get document at current index
- let docAtCurrentNext = await this.getDocAtIndex(current + 1);
- if (docAtCurrentNext === undefined) {
- return;
- }
- //asking for it's presentation id
- let curNextPresId = StrCast(docAtCurrentNext.presentId);
- let nextSelected = current + 1;
-
- //if curDoc is in a group, selection slides until last one, if not it's next one
- if (this.groupMappings.has(curNextPresId)) {
- let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!;
- nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext);
-
- //end of grup so go beyond
- if (nextSelected === current) nextSelected = current + 1;
- }
-
- this.gotoDocument(nextSelected, current);
-
- }
- back = async () => {
- const current = NumCast(this.curPresentation.selectedDoc);
- //requesting for the doc at current index
- let docAtCurrent = await this.getDocAtIndex(current);
- if (docAtCurrent === undefined) {
- return;
- }
-
- //asking for its presentation id.
- let curPresId = StrCast(docAtCurrent.presentId);
- let prevSelected = current - 1;
- let zoomOut: boolean = false;
-
- //checking if this presentation id is mapped to a group, if so chosing the first element in group
- if (this.groupMappings.has(curPresId)) {
- let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!;
- prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1;
- //end of grup so go beyond
- if (prevSelected === current) prevSelected = current - 1;
-
- //checking if any of the group members had used zooming in
- currentsArray.forEach((doc: Doc) => {
- //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
- if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) {
- zoomOut = true;
- return;
- }
- });
-
- }
-
- // if a group set that flag to zero or a single element
- //If so making sure to zoom out, which goes back to state before zooming action
- if (current > 0) {
- if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) {
- let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
- if (prevScale !== undefined) {
- if (prevScale !== curScale) {
- DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
- }
- }
- }
- }
- this.gotoDocument(prevSelected, current);
-
- }
-
- /**
- * This is the method that checks for the actions that need to be performed
- * after the document has been presented, which involves 3 button options:
- * Hide Until Presented, Hide After Presented, Fade After Presented
- */
- showAfterPresented = (index: number) => {
- this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
- let selectedButtons: boolean[] = presElem.selected;
- //the order of cases is aligned based on priority
- if (selectedButtons[buttonIndex.HideTillPressed]) {
- if (this.childrenDocs.indexOf(key) <= index) {
- key.opacity = 1;
- }
- }
- if (selectedButtons[buttonIndex.HideAfter]) {
- if (this.childrenDocs.indexOf(key) < index) {
- key.opacity = 0;
- }
- }
- if (selectedButtons[buttonIndex.FadeAfter]) {
- if (this.childrenDocs.indexOf(key) < index) {
- key.opacity = 0.5;
- }
- }
- });
- }
-
- /**
- * This is the method that checks for the actions that need to be performed
- * before the document has been presented, which involves 3 button options:
- * Hide Until Presented, Hide After Presented, Fade After Presented
- */
- hideIfNotPresented = (index: number) => {
- this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
- let selectedButtons: boolean[] = presElem.selected;
-
- //the order of cases is aligned based on priority
-
- if (selectedButtons[buttonIndex.HideAfter]) {
- if (this.childrenDocs.indexOf(key) >= index) {
- key.opacity = 1;
- }
- }
- if (selectedButtons[buttonIndex.FadeAfter]) {
- if (this.childrenDocs.indexOf(key) >= index) {
- key.opacity = 1;
- }
- }
- if (selectedButtons[buttonIndex.HideTillPressed]) {
- if (this.childrenDocs.indexOf(key) > index) {
- key.opacity = 0;
- }
- }
- });
- }
-
- /**
- * This method makes sure that cursor navigates to the element that
- * has the option open and last in the group. If not in the group, and it has
- * te option open, navigates to that element.
- */
- navigateToElement = async (curDoc: Doc, fromDoc: number) => {
- let docToJump: Doc = curDoc;
- let curDocPresId = StrCast(curDoc.presentId, null);
- let willZoom: boolean = false;
-
- //checking if in group
- if (curDocPresId !== undefined) {
- if (this.groupMappings.has(curDocPresId)) {
- let currentDocGroup = this.groupMappings.get(curDocPresId)!;
- currentDocGroup.forEach((doc: Doc, index: number) => {
- let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected;
- if (selectedButtons[buttonIndex.Navigate]) {
- docToJump = doc;
- willZoom = false;
- }
- if (selectedButtons[buttonIndex.Show]) {
- docToJump = doc;
- willZoom = true;
- }
- });
- }
-
- }
- //docToJump stayed same meaning, it was not in the group or was the last element in the group
- if (docToJump === curDoc) {
- //checking if curDoc has navigation open
- let curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
- if (curDocButtons[buttonIndex.Navigate]) {
- this.jumpToTabOrRight(curDocButtons, curDoc);
- } else if (curDocButtons[buttonIndex.Show]) {
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
- if (curDocButtons[buttonIndex.OpenRight]) {
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(curDoc, true);
- } else {
- await DocumentManager.Instance.jumpToDocument(curDoc, true, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
- }
-
- let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
- curDoc.viewScale = newScale;
-
- //saving the scale user was on before zooming in
- if (curScale !== 1) {
- this.childrenDocs[fromDoc].viewScale = curScale;
- }
-
- }
- return;
- }
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
- let curDocButtons = this.presElementsMappings.get(docToJump)!.selected;
-
-
- if (curDocButtons[buttonIndex.OpenRight]) {
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
- } else {
- await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
- }
- let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
- curDoc.viewScale = newScale;
- //saving the scale that user was on
- if (curScale !== 1) {
- this.childrenDocs[fromDoc].viewScale = curScale;
- }
-
- }
-
- /**
- * This function checks if right option is clicked on a presentation element, if not it does open it as a tab
- * with help of CollectionDockingView.
- */
- jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => {
- if (curDocButtons[buttonIndex.OpenRight]) {
- DocumentManager.Instance.jumpToDocument(curDoc, false);
- } else {
- DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
- }
- }
-
- /**
- * Async function that supposedly return the doc that is located at given index.
- */
- getDocAtIndex = async (index: number) => {
- const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (!list) {
- return undefined;
- }
- if (index < 0 || index >= list.length) {
- return undefined;
- }
-
- this.curPresentation.selectedDoc = index;
- //awaiting async call to finish to get Doc instance
- const doc = await list[index];
- return doc;
- }
-
- /**
- * The function that removes a doc from a presentation. It also makes sure to
- * do necessary updates to backUps and mappings stored locally.
- */
- @action
- public RemoveDoc = async (index: number) => {
- const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (value) {
- let removedDoc = await value.splice(index, 1)[0];
-
- //removing the Presentation Element stored for it
- this.presElementsMappings.delete(removedDoc);
-
- let removedDocPresentId = StrCast(removedDoc.presentId);
-
- //Removing it from local mapping of the groups
- if (this.groupMappings.has(removedDocPresentId)) {
- let removedDocsGroup = this.groupMappings.get(removedDocPresentId);
- if (removedDocsGroup) {
- removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1);
- if (removedDocsGroup.length === 0) {
- this.groupMappings.delete(removedDocPresentId);
- }
- }
- }
-
-
- let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
- if (castedList) {
- for (let doc of castedList) {
- let curDoc = await doc;
- let curDocId = StrCast(curDoc.docId);
- if (curDocId === removedDoc[Id]) {
- castedList.splice(castedList.indexOf(curDoc), 1);
- break;
-
- }
- }
- }
-
- //removing it from the backup of groups
- let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
- if (castedGroupDocs) {
- castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
- let castedKey = StrCast(groupDoc.presentIdStore, null);
- if (castedKey === removedDocPresentId) {
- let castedGrouping = await DocListCastAsync(groupDoc.grouping);
- if (castedGrouping) {
- castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1);
- if (castedGrouping.length === 0) {
- castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1);
- }
- }
- }
-
- });
-
- }
-
-
- }
- }
-
- /**
- * An alternative remove method that removes a doc from presentation by its actual
- * reference.
- */
- public removeDocByRef = (doc: Doc) => {
- let indexOfDoc = this.childrenDocs.indexOf(doc);
- const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (value) {
- value.splice(indexOfDoc, 1)[0];
- }
- if (indexOfDoc !== - 1) {
- return true;
- }
- return false;
- }
-
- //The function that is called when a document is clicked or reached through next or back.
- //it'll also execute the necessary actions if presentation is playing.
- @action
- public gotoDocument = async (index: number, fromDoc: number) => {
- const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (!list) {
- return;
- }
- if (index < 0 || index >= list.length) {
- return;
- }
- this.curPresentation.selectedDoc = index;
-
- if (!this.presStatus) {
- this.presStatus = true;
- this.startPresentation(index);
- }
-
- const doc = await list[index];
- if (this.presStatus) {
- this.navigateToElement(doc, fromDoc);
- this.hideIfNotPresented(index);
- this.showAfterPresented(index);
- }
-
- }
-
- //Function that is called to resetGroupIds, so that documents get new groupIds at
- //first load, when presentation is changed.
- resetGroupIds = async () => {
- let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
- if (castedGroupDocs !== undefined) {
- castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
- let castedGrouping = await DocListCastAsync(groupDoc.grouping);
- if (castedGrouping) {
- castedGrouping.forEach((doc: Doc) => {
- doc.presentId = Utils.GenerateGuid();
- });
- }
- });
- }
- runInAction(() => this.groupMappings = new Map());
- }
-
- /**
- * Adds a document to the presentation view
- **/
- @undoBatch
- @action
- public PinDoc(doc: Doc) {
- //add this new doc to props.Document
- const data = Cast(this.curPresentation.data, listSpec(Doc));
- if (data) {
- data.push(doc);
- } else {
- this.curPresentation.data = new List([doc]);
- }
-
- this.toggle(true);
- }
-
- //Function that sets the store of the children docs.
- @action
- setChildrenDocs = (docList: Doc[]) => {
- this.childrenDocs = docList;
- }
-
- //The function that is called to render the play or pause button depending on
- //if presentation is running or not.
- renderPlayPauseButton = () => {
- if (this.presStatus) {
- return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
- } else {
- return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
- }
- }
-
- //The function that starts or resets presentaton functionally, depending on status flag.
- @action
- startOrResetPres = async () => {
- if (this.presStatus) {
- this.resetPresentation();
- } else {
- this.presStatus = true;
- let startIndex = await this.findStartDocument();
- this.startPresentation(startIndex);
- const current = NumCast(this.curPresentation.selectedDoc);
- this.gotoDocument(startIndex, current);
- }
- this.curPresentation.presStatus = this.presStatus;
- }
-
- /**
- * This method is called to find the start document of presentation. So
- * that when user presses on play, the correct presentation element will be
- * selected.
- */
- findStartDocument = async () => {
- let docAtZero = await this.getDocAtIndex(0);
- if (docAtZero === undefined) {
- return 0;
- }
- let docAtZeroPresId = StrCast(docAtZero.presentId);
-
- if (this.groupMappings.has(docAtZeroPresId)) {
- let group = this.groupMappings.get(docAtZeroPresId)!;
- let lastDoc = group[group.length - 1];
- return this.childrenDocs.indexOf(lastDoc);
- } else {
- return 0;
- }
- }
-
- //The function that resets the presentation by removing every action done by it. It also
- //stops the presentaton.
- @action
- resetPresentation = () => {
- this.childrenDocs.forEach((doc: Doc) => {
- doc.opacity = 1;
- doc.viewScale = 1;
- });
- this.curPresentation.selectedDoc = 0;
- this.presStatus = false;
- this.curPresentation.presStatus = this.presStatus;
- if (this.childrenDocs.length === 0) {
- return;
- }
- DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
- }
-
-
- //The function that starts the presentation, also checking if actions should be applied
- //directly at start.
- startPresentation = (startIndex: number) => {
- let selectedButtons: boolean[];
- this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
- selectedButtons = component.selected;
- if (selectedButtons[buttonIndex.HideTillPressed]) {
- if (this.childrenDocs.indexOf(doc) > startIndex) {
- doc.opacity = 0;
- }
-
- }
- if (selectedButtons[buttonIndex.HideAfter]) {
- if (this.childrenDocs.indexOf(doc) < startIndex) {
- doc.opacity = 0;
- }
- }
- if (selectedButtons[buttonIndex.FadeAfter]) {
- if (this.childrenDocs.indexOf(doc) < startIndex) {
- doc.opacity = 0.5;
- }
- }
-
- });
-
- }
-
- /**
- * The function that is called to add a new presentation to the presentationView.
- * It sets up te mappings and local copies of it. Resets the groupings and presentation.
- * Makes the new presentation current selected, and retrieve the back-Ups if present.
- */
- @action
- addNewPresentation = (presTitle: string) => {
- //creating a new presentation doc
- let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
- this.props.Documents.push(newPresentationDoc);
-
- //setting that new doc as current
- this.curPresentation = newPresentationDoc;
-
- //storing the doc in local copies for easier access
- let newGuid = Utils.GenerateGuid();
- this.presentationsMapping.set(newGuid, newPresentationDoc);
- this.presentationsKeyMapping.set(newPresentationDoc, newGuid);
-
- //resetting the previous presentation's actions so that new presentation can be loaded.
- this.resetGroupIds();
- this.resetPresentation();
- this.presElementsMappings = new Map();
- this.currentSelectedPresValue = newGuid;
- this.setPresentationBackUps();
-
- }
-
- /**
- * The function that is called to change the current selected presentation.
- * Changes the presentation, also resetting groupings and presentation in process.
- * Plus retrieving the backUps for the newly selected presentation.
- */
- @action
- getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => {
- //get the guid of the selected presentation
- let selectedGuid = e.target.value;
- //set that as current presentation
- this.curPresentation = this.presentationsMapping.get(selectedGuid)!;
-
- //reset current Presentations local things so that new one can be loaded
- this.resetGroupIds();
- this.resetPresentation();
- this.presElementsMappings = new Map();
- this.currentSelectedPresValue = selectedGuid;
- this.setPresentationBackUps();
-
-
- }
-
- /**
- * The function that is called to render either select for presentations, or title inputting.
- */
- renderSelectOrPresSelection = () => {
- let presentationList = DocListCast(this.props.Documents);
- if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
- return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
- } else {
- return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}>
- {presentationList.map((doc: Doc, index: number) => {
- let mappedGuid = this.presentationsKeyMapping.get(doc);
- let docGuid: string = mappedGuid ? mappedGuid.toString() : "";
- return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>;
- })}
- </select>;
- }
- }
-
- /**
- * The function that is called on enter press of title input. It gives the
- * new presentation the title user entered. If nothing is entered, gives a default title.
- */
- @action
- submitPresentationTitle = (e: React.KeyboardEvent) => {
- if (e.keyCode === 13) {
- let presTitle = this.titleInputElement!.value;
- this.titleInputElement!.value = "";
- if (this.PresTitleInputOpen) {
- if (presTitle === "") {
- presTitle = "Presentation";
- }
- this.PresTitleInputOpen = false;
- this.addNewPresentation(presTitle);
- } else if (this.PresTitleChangeOpen) {
- this.PresTitleChangeOpen = false;
- this.changePresentationTitle(presTitle);
- }
- }
- }
-
- /**
- * The function that is called to remove a presentation from all its copies, and the main Container's
- * list. Sets up the next presentation as current.
- */
- @action
- removePresentation = async () => {
- if (this.presentationsMapping.size !== 1) {
- let presentationList = Cast(this.props.Documents, listSpec(Doc));
- let batch = UndoManager.StartBatch("presRemoval");
-
- //getting the presentation that will be removed
- let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!);
- //that presentation is removed
- presentationList!.splice(presentationList!.indexOf(removedDoc!), 1);
-
- //its mappings are removed from local copies
- this.presentationsKeyMapping.delete(removedDoc!);
- this.presentationsMapping.delete(this.currentSelectedPresValue!);
-
- //the next presentation is set as current
- let remainingPresentations = this.presentationsMapping.values();
- let nextDoc = remainingPresentations.next().value;
- this.curPresentation = nextDoc;
-
-
- //Storing these for being able to undo changes
- let curGuid = this.currentSelectedPresValue!;
- let curPresStatus = this.presStatus;
-
- //resetting the groups and presentation actions so that next presentation gets loaded
- this.resetGroupIds();
- this.resetPresentation();
- this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
- this.setPresentationBackUps();
-
- //Storing for undo
- let currentGroups = this.groupMappings;
- let curPresElemMapping = this.presElementsMappings;
-
- //Event to undo actions that are not related to doc directly, aka. local things
- UndoManager.AddEvent({
- undo: action(() => {
- this.curPresentation = removedDoc!;
- this.presentationsMapping.set(curGuid, removedDoc!);
- this.presentationsKeyMapping.set(removedDoc!, curGuid);
- this.currentSelectedPresValue = curGuid;
-
- this.presStatus = curPresStatus;
- this.groupMappings = currentGroups;
- this.presElementsMappings = curPresElemMapping;
- this.setPresentationBackUps();
-
- }),
- redo: action(() => {
- this.curPresentation = nextDoc;
- this.presStatus = false;
- this.presentationsKeyMapping.delete(removedDoc!);
- this.presentationsMapping.delete(curGuid);
- this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
- this.setPresentationBackUps();
-
- }),
- });
-
- batch.end();
- }
- }
-
- /**
- * The function that is called to change title of presentation to what user entered.
- */
- @undoBatch
- changePresentationTitle = (newTitle: string) => {
- if (newTitle === "") {
- return;
- }
- this.curPresentation.title = newTitle;
- }
-
- /**
- * On pointer down element that is catched on resizer of te
- * presentation view. Sets up the event listeners to change the size with
- * mouse move.
- */
- _downsize = 0;
- onPointerDown = (e: React.PointerEvent) => {
- this._downsize = e.clientX;
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- e.stopPropagation();
- e.preventDefault();
- }
- /**
- * Changes the size of the presentation view, with mouse move.
- * Minimum size is set to 300, so that every button is visible.
- */
- @action
- onPointerMove = (e: PointerEvent) => {
-
- this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth);
- }
-
- /**
- * The method that is called on pointer up event. It checks if the button is just
- * clicked so that presentation view will be closed. The way it's done is to check
- * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger.
- */
- @action
- onPointerUp = (e: PointerEvent) => {
- if (Math.abs(e.clientX - this._downsize) < 4) {
- let presWidth = NumCast(this.curPresentation.width);
- if (presWidth - presMinWidth !== 0) {
- this.curPresentation.width = 0;
- }
- if (presWidth === 0) {
- this.curPresentation.width = presMinWidth;
- }
- }
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- /**
- * This function is a setter that opens up the
- * presentation mode, by setting it's render flag
- * to true. It also closes the presentation view.
- */
- @action
- openPresMode = () => {
- if (!this.presMode) {
- this.curPresentation.width = 0;
- this.presMode = true;
- }
- }
-
- /**
- * This function closes the presentation mode by setting its
- * render flag to false. It also opens up the presentation view.
- * By setting it to it's minimum size.
- */
- @action
- closePresMode = () => {
- if (this.presMode) {
- this.presMode = false;
- this.curPresentation.width = presMinWidth;
- }
-
- }
-
- /**
- * Function that is called to render the presentation mode, depending on its flag.
- */
- renderPresMode = () => {
- if (this.presMode) {
- return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />;
- } else {
- return (null);
- }
-
- }
-
- render() {
-
- let width = NumCast(this.curPresentation.width);
-
- return (
- <div>
- <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}>
- <div className="presentationView-heading">
- {this.renderSelectOrPresSelection()}
- <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
- <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button>
- <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
- runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
- runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
- }}><FontAwesomeIcon icon={"plus"} /></button>
- <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
- <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
- runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
- runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
- }}><FontAwesomeIcon icon={"edit"} /></button>
- </div>
- <div className="presentation-buttons">
- <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- {this.renderPlayPauseButton()}
- <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
- </div>
-
- <PresentationViewList
- mainDocument={this.curPresentation}
- deleteDocument={this.RemoveDoc}
- gotoDocument={this.gotoDocument}
- groupMappings={this.groupMappings}
- PresElementsMappings={this.presElementsMappings}
- setChildrenDocs={this.setChildrenDocs}
- presStatus={this.presStatus}
- presButtonBackUp={this.presButtonBackUp}
- presGroupBackUp={this.presGroupBackUp}
- removeDocByRef={this.removeDocByRef}
- clearElemMap={() => this.presElementsMappings.clear()}
- />
- <input
- type="checkbox"
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.persistOpacity = e.target.checked;
- this.opacity = this.persistOpacity ? 1 : 0.4;
- })}
- checked={this.persistOpacity}
- style={{ position: "absolute", bottom: 5, left: 5 }}
- onPointerEnter={action(() => this.labelOpacity = 1)}
- onPointerLeave={action(() => this.labelOpacity = 0)}
- />
- <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
- </div>
- <div className="mainView-libraryHandle"
- style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }}
- onPointerDown={this.onPointerDown}>
- <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
- </div>
- {this.renderPresMode()}
-
- </div>
- );
- }
-}
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 3e8582d61..c13d1d276 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -6,7 +6,7 @@ import { faTimes, faCheckCircle, faObjectGroup } from '@fortawesome/free-solid-s
import { library } from '@fortawesome/fontawesome-svg-core';
import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { DocumentType } from '../../documents/Documents';
+import { DocumentType } from "../../documents/DocumentTypes";
import { Cast, StrCast } from '../../../new_fields/Types';
import * as _ from "lodash";
import { IconBar } from './IconBar';
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index 4712b0abc..c9924222f 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -4,7 +4,6 @@ import { observable, action } from 'mobx';
// import "./SearchBox.scss";
import "./IconBar.scss";
import "./IconButton.scss";
-import { DocumentType } from '../../documents/Documents';
import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faTimesCircle, faCheckCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library } from '@fortawesome/fontawesome-svg-core';
diff --git a/src/client/views/search/IconButton.tsx b/src/client/views/search/IconButton.tsx
index 5d23f6eeb..d2cfe7fad 100644
--- a/src/client/views/search/IconButton.tsx
+++ b/src/client/views/search/IconButton.tsx
@@ -6,7 +6,7 @@ import "./IconButton.scss";
import { faSearch, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faMusic, faLink, faChartBar, faGlobeAsia, faBan, faVideo, faCaretDown } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { library, icon } from '@fortawesome/fontawesome-svg-core';
-import { DocumentType } from '../../documents/Documents';
+import { DocumentType } from "../../documents/DocumentTypes";
import '../globalCssVariables.scss';
import * as _ from "lodash";
import { IconBar } from './IconBar';
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index fcdc79220..5ed33a596 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -37,6 +37,11 @@
margin-left: 2px;
margin-right: 2px
}
+
+ &.searchBox-close {
+ color: $light-color;
+ max-height: 32px;
+ }
}
}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 2214ac8af..2ad69daca 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -4,6 +4,8 @@ import { observable, action, runInAction, flow, computed } from 'mobx';
import "./SearchBox.scss";
import "./FilterBox.scss";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
import { SetupDrag } from '../../util/DragManager';
import { Docs } from '../../documents/Documents';
import { NumCast, Cast } from '../../../new_fields/Types';
@@ -14,8 +16,12 @@ import { Id } from '../../../new_fields/FieldSymbols';
import { SearchUtil } from '../../util/SearchUtil';
import { RouteStore } from '../../../server/RouteStore';
import { FilterBox } from './FilterBox';
+import { ReadStream } from 'fs';
+import * as $ from 'jquery';
+import { MainView } from '../MainView';
import { Utils } from '../../../Utils';
+library.add(faTimes);
@observer
export class SearchBox extends React.Component {
@@ -29,6 +35,7 @@ export class SearchBox extends React.Component {
@observable private _visibleElements: JSX.Element[] = [];
private resultsRef = React.createRef<HTMLDivElement>();
+ public inputRef = React.createRef<HTMLInputElement>();
private _isSearch: ("search" | "placeholder" | undefined)[] = [];
private _numTotalResults = -1;
@@ -46,6 +53,15 @@ export class SearchBox extends React.Component {
this.resultsScrolled = this.resultsScrolled.bind(this);
}
+ componentDidMount = () => {
+ if (this.inputRef.current) {
+ this.inputRef.current.focus();
+ runInAction(() => {
+ this._searchbarOpen = true;
+ });
+ }
+ }
+
@action
getViews = async (doc: Doc) => {
const results = await SearchUtil.GetViewsOfDocument(doc);
@@ -321,11 +337,12 @@ export class SearchBox extends React.Component {
<span className="searchBox-barChild searchBox-collection" onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef} title="Drag Results as Collection">
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
- <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
<button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
<button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
+ <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button>
</div>
<div className="searchBox-results" onScroll={this.resultsScrolled} style={{
display: this._resultsOpen ? "flex" : "none",
@@ -336,5 +353,4 @@ export class SearchBox extends React.Component {
</div>
);
}
-
} \ No newline at end of file
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 8201aa374..672892fdf 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -11,7 +11,7 @@ import { RichTextField } from "../../../new_fields/RichTextField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
-import { DocumentType } from "../../documents/Documents";
+import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
import { LinkManager } from "../../util/LinkManager";
@@ -203,12 +203,12 @@ export class SearchItem extends React.Component<SearchItemProps> {
removeDocument={returnFalse}
ScreenToLocalTransform={Transform.Identity}
addDocTab={returnFalse}
+ pinToPres={returnFalse}
renderDepth={1}
PanelWidth={returnXDimension}
PanelHeight={returnYDimension}
focus={emptyFunction}
backgroundColor={returnEmptyString}
- selectOnLoad={false}
parentActive={returnFalse}
whenActiveChanged={returnFalse}
bringToFront={emptyFunction}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 543ee46cc..6f1ef38d1 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,19 +1,18 @@
-import { observable, action, runInAction } from "mobx";
-import { serializable, primitive, map, alias, list, PropSchema, custom } from "serializr";
-import { autoObject, SerializationHelper, Deserializable, afterDocDeserialize } from "../client/util/SerializationHelper";
+import { observable, ObservableMap, runInAction } from "mobx";
+import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
-import { setter, getter, getField, updateFunction, deleteProperty, makeEditable, makeReadOnly } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, BoolCast, StrCast } from "./Types";
-import { listSpec } from "./Schema";
-import { ObjectField } from "./ObjectField";
-import { RefField, FieldId } from "./RefField";
-import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
-import { scriptingGlobal, CompileScript, Scripting } from "../client/util/Scripting";
+import { DocumentType } from "../client/documents/DocumentTypes";
+import { CompileScript, Scripting, scriptingGlobal } from "../client/util/Scripting";
+import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
+import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, Update } from "./FieldSymbols";
import { List } from "./List";
-import { DocumentType } from "../client/documents/Documents";
-import { ComputedField, ScriptField } from "./ScriptField";
+import { ObjectField } from "./ObjectField";
import { PrefetchProxy, ProxyField } from "./Proxy";
-import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
+import { FieldId, RefField } from "./RefField";
+import { listSpec } from "./Schema";
+import { ComputedField } from "./ScriptField";
+import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
+import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -72,6 +71,7 @@ export const HeightSym = Symbol("Height");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
const CachedUpdates = Symbol("Cached updates");
+
function fetchProto(doc: Doc) {
const proto = doc.proto;
if (proto instanceof Promise) {
@@ -151,10 +151,10 @@ export class Doc extends RefField {
}
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
-
+ public static CurrentUserEmail: string = "";
public async [HandleUpdate](diff: any) {
const set = diff.$set;
- const sameAuthor = this.author === CurrentUserUtils.email;
+ const sameAuthor = this.author === Doc.CurrentUserEmail;
if (set) {
for (const key in set) {
if (!key.startsWith("fields.")) {
@@ -310,6 +310,10 @@ export namespace Doc {
export function GetProto(doc: Doc) {
return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc);
}
+ export function GetDataDoc(doc: Doc): Doc {
+ let proto = Doc.GetProto(doc);
+ return proto === doc ? proto : Doc.GetDataDoc(proto);
+ }
export function allKeys(doc: Doc): string[] {
const results: Set<string> = new Set;
@@ -323,14 +327,12 @@ export namespace Doc {
return Array.from(results);
}
- export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean) {
+ export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
if (target[key] === undefined) {
- console.log("target key undefined");
Doc.GetProto(target)[key] = new List<Doc>();
}
let list = Cast(target[key], listSpec(Doc));
if (list) {
- console.log("has list");
if (allowDuplicates !== true) {
let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
if (pind !== -1) {
@@ -338,15 +340,18 @@ export namespace Doc {
}
}
if (first) {
- console.log("is first");
list.splice(0, 0, doc);
}
else {
- console.log("not first");
let ind = relativeTo ? list.indexOf(relativeTo) : -1;
- if (ind === -1) list.push(doc);
- else list.splice(before ? ind : ind + 1, 0, doc);
- console.log("index", ind);
+ if (ind === -1) {
+ if (reversed) list.splice(0, 0, doc);
+ else list.push(doc);
+ }
+ else {
+ if (reversed) list.splice(before ? (list.length - ind) + 1 : list.length - ind, 0, doc);
+ else list.splice(before ? ind : ind + 1, 0, doc);
+ }
}
}
return true;
@@ -384,7 +389,7 @@ export namespace Doc {
docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
docExtensionForField.type = DocumentType.EXTENSION;
let proto: Doc | undefined = doc;
- while (proto && !Doc.IsPrototype(proto)) {
+ while (proto && !Doc.IsPrototype(proto) && proto.proto) {
proto = proto.proto;
}
(proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
@@ -441,21 +446,24 @@ export namespace Doc {
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
+ if (expandedTemplateLayout instanceof Promise) {
+ return undefined;
+ }
let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
if (expandedTemplateLayout === undefined) {
- setTimeout(() =>
- dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]"), 0);
+ setTimeout(() => dataDoc[expandedLayoutFieldKey] === undefined &&
+ (dataDoc[expandedLayoutFieldKey] = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]")), 0);
}
- return templateLayoutDoc; // use the templateLayout when it's not a template or the expandedTemplate is pending.
+ return undefined; // use the templateLayout when it's not a template or the expandedTemplate is pending.
}
export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
- let layoutDoc = childDocLayout;
- let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc ? dataDoc : undefined;
+ let layoutDoc: Doc | undefined = childDocLayout;
+ let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
let fieldExtensionDoc = Doc.resolvedFieldDataDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
@@ -503,7 +511,8 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
if (!templateDoc) return undefined;
- let otherdoc = new Doc();
+ let datadoc = new Doc();
+ let otherdoc = Doc.MakeDelegate(datadoc);
otherdoc.width = templateDoc[WidthSym]();
otherdoc.height = templateDoc[HeightSym]();
otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")";
@@ -511,15 +520,28 @@ export namespace Doc {
otherdoc.miniLayout = StrCast(templateDoc.miniLayout);
otherdoc.detailedLayout = otherdoc.layout;
otherdoc.type = DocumentType.TEMPLATE;
+ !templateDoc.nativeWidth && (otherdoc.nativeWidth = 0);
+ !templateDoc.nativeHeight && (otherdoc.nativeHeight = 0);
return otherdoc;
}
export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) {
+ if (!templateDoc) {
+ target.layout = undefined;
+ target.nativeWidth = undefined;
+ target.nativeHeight = undefined;
+ target.onClick = undefined;
+ target.type = undefined;
+ return;
+ }
let temp = Doc.MakeDelegate(templateDoc);
target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined;
target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined;
+ !templateDoc.nativeWidth && (target.nativeWidth = 0);
+ !templateDoc.nativeHeight && (target.nativeHeight = 0);
target.width = templateDoc.width;
target.height = templateDoc.height;
- Doc.GetProto(target).type = DocumentType.TEMPLATE;
+ target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
+ target.type = DocumentType.TEMPLATE;
if (targetData && targetData.layout === target) {
targetData.layout = temp;
targetData.miniLayout = StrCast(templateDoc.miniLayout);
@@ -567,7 +589,8 @@ export namespace Doc {
let miniLayout = await PromiseValue(d.miniLayout);
let detailLayout = await PromiseValue(d.detailedLayout);
d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);
- if (d.layout === detailLayout) Doc.GetProto(d).nativeWidth = Doc.GetProto(d).nativeHeight = undefined;
+ if (d.layout === detailLayout) d.nativeWidth = d.nativeHeight = 0;
+ if (StrCast(d.layout) !== "") d.nativeWidth = d.nativeHeight = undefined;
});
}
export function UseDetailLayout(d: Doc) {
@@ -585,24 +608,33 @@ export namespace Doc {
});
}
- export class DocBrush {
- @observable BrushedDoc: Doc[] = [];
+
+ export class DocData {
+ @observable _user_doc: Doc = undefined!;
+ @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
}
- const manager = new DocBrush();
+ const manager = new DocData();
+ export function UserDoc(): Doc { return manager._user_doc; }
+ export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
export function IsBrushed(doc: Doc) {
- return manager.BrushedDoc.some(d => Doc.AreProtosEqual(d, doc));
+ return manager.BrushedDoc.has(doc) || manager.BrushedDoc.has(Doc.GetDataDoc(doc));
}
export function IsBrushedDegree(doc: Doc) {
- return manager.BrushedDoc.some(d => d === doc) ? 2 : Doc.IsBrushed(doc) ? 1 : 0;
+ return manager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 2 : manager.BrushedDoc.has(doc) ? 1 : 0;
}
export function BrushDoc(doc: Doc) {
- if (manager.BrushedDoc.indexOf(doc) === -1) runInAction(() => manager.BrushedDoc.push(doc));
+ manager.BrushedDoc.set(doc, true);
+ manager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
}
export function UnBrushDoc(doc: Doc) {
- let index = manager.BrushedDoc.indexOf(doc);
- if (index !== -1) runInAction(() => manager.BrushedDoc.splice(index, 1));
+ manager.BrushedDoc.delete(doc);
+ manager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ }
+ export function UnBrushAllDocs() {
+ manager.BrushedDoc.clear();
}
}
-Scripting.addGlobal(function renameAlias(doc: any, n: any) {
- return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`;
-}); \ No newline at end of file
+Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`; });
+Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
+Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
+Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); }); \ No newline at end of file
diff --git a/src/new_fields/ListSpec.ts b/src/new_fields/ListSpec.ts
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/new_fields/ListSpec.ts
diff --git a/src/new_fields/PresField.ts b/src/new_fields/PresField.ts
new file mode 100644
index 000000000..f236a04fd
--- /dev/null
+++ b/src/new_fields/PresField.ts
@@ -0,0 +1,6 @@
+//insert code here
+import { ObjectField } from "./ObjectField";
+
+export abstract class PresField extends ObjectField {
+
+} \ No newline at end of file
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 89799b2af..1b52e6f82 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,6 +4,11 @@ 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 {
@@ -22,4 +27,49 @@ export class RichTextField extends ObjectField {
[ToScriptString]() {
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/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
index 23605cfb0..7494c9bd1 100644
--- a/src/new_fields/SchemaHeaderField.ts
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -1,8 +1,8 @@
import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, createSimpleSchema, primitive } from "serializr";
+import { serializable, primitive } from "serializr";
import { ObjectField } from "./ObjectField";
import { Copy, ToScriptString, OnUpdate } from "./FieldSymbols";
-import { scriptingGlobal, Scripting } from "../client/util/Scripting";
+import { scriptingGlobal } from "../client/util/Scripting";
import { ColumnType } from "../client/views/collections/CollectionSchemaView";
export const PastelSchemaPalette = new Map<string, string>([
@@ -53,9 +53,11 @@ export class SchemaHeaderField extends ObjectField {
@serializable(primitive())
width: number;
@serializable(primitive())
+ collapsed: boolean | undefined;
+ @serializable(primitive())
desc: boolean | undefined; // boolean determines sort order, undefined when no sort
- constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean) {
+ constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
super();
this.heading = heading;
@@ -63,6 +65,7 @@ export class SchemaHeaderField extends ObjectField {
this.type = type ? type : 0;
this.width = width ? width : -1;
this.desc = desc;
+ this.collapsed = collapsed;
}
setHeading(heading: string) {
@@ -90,6 +93,11 @@ export class SchemaHeaderField extends ObjectField {
this[OnUpdate]();
}
+ setCollapsed(collapsed: boolean | undefined) {
+ this.collapsed = collapsed;
+ this[OnUpdate]();
+ }
+
[Copy]() {
return new SchemaHeaderField(this.heading, this.color, this.type);
}
diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts
index 09cbff25e..0ca35fab2 100644
--- a/src/new_fields/Types.ts
+++ b/src/new_fields/Types.ts
@@ -2,6 +2,7 @@ import { Field, Opt, FieldResult, Doc } from "./Doc";
import { List } from "./List";
import { RefField } from "./RefField";
import { DateField } from "./DateField";
+import { ScriptField } from "./ScriptField";
export type ToType<T extends InterfaceValue> =
T extends "string" ? string :
@@ -86,6 +87,9 @@ export function BoolCast(field: FieldResult, defaultVal: boolean | null = false)
export function DateCast(field: FieldResult) {
return Cast(field, DateField, null);
}
+export function ScriptCast(field: FieldResult) {
+ return Cast(field, ScriptField, null);
+}
type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefField ? (R | Promise<R>)[] : R[]) : T;
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index c546e2aac..04194509c 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -7,7 +7,6 @@ import { ObjectField } from "./ObjectField";
import { action } from "mobx";
import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
-import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -61,7 +60,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
}
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
- const sameAuthor = fromServer || (receiver.author === CurrentUserUtils.email);
+ const sameAuthor = fromServer || (receiver.author === Doc.CurrentUserEmail);
const writeToDoc = sameAuthor || (writeMode !== DocServer.WriteMode.LiveReadonly);
const writeToServer = sameAuthor || (writeMode === DocServer.WriteMode.Default);
if (writeToDoc) {
diff --git a/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud b/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud
new file mode 100644
index 000000000..f71886d8c
--- /dev/null
+++ b/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud
Binary files differ
diff --git a/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud
new file mode 100644
index 000000000..30ddb3091
--- /dev/null
+++ b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx
deleted file mode 100644
index a514926d2..000000000
--- a/src/scraping/buxton/source/Bill_Notes_NewO.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_OLPC.docx b/src/scraping/buxton/source/Bill_Notes_OLPC.docx
deleted file mode 100644
index 7a636e2d6..000000000
--- a/src/scraping/buxton/source/Bill_Notes_OLPC.docx
+++ /dev/null
Binary files differ
diff --git a/src/server/Message.ts b/src/server/Message.ts
index aaee143e8..4ec390ade 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,4 +1,5 @@
import { Utils } from "../Utils";
+import { google, docs_v1 } from "googleapis";
export class Message<T> {
private _name: string;
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index e30015e39..014906054 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -30,6 +30,7 @@ export enum RouteStore {
reset = "/reset/:token",
// APIS
- cognitiveServices = "/cognitiveservices"
+ cognitiveServices = "/cognitiveservices",
+ googleDocs = "/googleDocs"
} \ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
new file mode 100644
index 000000000..8785cd974
--- /dev/null
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -0,0 +1,130 @@
+import { google, docs_v1, slides_v1 } from "googleapis";
+import { createInterface } from "readline";
+import { readFile, writeFile } from "fs";
+import { OAuth2Client } from "google-auth-library";
+import { Opt } from "../../../new_fields/Doc";
+import { GlobalOptions } from "googleapis-common";
+import { GaxiosResponse } from "gaxios";
+
+/**
+ * Server side authentication for Google Api queries.
+ */
+export namespace GoogleApiServerUtils {
+
+ // If modifying these scopes, delete token.json.
+ const prefix = 'https://www.googleapis.com/auth/';
+ const SCOPES = [
+ 'documents.readonly',
+ 'documents',
+ 'presentations',
+ 'presentations.readonly',
+ 'drive',
+ 'drive.file',
+ ];
+
+ export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
+
+ export enum Service {
+ Documents = "Documents",
+ Slides = "Slides"
+ }
+
+
+ export interface CredentialPaths {
+ credentials: string;
+ token: string;
+ }
+
+ export type ApiResponse = Promise<GaxiosResponse>;
+ export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse;
+ export type ApiHandler = (parameters: any) => ApiResponse;
+ export type Action = "create" | "retrieve" | "update";
+
+ export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
+ export type EndpointParameters = GlobalOptions & { version: "v1" };
+
+ export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
+ return new Promise<Opt<Endpoint>>((resolve, reject) => {
+ readFile(paths.credentials, (err, credentials) => {
+ if (err) {
+ reject(err);
+ return console.log('Error loading client secret file:', err);
+ }
+ return authorize(parseBuffer(credentials), paths.token).then(auth => {
+ let routed: Opt<Endpoint>;
+ let parameters: EndpointParameters = { auth, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
+ });
+ });
+ });
+ };
+
+
+ /**
+ * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
+ * @param {Object} credentials The authorization client credentials.
+ */
+ export function authorize(credentials: any, token_path: string): Promise<OAuth2Client> {
+ const { client_secret, client_id, redirect_uris } = credentials.installed;
+ const oAuth2Client = new google.auth.OAuth2(
+ client_id, client_secret, redirect_uris[0]);
+
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ readFile(token_path, (err, token) => {
+ // Check if we have previously stored a token.
+ if (err) {
+ return getNewToken(oAuth2Client, token_path).then(resolve, reject);
+ }
+ oAuth2Client.setCredentials(parseBuffer(token));
+ resolve(oAuth2Client);
+ });
+ });
+ }
+
+ /**
+ * Get and store new token after prompting for user authorization, and then
+ * execute the given callback with the authorized OAuth2 client.
+ * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
+ * @param {getEventsCallback} callback The callback for the authorized client.
+ */
+ function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ const authUrl = oAuth2Client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES.map(relative => prefix + relative),
+ });
+ console.log('Authorize this app by visiting this url:', authUrl);
+ const rl = createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ rl.question('Enter the code from that page here: ', (code) => {
+ rl.close();
+ oAuth2Client.getToken(code, (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ // Store the token to disk for later program executions
+ writeFile(token_path, JSON.stringify(token), (err) => {
+ if (err) {
+ console.error(err);
+ reject(err);
+ }
+ console.log('Token stored to', token_path);
+ });
+ resolve(oAuth2Client);
+ });
+ });
+ });
+ }
+} \ No newline at end of file
diff --git a/src/server/youtubeApi/youtubeApiSample.d.ts b/src/server/apis/youtube/youtubeApiSample.d.ts
index 427f54608..427f54608 100644
--- a/src/server/youtubeApi/youtubeApiSample.d.ts
+++ b/src/server/apis/youtube/youtubeApiSample.d.ts
diff --git a/src/server/youtubeApi/youtubeApiSample.js b/src/server/apis/youtube/youtubeApiSample.js
index 50b3c7b38..50b3c7b38 100644
--- a/src/server/youtubeApi/youtubeApiSample.js
+++ b/src/server/apis/youtube/youtubeApiSample.js
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index d42741410..10b17de71 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -42,9 +42,11 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
const provider = req.path.split("/").slice(-1)[0];
- if (_.find(req.user.tokens, { kind: provider })) {
- next();
- } else {
- res.redirect(`/auth/${provider}`);
+ if (req.user) {
+ if (_.find((req.user as any).tokens, { kind: provider })) {
+ next();
+ } else {
+ res.redirect(`/auth/${provider}`);
+ }
}
}; \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 91d7ba87d..83e45d3ce 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -10,20 +10,17 @@ import { CollectionView } from "../../../client/views/collections/CollectionView
import { Doc } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
-import { RouteStore } from "../../RouteStore";
+import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
+import { RouteStore } from "../../RouteStore";
export class CurrentUserUtils {
- private static curr_email: string;
private static curr_id: string;
- @observable private static user_document: Doc;
//TODO tfs: these should be temporary...
private static mainDocId: string | undefined;
- public static get email() { return this.curr_email; }
public static get id() { return this.curr_id; }
- @computed public static get UserDocument() { return this.user_document; }
+ @computed public static get UserDocument() { return Doc.UserDoc(); }
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
@@ -32,7 +29,7 @@ export class CurrentUserUtils {
doc.viewType = CollectionViewType.Tree;
doc.dropAction = "alias";
doc.layout = CollectionView.LayoutString();
- doc.title = this.email;
+ doc.title = Doc.CurrentUserEmail;
this.updateUserDocument(doc);
doc.data = new List<Doc>();
doc.gridGap = 5;
@@ -41,8 +38,6 @@ export class CurrentUserUtils {
doc.boxShadow = "0 0";
doc.excludeFromLibrary = true;
doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
- // doc.library = Docs.Create.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` });
- // (doc.library as Doc).excludeFromLibrary = true;
return doc;
}
@@ -54,12 +49,20 @@ export class CurrentUserUtils {
workspaces.boxShadow = "0 0";
doc.workspaces = workspaces;
}
+ PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => workspaces && (workspaces.preventTreeViewOpen = true));
if (doc.recentlyClosed === undefined) {
const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
recentlyClosed.excludeFromLibrary = true;
recentlyClosed.boxShadow = "0 0";
doc.recentlyClosed = recentlyClosed;
}
+ PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => recent && (recent.preventTreeViewOpen = true));
+ if (doc.curPresentation === undefined) {
+ const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
+ curPresentation.excludeFromLibrary = true;
+ curPresentation.boxShadow = "0 0";
+ doc.curPresentation = curPresentation;
+ }
if (doc.sidebar === undefined) {
const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
sidebar.excludeFromLibrary = true;
@@ -70,8 +73,15 @@ export class CurrentUserUtils {
sidebar.boxShadow = "1 1 3";
doc.sidebar = sidebar;
}
+ if (doc.overlays === undefined) {
+ const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
+ overlays.excludeFromLibrary = true;
+ Doc.GetProto(overlays).backgroundColor = "#aca3a6";
+ doc.overlays = overlays;
+ }
StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
doc.width = 100;
+ doc.preventTreeViewOpen = true;
}
public static loadCurrentUser() {
@@ -87,15 +97,15 @@ export class CurrentUserUtils {
public static async loadUserDocument({ id, email }: { id: string, email: string }) {
this.curr_id = id;
- this.curr_email = email;
+ Doc.CurrentUserEmail = email;
await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
if (id) {
return DocServer.GetRefField(id).then(async field => {
if (field instanceof Doc) {
await this.updateUserDocument(field);
- runInAction(() => this.user_document = field);
+ runInAction(() => Doc.SetUserDoc(field));
} else {
- runInAction(() => this.user_document = this.createUserDocument(id));
+ runInAction(() => Doc.SetUserDoc(this.createUserDocument(id)));
}
});
} else {
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
new file mode 100644
index 000000000..8d097d363
--- /dev/null
+++ b/src/server/credentials/google_docs_credentials.json
@@ -0,0 +1 @@
+{"installed":{"client_id":"343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com","project_id":"quickstart-1565056383187","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"w8KIFSc0MQpmUYHed4qEzn8b","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
new file mode 100644
index 000000000..07c02d56c
--- /dev/null
+++ b/src/server/credentials/google_docs_token.json
@@ -0,0 +1 @@
+{"access_token":"ya29.GltjB4-x03xFpd2NY2555cxg1xlT_ajqRi78M9osOfdOF2jTIjlPkn_UZL8cUwVP0DPC8rH3vhhg8RpspFe8Vewx92shAO3RPos_uMH0CUqEiCiZlaaB5I3Jq3Mv","refresh_token":"1/teUKUqGKMLjVqs-eed0L8omI02pzSxMUYaxGc2QxBw0","scope":"https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1565654175862} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 7a81e362e..e1ecc4ac0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -14,7 +14,6 @@ import * as mobileDetect from 'mobile-detect';
import * as passport from 'passport';
import * as path from 'path';
import * as request from 'request';
-import * as rp from 'request-promise';
import * as io from 'socket.io';
import { Socket } from 'socket.io';
import * as webpack from 'webpack';
@@ -36,21 +35,22 @@ const port = 1050; // default port to listen
const serverPort = 4321;
import expressFlash = require('express-flash');
import flash = require('connect-flash');
-import c = require("crypto");
import { Search } from './Search';
-import { debug } from 'util';
import _ = require('lodash');
import * as Archiver from 'archiver';
-import * as AdmZip from 'adm-zip';
-import * as YoutubeApi from './youtubeApi/youtubeApiSample.js';
+var AdmZip = require('adm-zip');
+import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
import { Response } from 'express-serve-static-core';
import { DocComponent } from '../client/views/DocComponent';
import { Recommender } from "./Recommender";
+import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
+import { GaxiosResponse } from 'gaxios';
+import { Opt } from '../new_fields/Doc';
+import { docs_v1 } from 'googleapis';
+import { Endpoint } from 'googleapis-common';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const probe = require("probe-image-size");
-var SolrNode = require('solr-node');
-var shell = require('shelljs');
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
@@ -118,7 +118,7 @@ function addSecureRoute(method: Method,
) {
let abstracted = (req: express.Request, res: express.Response) => {
if (req.user) {
- handler(req.user, res, req);
+ handler(req.user as any, res, req);
} else {
req.session!.target = req.originalUrl;
onRejection(res, req);
@@ -176,6 +176,13 @@ const read_text_file = (relativePath: string) => {
});
};
+const write_text_file = (relativePath: string, contents: any) => {
+ let target = path.join(__dirname, relativePath);
+ return new Promise<void>((resolve, reject) => {
+ fs.writeFile(target, contents, (err) => err ? reject(err) : resolve());
+ });
+};
+
app.get("/version", (req, res) => {
exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => {
if (err) {
@@ -363,13 +370,17 @@ app.post("/uploadDoc", (req, res) => {
// zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false);
// zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false);
// zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false);
- zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
- dirname = "/" + dirname;
-
- fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname));
- fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname));
- fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname));
- fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname));
+ try {
+ zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false);
+ dirname = "/" + dirname;
+
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname));
+ fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname));
+ } catch (e) {
+ console.log(e);
+ }
});
const json = zip.getEntry("doc.json");
let docs: any;
@@ -807,6 +818,32 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any
}
}
+const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json");
+const token = path.join(__dirname, "./credentials/google_docs_token.json");
+
+const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
+ ["create", (api, params) => api.create(params)],
+ ["retrieve", (api, params) => api.get(params)],
+ ["update", (api, params) => api.batchUpdate(params)],
+]);
+
+app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
+ let sector: any = req.params.sector;
+ let action: any = req.params.action;
+ GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => {
+ let handler = EndpointHandlerMap.get(action);
+ if (endpoint && handler) {
+ let execute = handler(endpoint, req.body).then(
+ response => res.send(response.data),
+ rejection => res.send(rejection)
+ );
+ execute.catch(exception => res.send(exception));
+ return;
+ }
+ res.send(undefined);
+ });
+});
+
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
diff --git a/src/server/slides.json b/src/server/slides.json
new file mode 100644
index 000000000..323cac3a6
--- /dev/null
+++ b/src/server/slides.json
@@ -0,0 +1,10820 @@
+{
+ "presentationId": "1gHxyT6bBhsPVeuWNnWDzI33yEviMVo8n60JtZiVy3tY",
+ "pageSize": {
+ "width": {
+ "magnitude": 9144000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 5143500,
+ "unit": "EMU"
+ }
+ },
+ "slides": [
+ {
+ "objectId": "p",
+ "pageElements": [
+ {
+ "objectId": "i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.6842,
+ "translateX": 311708.35000000003,
+ "translateY": 744575,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 20,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 20,
+ "textRun": {
+ "content": "Importing into Dash\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "CENTERED_TITLE",
+ "parentObjectId": "p2_i0"
+ }
+ }
+ },
+ {
+ "objectId": "i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.2642,
+ "translateX": 311700,
+ "translateY": 2834125,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 15,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 15,
+ "textRun": {
+ "content": "By Sam Wilkins\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SUBTITLE",
+ "parentObjectId": "p2_i1"
+ }
+ }
+ }
+ ],
+ "slideProperties": {
+ "layoutObjectId": "p2",
+ "masterObjectId": "simple-light-2",
+ "notesPage": {
+ "objectId": "p:notes",
+ "pageType": "NOTES",
+ "pageElements": [
+ {
+ "objectId": "i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.032025,
+ "scaleY": 1.143,
+ "translateX": 381300,
+ "translateY": 685800,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeProperties": {
+ "outline": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_IMAGE",
+ "parentObjectId": "n:slide"
+ }
+ }
+ },
+ {
+ "objectId": "i3",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.8288,
+ "scaleY": 1.3716,
+ "translateX": 685800,
+ "translateY": 4343400,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "index": 1,
+ "parentObjectId": "n:text"
+ }
+ }
+ }
+ ],
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "notesProperties": {
+ "speakerNotesObjectId": "i3"
+ }
+ }
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "g5f40953d50_0_0",
+ "pageElements": [
+ {
+ "objectId": "g5f40953d50_0_1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.1909,
+ "translateX": 311700,
+ "translateY": 445025,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 10,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 10,
+ "textRun": {
+ "content": "Dr. Seuss\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p4_i0"
+ }
+ }
+ },
+ {
+ "objectId": "g5f40953d50_0_2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 1.1388,
+ "translateX": 311700,
+ "translateY": 1152475,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 25,
+ "paragraphMarker": {
+ "style": {
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 25,
+ "textRun": {
+ "content": "Here is a bulleted list!\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 25,
+ "endIndex": 34,
+ "paragraphMarker": {
+ "style": {
+ "indentStart": {
+ "magnitude": 36,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "kix.wifbmqnyqu4p",
+ "glyph": "●",
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 25,
+ "endIndex": 34,
+ "textRun": {
+ "content": "One fish\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 34,
+ "endIndex": 43,
+ "paragraphMarker": {
+ "style": {
+ "indentStart": {
+ "magnitude": 36,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "kix.wifbmqnyqu4p",
+ "glyph": "●",
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 34,
+ "endIndex": 43,
+ "textRun": {
+ "content": "Two fish\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 43,
+ "endIndex": 52,
+ "paragraphMarker": {
+ "style": {
+ "indentStart": {
+ "magnitude": 36,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "kix.wifbmqnyqu4p",
+ "glyph": "●",
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 43,
+ "endIndex": 52,
+ "textRun": {
+ "content": "Red fish\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 52,
+ "endIndex": 62,
+ "paragraphMarker": {
+ "style": {
+ "indentStart": {
+ "magnitude": 36,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "kix.wifbmqnyqu4p",
+ "glyph": "●",
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 52,
+ "endIndex": 62,
+ "textRun": {
+ "content": "Blue fish\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "kix.wifbmqnyqu4p": {
+ "listId": "kix.wifbmqnyqu4p",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ },
+ "kix.yuy8atv38lqp": {
+ "listId": "kix.yuy8atv38lqp",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "underline": false
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "underline": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p4_i1"
+ }
+ }
+ }
+ ],
+ "slideProperties": {
+ "layoutObjectId": "p4",
+ "masterObjectId": "simple-light-2",
+ "notesPage": {
+ "objectId": "g5f40953d50_0_0:notes",
+ "pageType": "NOTES",
+ "pageElements": [
+ {
+ "objectId": "g5f40953d50_0_3",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.032,
+ "scaleY": 1.143,
+ "translateX": 381300,
+ "translateY": 685800,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeProperties": {
+ "outline": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_IMAGE",
+ "parentObjectId": "n:slide"
+ }
+ }
+ },
+ {
+ "objectId": "g5f40953d50_0_4",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.8288,
+ "scaleY": 1.3716,
+ "translateX": 685800,
+ "translateY": 4343400,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "index": 1,
+ "parentObjectId": "n:text"
+ }
+ }
+ }
+ ],
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "notesProperties": {
+ "speakerNotesObjectId": "g5f40953d50_0_4"
+ }
+ }
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ }
+ ],
+ "title": "THIS IS MY FIRST DASH PRESENTATION",
+ "masters": [
+ {
+ "objectId": "simple-light-2",
+ "pageType": "MASTER",
+ "pageElements": [
+ {
+ "objectId": "p1_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.1909,
+ "translateX": 311700,
+ "translateY": 445025,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK1"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "NOT_RENDERED",
+ "solidFill": {
+ "color": {
+ "rgbColor": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ "alpha": 1
+ }
+ },
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID",
+ "propertyState": "NOT_RENDERED"
+ },
+ "shadow": {
+ "type": "OUTER",
+ "transform": {
+ "scaleX": 1,
+ "scaleY": 1,
+ "unit": "EMU"
+ },
+ "alignment": "BOTTOM_LEFT",
+ "blurRadius": {
+ "unit": "EMU"
+ },
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1,
+ "rotateWithShape": false,
+ "propertyState": "NOT_RENDERED"
+ },
+ "contentAlignment": "TOP"
+ },
+ "placeholder": {
+ "type": "TITLE"
+ }
+ }
+ },
+ {
+ "objectId": "p1_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 1.1388,
+ "translateX": 311700,
+ "translateY": 1152475,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 115,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "magnitude": 16,
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "NOT_RENDERED",
+ "solidFill": {
+ "color": {
+ "rgbColor": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ "alpha": 1
+ }
+ },
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID",
+ "propertyState": "NOT_RENDERED"
+ },
+ "shadow": {
+ "type": "OUTER",
+ "transform": {
+ "scaleX": 1,
+ "scaleY": 1,
+ "unit": "EMU"
+ },
+ "alignment": "BOTTOM_LEFT",
+ "blurRadius": {
+ "unit": "EMU"
+ },
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1,
+ "rotateWithShape": false,
+ "propertyState": "NOT_RENDERED"
+ },
+ "contentAlignment": "TOP"
+ },
+ "placeholder": {
+ "type": "BODY"
+ }
+ }
+ },
+ {
+ "objectId": "p1_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "END",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "NEVER_COLLAPSE"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 10,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "themeColor": "DARK2"
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 10,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "NOT_RENDERED",
+ "solidFill": {
+ "color": {
+ "rgbColor": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ "alpha": 1
+ }
+ },
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID",
+ "propertyState": "NOT_RENDERED"
+ },
+ "shadow": {
+ "type": "OUTER",
+ "transform": {
+ "scaleX": 1,
+ "scaleY": 1,
+ "unit": "EMU"
+ },
+ "alignment": "BOTTOM_LEFT",
+ "blurRadius": {
+ "unit": "EMU"
+ },
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1,
+ "rotateWithShape": false,
+ "propertyState": "NOT_RENDERED"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER"
+ }
+ }
+ }
+ ],
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "solidFill": {
+ "color": {
+ "themeColor": "LIGHT1"
+ },
+ "alpha": 1
+ }
+ },
+ "colorScheme": {
+ "colors": [
+ {
+ "type": "DARK1",
+ "color": {}
+ },
+ {
+ "type": "LIGHT1",
+ "color": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ {
+ "type": "DARK2",
+ "color": {
+ "red": 0.34901962,
+ "green": 0.34901962,
+ "blue": 0.34901962
+ }
+ },
+ {
+ "type": "LIGHT2",
+ "color": {
+ "red": 0.93333334,
+ "green": 0.93333334,
+ "blue": 0.93333334
+ }
+ },
+ {
+ "type": "ACCENT1",
+ "color": {
+ "red": 1,
+ "green": 0.67058825,
+ "blue": 0.2509804
+ }
+ },
+ {
+ "type": "ACCENT2",
+ "color": {
+ "red": 0.12941177,
+ "green": 0.12941177,
+ "blue": 0.12941177
+ }
+ },
+ {
+ "type": "ACCENT3",
+ "color": {
+ "red": 0.47058824,
+ "green": 0.5647059,
+ "blue": 0.6117647
+ }
+ },
+ {
+ "type": "ACCENT4",
+ "color": {
+ "red": 1,
+ "green": 0.67058825,
+ "blue": 0.2509804
+ }
+ },
+ {
+ "type": "ACCENT5",
+ "color": {
+ "green": 0.5921569,
+ "blue": 0.654902
+ }
+ },
+ {
+ "type": "ACCENT6",
+ "color": {
+ "red": 0.93333334,
+ "green": 1,
+ "blue": 0.25490198
+ }
+ },
+ {
+ "type": "HYPERLINK",
+ "color": {
+ "green": 0.5921569,
+ "blue": 0.654902
+ }
+ },
+ {
+ "type": "FOLLOWED_HYPERLINK",
+ "color": {
+ "green": 0.5921569,
+ "blue": 0.654902
+ }
+ },
+ {
+ "type": "TEXT1",
+ "color": {}
+ },
+ {
+ "type": "BACKGROUND1",
+ "color": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ {
+ "type": "TEXT2",
+ "color": {
+ "red": 0.93333334,
+ "green": 0.93333334,
+ "blue": 0.93333334
+ }
+ },
+ {
+ "type": "BACKGROUND2",
+ "color": {
+ "red": 0.34901962,
+ "green": 0.34901962,
+ "blue": 0.34901962
+ }
+ }
+ ]
+ }
+ },
+ "masterProperties": {
+ "displayName": "Simple Light"
+ }
+ }
+ ],
+ "layouts": [
+ {
+ "objectId": "p2",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p2_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.6842,
+ "translateX": 311708.35000000003,
+ "translateY": 744575,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 52,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "BOTTOM"
+ },
+ "placeholder": {
+ "type": "CENTERED_TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p2_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.2642,
+ "translateX": 311700,
+ "translateY": 2834125,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 28,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SUBTITLE",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p2_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "TITLE",
+ "displayName": "Title slide"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p3",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p3_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.2806,
+ "translateX": 311700,
+ "translateY": 2150850,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 36,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p3_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "SECTION_HEADER",
+ "displayName": "Section header"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p4",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p4_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.1909,
+ "translateX": 311700,
+ "translateY": 445025,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p4_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 1.1388,
+ "translateX": 311700,
+ "translateY": 1152475,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p4_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "TITLE_AND_BODY",
+ "displayName": "Title and body"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p5",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p5_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.1909,
+ "translateX": 311700,
+ "translateY": 445025,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p5_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.3333,
+ "scaleY": 1.1388,
+ "translateX": 311700,
+ "translateY": 1152475,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p5_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.3333,
+ "scaleY": 1.1388,
+ "translateX": 4832400,
+ "translateY": 1152475,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 14,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "index": 1,
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p5_i3",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "TITLE_AND_TWO_COLUMNS",
+ "displayName": "Title and two columns"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p6",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p6_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.1909,
+ "translateX": 311700,
+ "translateY": 445025,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p6_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "TITLE_ONLY",
+ "displayName": "Title only"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p7",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p7_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.936,
+ "scaleY": 0.2519,
+ "translateX": 311700,
+ "translateY": 555600,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 24,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "BOTTOM"
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p7_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.936,
+ "scaleY": 1.0598,
+ "translateX": 311700,
+ "translateY": 1389600,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 12,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p7_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "ONE_COLUMN_TEXT",
+ "displayName": "One column text"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p8",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p8_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.1226,
+ "scaleY": 1.3636,
+ "translateX": 490250,
+ "translateY": 450150,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 48,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p8_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "MAIN_POINT",
+ "displayName": "Main point"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p9",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p9_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.524,
+ "scaleY": 1.7145,
+ "translateX": 4572000,
+ "translateY": -125,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "RECTANGLE",
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "solidFill": {
+ "color": {
+ "themeColor": "LIGHT2"
+ },
+ "alpha": 1
+ }
+ },
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID",
+ "propertyState": "NOT_RENDERED"
+ },
+ "shadow": {
+ "type": "OUTER",
+ "transform": {
+ "scaleX": 1,
+ "scaleY": 1,
+ "unit": "EMU"
+ },
+ "alignment": "BOTTOM_LEFT",
+ "blurRadius": {
+ "unit": "EMU"
+ },
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1,
+ "rotateWithShape": false,
+ "propertyState": "NOT_RENDERED"
+ },
+ "contentAlignment": "MIDDLE"
+ }
+ }
+ },
+ {
+ "objectId": "p9_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.3484,
+ "scaleY": 0.4941,
+ "translateX": 265500,
+ "translateY": 1233175,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 42,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "BOTTOM"
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p9_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.3484,
+ "scaleY": 0.4117,
+ "translateX": 265500,
+ "translateY": 2803075,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "CENTER",
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 21,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SUBTITLE",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p9_i3",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.279,
+ "scaleY": 1.2317,
+ "translateX": 4939500,
+ "translateY": 724075,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p9_i4",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "SECTION_TITLE_AND_DESCRIPTION",
+ "displayName": "Section title and description"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p10",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p10_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.9996,
+ "scaleY": 0.2017,
+ "translateX": 311700,
+ "translateY": 4230575,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 18,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p10_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "CAPTION_ONLY",
+ "displayName": "Caption"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p11",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p11_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.6545,
+ "translateX": 311700,
+ "translateY": 1106125,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": " ",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "fontSize": {
+ "magnitude": 120,
+ "unit": "PT"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ },
+ "contentAlignment": "BOTTOM"
+ },
+ "placeholder": {
+ "type": "TITLE",
+ "parentObjectId": "p1_i0"
+ }
+ }
+ },
+ {
+ "objectId": "p11_i1",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.8402,
+ "scaleY": 0.4336,
+ "translateX": 311700,
+ "translateY": 3152225,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "alignment": "CENTER",
+ "direction": "LEFT_TO_RIGHT"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {}
+ },
+ "1": {
+ "bulletStyle": {}
+ },
+ "2": {
+ "bulletStyle": {}
+ },
+ "3": {
+ "bulletStyle": {}
+ },
+ "4": {
+ "bulletStyle": {}
+ },
+ "5": {
+ "bulletStyle": {}
+ },
+ "6": {
+ "bulletStyle": {}
+ },
+ "7": {
+ "bulletStyle": {}
+ },
+ "8": {
+ "bulletStyle": {}
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "BODY",
+ "parentObjectId": "p1_i1"
+ }
+ }
+ },
+ {
+ "objectId": "p11_i2",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "BIG_NUMBER",
+ "displayName": "Big number"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ },
+ {
+ "objectId": "p12",
+ "pageType": "LAYOUT",
+ "pageElements": [
+ {
+ "objectId": "p12_i0",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 0.1829,
+ "scaleY": 0.1312,
+ "translateX": 8472457.8125,
+ "translateY": 4663216.797499999,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "direction": "LEFT_TO_RIGHT"
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "autoText": {
+ "type": "SLIDE_NUMBER",
+ "content": "#",
+ "style": {}
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {}
+ }
+ }
+ ]
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "INHERIT"
+ },
+ "outline": {
+ "propertyState": "INHERIT"
+ },
+ "shadow": {
+ "propertyState": "INHERIT"
+ }
+ },
+ "placeholder": {
+ "type": "SLIDE_NUMBER",
+ "parentObjectId": "p1_i2"
+ }
+ }
+ }
+ ],
+ "layoutProperties": {
+ "masterObjectId": "simple-light-2",
+ "name": "BLANK",
+ "displayName": "Blank"
+ },
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "INHERIT"
+ }
+ }
+ }
+ ],
+ "locale": "en",
+ "revisionId": "kaHql7SEgvqFcw",
+ "notesMaster": {
+ "objectId": "n",
+ "pageType": "NOTES_MASTER",
+ "pageElements": [
+ {
+ "objectId": "n:slide",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 2.032025,
+ "scaleY": 1.143,
+ "translateX": 381300,
+ "translateY": 685800,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeProperties": {
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID"
+ },
+ "contentAlignment": "MIDDLE"
+ },
+ "placeholder": {
+ "type": "SLIDE_IMAGE"
+ }
+ }
+ },
+ {
+ "objectId": "n:text",
+ "size": {
+ "width": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ },
+ "height": {
+ "magnitude": 3000000,
+ "unit": "EMU"
+ }
+ },
+ "transform": {
+ "scaleX": 1.8288,
+ "scaleY": 1.3716,
+ "translateX": 685800,
+ "translateY": 4343400,
+ "unit": "EMU"
+ },
+ "shape": {
+ "shapeType": "TEXT_BOX",
+ "text": {
+ "textElements": [
+ {
+ "endIndex": 1,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "endIndex": 1,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 1,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 1,
+ "endIndex": 2,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 2,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 2,
+ "endIndex": 3,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 3,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 3,
+ "endIndex": 4,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 4,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 4,
+ "endIndex": 5,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 5,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 5,
+ "endIndex": 6,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 6,
+ "glyph": "●",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 6,
+ "endIndex": 7,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 7,
+ "glyph": "○",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 7,
+ "endIndex": 8,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "paragraphMarker": {
+ "style": {
+ "lineSpacing": 100,
+ "alignment": "START",
+ "indentStart": {
+ "unit": "PT"
+ },
+ "indentEnd": {
+ "unit": "PT"
+ },
+ "spaceAbove": {
+ "unit": "PT"
+ },
+ "spaceBelow": {
+ "unit": "PT"
+ },
+ "indentFirstLine": {
+ "unit": "PT"
+ },
+ "direction": "LEFT_TO_RIGHT",
+ "spacingMode": "COLLAPSE_LISTS"
+ },
+ "bullet": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": 8,
+ "glyph": "■",
+ "bulletStyle": {}
+ }
+ }
+ },
+ {
+ "startIndex": 8,
+ "endIndex": 9,
+ "textRun": {
+ "content": "\n",
+ "style": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ ],
+ "lists": {
+ "bodyPlaceholderListEntity": {
+ "listId": "bodyPlaceholderListEntity",
+ "nestingLevel": {
+ "0": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "1": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "2": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "3": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "4": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "5": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "6": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "7": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ },
+ "8": {
+ "bulletStyle": {
+ "backgroundColor": {},
+ "foregroundColor": {
+ "opaqueColor": {
+ "rgbColor": {}
+ }
+ },
+ "bold": false,
+ "italic": false,
+ "fontFamily": "Arial",
+ "fontSize": {
+ "magnitude": 11,
+ "unit": "PT"
+ },
+ "baselineOffset": "NONE",
+ "smallCaps": false,
+ "strikethrough": false,
+ "underline": false,
+ "weightedFontFamily": {
+ "fontFamily": "Arial",
+ "weight": 400
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "shapeProperties": {
+ "shapeBackgroundFill": {
+ "propertyState": "NOT_RENDERED",
+ "solidFill": {
+ "color": {
+ "rgbColor": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ "alpha": 1
+ }
+ },
+ "outline": {
+ "outlineFill": {
+ "solidFill": {
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1
+ }
+ },
+ "weight": {
+ "magnitude": 9525,
+ "unit": "EMU"
+ },
+ "dashStyle": "SOLID",
+ "propertyState": "NOT_RENDERED"
+ },
+ "shadow": {
+ "type": "OUTER",
+ "transform": {
+ "scaleX": 1,
+ "scaleY": 1,
+ "unit": "EMU"
+ },
+ "alignment": "BOTTOM_LEFT",
+ "blurRadius": {
+ "unit": "EMU"
+ },
+ "color": {
+ "rgbColor": {}
+ },
+ "alpha": 1,
+ "rotateWithShape": false,
+ "propertyState": "NOT_RENDERED"
+ },
+ "contentAlignment": "TOP"
+ },
+ "placeholder": {
+ "type": "BODY",
+ "index": 1
+ }
+ }
+ }
+ ],
+ "pageProperties": {
+ "pageBackgroundFill": {
+ "propertyState": "NOT_RENDERED",
+ "solidFill": {
+ "color": {
+ "rgbColor": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ "alpha": 1
+ }
+ },
+ "colorScheme": {
+ "colors": [
+ {
+ "type": "DARK1",
+ "color": {}
+ },
+ {
+ "type": "LIGHT1",
+ "color": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ {
+ "type": "DARK2",
+ "color": {
+ "red": 0.08235294,
+ "green": 0.5058824,
+ "blue": 0.34509805
+ }
+ },
+ {
+ "type": "LIGHT2",
+ "color": {
+ "red": 0.9529412,
+ "green": 0.9529412,
+ "blue": 0.9529412
+ }
+ },
+ {
+ "type": "ACCENT1",
+ "color": {
+ "red": 0.019607844,
+ "green": 0.5529412,
+ "blue": 0.78039217
+ }
+ },
+ {
+ "type": "ACCENT2",
+ "color": {
+ "red": 0.3137255,
+ "green": 0.7058824,
+ "blue": 0.19607843
+ }
+ },
+ {
+ "type": "ACCENT3",
+ "color": {
+ "red": 0.92941177,
+ "green": 0.3372549,
+ "blue": 0.105882354
+ }
+ },
+ {
+ "type": "ACCENT4",
+ "color": {
+ "red": 0.92941177,
+ "green": 0.9372549
+ }
+ },
+ {
+ "type": "ACCENT5",
+ "color": {
+ "red": 0.14117648,
+ "green": 0.79607844,
+ "blue": 0.8980392
+ }
+ },
+ {
+ "type": "ACCENT6",
+ "color": {
+ "red": 0.39215687,
+ "green": 0.8980392,
+ "blue": 0.44705883
+ }
+ },
+ {
+ "type": "HYPERLINK",
+ "color": {
+ "red": 0.13333334,
+ "blue": 0.8
+ }
+ },
+ {
+ "type": "FOLLOWED_HYPERLINK",
+ "color": {
+ "red": 0.33333334,
+ "green": 0.101960786,
+ "blue": 0.54509807
+ }
+ },
+ {
+ "type": "TEXT1",
+ "color": {}
+ },
+ {
+ "type": "BACKGROUND1",
+ "color": {
+ "red": 1,
+ "green": 1,
+ "blue": 1
+ }
+ },
+ {
+ "type": "TEXT2",
+ "color": {
+ "red": 0.9529412,
+ "green": 0.9529412,
+ "blue": 0.9529412
+ }
+ },
+ {
+ "type": "BACKGROUND2",
+ "color": {
+ "red": 0.08235294,
+ "green": 0.5058824,
+ "blue": 0.34509805
+ }
+ }
+ ]
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/views/stylesheets/authentication.css b/views/stylesheets/authentication.css
index dea0474e4..36bb880af 100644
--- a/views/stylesheets/authentication.css
+++ b/views/stylesheets/authentication.css
@@ -69,10 +69,10 @@ body {
.workspaceId {
list-style-type: none;
- font-family: Arial, Helvetica, sans-serif;
+ font-family: Arial, Helvetica, sans-serif;
margin-left: -20px;
cursor: grab;
- padding-bottom: 15px;
+ padding-bottom: 15px;
}
.workspaceId:hover {
@@ -106,7 +106,7 @@ body {
}
.overlay {
- border: 2px solid yellow;
+ border: 2px gray;
text-align: center;
position: absolute;
margin: auto;