diff options
author | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-08-28 21:06:02 -0400 |
---|---|---|
committer | Abdullah Ahmed <abdullah_ahmed@brown.edu> | 2019-08-28 21:06:02 -0400 |
commit | 38c8b841dcf3f42b96b3d93e7bcc2fc6c9495613 (patch) | |
tree | 9b0773f05ff9db5b67b47da398f2c16c2511f0bb | |
parent | e03a1b2cc90e0fdb7789f4826e482e9040aa7075 (diff) | |
parent | 4b7672c75fe5cdf6afe534e67213917b24980c3e (diff) |
merged, docview still weird
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 Binary files differnew file mode 100644 index 000000000..f71886d8c --- /dev/null +++ b/src/scraping/buxton/source/.Bill_Notes_NewO.docx.icloud diff --git a/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud Binary files differnew file mode 100644 index 000000000..30ddb3091 --- /dev/null +++ b/src/scraping/buxton/source/.Bill_Notes_OLPC.docx.icloud diff --git a/src/scraping/buxton/source/Bill_Notes_NewO.docx b/src/scraping/buxton/source/Bill_Notes_NewO.docx Binary files differdeleted file mode 100644 index a514926d2..000000000 --- a/src/scraping/buxton/source/Bill_Notes_NewO.docx +++ /dev/null diff --git a/src/scraping/buxton/source/Bill_Notes_OLPC.docx b/src/scraping/buxton/source/Bill_Notes_OLPC.docx Binary files differdeleted file mode 100644 index 7a636e2d6..000000000 --- a/src/scraping/buxton/source/Bill_Notes_OLPC.docx +++ /dev/null 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; |