diff options
42 files changed, 1229 insertions, 456 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index fc315ffaf..5df697fee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "editor.formatOnSave": true, "editor.detectIndentation": false, "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, - "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, + "search.usePCRE2": true }
\ No newline at end of file diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex fc746835f..071dafa1e 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index e8a80bdc3..64abd7fc1 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -73,14 +73,14 @@ export class Utils { }; } - public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) { + public static emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) { this.log("Emit", message.Name, args, false); socket.emit(message.Message, args); } - public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>; - public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void; - public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> { + public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>; + public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void; + public static emitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> { this.log("Emit", message.Name, args, false); if (fn) { socket.emit(message.Message, args, this.loggingCallback('Receiving', fn, message.Name)); diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 652a9b701..e8f1aa1b8 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,37 +1,139 @@ import * as OpenSocket from 'socket.io-client'; -import { MessageStore } from "./../server/Message"; +import { MessageStore, Diff } from "./../server/Message"; import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField } from '../new_fields/RefField'; import { Id, HandleUpdate } from '../new_fields/FieldSymbols'; +/** + * This class encapsulates the transfer and cross-client synchronization of + * data stored only in documents (RefFields). In the process, it also + * creates and maintains a cache of documents so that they can be accessed + * more efficiently. Currently, there is no cache eviction scheme in place. + * + * NOTE: while this class is technically abstracted to work with any [RefField], because + * [Doc] instances are the only [RefField] we need / have implemented at the moment, the documentation + * will treat all data used here as [Doc]s + * + * Any time we want to write a new field to the database (via the server) + * or update ourselves based on the server's update message, that occurs here + */ export namespace DocServer { + // a document cache for efficient document retrieval const _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {}; + // the handle / client side endpoint of the web socket (https://bit.ly/2TeALea for more info) connection established with the server const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); + // this client's distinct GUID created at initialization const GUID: string = Utils.GenerateGuid(); + // indicates whether or not a document is currently being udpated, and, if so, its id + let updatingId: string | undefined; - export function makeReadOnly() { - _CreateField = field => { - _cache[field[Id]] = field; - }; - _UpdateField = emptyFunction; - _respondToUpdate = emptyFunction; - } - + /** + * A convenience method. Prepends the full path (i.e. http://localhost:1050) to the + * requested extension + * @param extension the specified sub-path to append to the window origin + */ export function prepend(extension: string): string { return window.location.origin + extension; } - export function DeleteDatabase() { - Utils.Emit(_socket, MessageStore.DeleteAll, {}); + export namespace Control { + + let _isReadOnly = false; + export function makeReadOnly() { + if (_isReadOnly) return; + _isReadOnly = true; + _CreateField = field => { + _cache[field[Id]] = field; + }; + _UpdateField = emptyFunction; + _RespondToUpdate = emptyFunction; + } + + export function makeEditable() { + if (!_isReadOnly) return; + location.reload(); + } + + export function isReadOnly() { return _isReadOnly; } + } + export namespace Util { + + /** + * Whenever the server sends us its handshake message on our + * websocket, we use the above function to return the handshake. + */ + Utils.AddServerHandler(_socket, MessageStore.Foo, onConnection); + + /** + * This function emits a message (with this client's + * unique GUID) to the server + * indicating that this client has connected + */ + function onConnection() { + _socket.emit(MessageStore.Bar.Message, GUID); + } + + /** + * Emits a message to the server that wipes + * all documents in the database. + */ + export function deleteDatabase() { + Utils.emit(_socket, MessageStore.DeleteAll, {}); + } + + /** + * This disables this client's ability to write new fields, + * update existing fields, and update and reflect the changes if + * other clients update shared fields. Thus, the client can only read + * a static snapshot of their workspaces + * + * Currently this is conditionally called in MainView.tsx when analyzing + * the document's url. + */ + export function makeReadOnly() { + _CreateField = field => { + _cache[field[Id]] = field; + }; + _UpdateField = emptyFunction; + _RespondToUpdate = emptyFunction; + } + + } + + // RETRIEVE DOCS FROM SERVER + + /** + * Given a single Doc GUID, this utility function will asynchronously attempt to fetch the id's associated + * field, first looking in the RefField cache and then communicating with + * the server if the document has not been cached. + * @param id the id of the requested document + */ export async function GetRefField(id: string): Promise<Opt<RefField>> { + // an initial pass through the cache to determine whether the document needs to be fetched, + // is already in the process of being fetched or already exists in the + // cache let cached = _cache[id]; if (cached === undefined) { - const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => { + // NOT CACHED => we'll have to send a request to the server + + // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) + // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of + // the field has been returned from the server + const getSerializedField = Utils.emitCallback(_socket, MessageStore.GetRefField, id); + + // when the serialized RefField has been received, go head and begin deserializing it into an object. + // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all + // future .proto calls on the Doc won't have to go farther than the cache to get their actual value. + const deserializeField = getSerializedField.then(async fieldJson => { + // deserialize const field = SerializationHelper.Deserialize(fieldJson); + // either way, overwrite or delete any promises cached at this id (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. if (field !== undefined) { await field.proto; _cache[id] = field; @@ -40,45 +142,98 @@ export namespace DocServer { } return field; }); - _cache[id] = prom; - return prom; + // here, indicate that the document associated with this id is currently + // being retrieved and cached + _cache[id] = deserializeField; + return deserializeField; } else if (cached instanceof Promise) { + // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s), + // and requested the document I'm looking for. Shouldn't fetch again, just + // return this promise which will resolve to the field itself (see 7) return cached; } else { + // CACHED => great, let's just return the cached field we have return cached; } } + /** + * Given a list of Doc GUIDs, this utility function will asynchronously attempt to each id's associated + * field, first looking in the RefField cache and then communicating with + * the server if the document has not been cached. + * @param ids the ids that map to the reqested documents + */ export async function GetRefFields(ids: string[]): Promise<{ [id: string]: Opt<RefField> }> { const requestedIds: string[] = []; const waitingIds: string[] = []; const promises: Promise<Opt<RefField>>[] = []; const map: { [id: string]: Opt<RefField> } = {}; + + // 1) an initial pass through the cache to determine + // i) which documents need to be fetched + // ii) which are already in the process of being fetched + // iii) which already exist in the cache for (const id of ids) { const cached = _cache[id]; if (cached === undefined) { + // NOT CACHED => we'll have to send a request to the server requestedIds.push(id); } else if (cached instanceof Promise) { + // BEING RETRIEVED AND CACHED => some other caller previously (likely recently) called GetRefField(s), + // and requested one of the documents I'm looking for. Shouldn't fetch again, just + // wait until this promise is resolved (see 7) promises.push(cached); waitingIds.push(id); } else { + // CACHED => great, let's just add it to the field map map[id] = cached; } } - const prom = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds).then(fields => { + + // 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string) + // fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of + // the fields have been returned from the server + const getSerializedFields: Promise<any> = Utils.emitCallback(_socket, MessageStore.GetRefFields, requestedIds); + + // 3) when the serialized RefFields have been received, go head and begin deserializing them into objects. + // Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all + // future .proto calls on the Doc won't have to go farther than the cache to get their actual value. + const deserializeFields = getSerializedFields.then(async fields => { const fieldMap: { [id: string]: RefField } = {}; + const protosToLoad: any = []; for (const field of fields) { if (field !== undefined) { - fieldMap[field.id] = SerializationHelper.Deserialize(field); + // deserialize + let deserialized: any = SerializationHelper.Deserialize(field); + fieldMap[field.id] = deserialized; + // adds to a list of promises that will be awaited asynchronously + protosToLoad.push(deserialized.proto); } } - + // this actually handles the loading of prototypes + await Promise.all(protosToLoad); return fieldMap; }); - requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id])); - const fields = await prom; + + // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache) + // we set the value at the field's id to a promise that will resolve to the field. + // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method). + // The mapping in the .then call ensures that when other callers await these promises, they'll + // get the resolved field + requestedIds.forEach(id => _cache[id] = deserializeFields.then(fields => fields[id])); + + // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose + // prototype documents, if any, have also been fetched and cached. + const fields = await deserializeFields; + + // 6) with this confidence, we can now go through and update the cache at the ids of the fields that + // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given + // id to the soon-to-be-returned field mapping. requestedIds.forEach(id => { const field = fields[id]; + // either way, overwrite or delete any promises (that we inserted as flags + // to indicate that the field was in the process of being fetched). Now everything + // should be an actual value within or entirely absent from the cache. if (field !== undefined) { _cache[id] = field; } else { @@ -86,70 +241,142 @@ export namespace DocServer { } map[id] = field; }); - await Promise.all(requestedIds.map(async id => { - const field = fields[id]; - if (field) { - await (field as any).proto; - } - })); - const otherFields = await Promise.all(promises); - waitingIds.forEach((id, index) => map[id] = otherFields[index]); + + // 7) those promises we encountered in the else if of 1), which represent + // other callers having already submitted a request to the server for (a) document(s) + // in which we're interested, must still be awaited so that we can return the proper + // values for those as well. + // + // fortunately, those other callers will also hit their own version of 6) and clean up + // the shared cache when these promises resolve, so all we have to do is... + const otherCallersFetching = await Promise.all(promises); + // ...extract the RefFields returned from the resolution of those promises and add them to our + // own map. + waitingIds.forEach((id, index) => map[id] = otherCallersFetching[index]); + + // now, we return our completed mapping from all of the ids that were passed into the method + // to their actual RefField | undefined values. This return value either becomes the input + // argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...)) + // or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])). return map; } - let _UpdateField = (id: string, diff: any) => { - if (id === updatingId) { - return; - } - Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); - }; + // WRITE A NEW DOCUMENT TO THE SERVER - export function UpdateField(id: string, diff: any) { - _UpdateField(id, diff); + /** + * A wrapper around the function local variable _createField. + * This allows us to swap in different executions while comfortably + * calling the same function throughout the code base (such as in Util.makeReadonly()) + * @param field the [RefField] to be serialized and sent to the server to be stored in the database + */ + export function CreateField(field: RefField) { + _CreateField(field); } + /** + * The default behavior for field creation. This inserts the [Doc] instance + * in the cache at its id, serializes the [Doc]'s initial state + * and finally sends that seruialized data to the server. + * @param field the [RefField] to be serialized and sent to the server to be stored in the database + */ let _CreateField = (field: RefField) => { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); - Utils.Emit(_socket, MessageStore.CreateField, initialState); + Utils.emit(_socket, MessageStore.CreateField, initialState); }; - export function CreateField(field: RefField) { - _CreateField(field); + // NOTIFY THE SERVER OF AN UPDATE TO A DOC'S STATE + + /** + * A wrapper around the function local variable _emitFieldUpdate. + * This allows us to swap in different executions while comfortably + * calling the same function throughout the code base (such as in Util.makeReadonly()) + * @param id the id of the [Doc] whose state has been updated in our client + * @param updatedState the new value of the document. At some point, this + * should actually be a proper diff, to improve efficiency + */ + export function UpdateField(id: string, updatedState: any) { + _UpdateField(id, updatedState); } - let updatingId: string | undefined; - let _respondToUpdate = (diff: any) => { + /** + * The default behavior for indicating to the server that we've locally updated + * a document. + * @param id the id of the [Doc] whose state has been updated in our client + * @param updatedState the new value of the document. At some point, this + * should actually be a proper diff, to improve efficiency + */ + let _UpdateField = (id: string, updatedState: any) => { + // don't emit a duplicate message if the server is already + // (asynchronously) still updating this document's state. + if (id === updatingId) { + return; + } + // creates the diff object to send to the server + let diff: Diff = { id, diff: updatedState }; + // emit this diff to notify server + Utils.emit(_socket, MessageStore.UpdateField, diff); + }; + + // RESPOND TO THE SERVER'S INDICATION THAT A DOC'S STATE HAS BEEN UPDATED + + /** + * Whenever the client receives an update, execute the + * current behavior. + */ + Utils.AddServerHandler(_socket, MessageStore.UpdateField, RespondToUpdate); + + /** + * A wrapper around the function local variable _respondToUpdate. + * This allows us to swap in different executions while comfortably + * calling the same function throughout the code base (such as in Util.makeReadonly()) + * @param diff kept as [any], but actually the [Diff] object sent from the server containing + * the [Doc]'s id and its new state + */ + function RespondToUpdate(diff: any) { + _RespondToUpdate(diff); + } + + /** + * The default behavior for responding to another client's indication + * that it has updated the state of a [Doc] that is also in use by + * this client + * @param diff kept as [any], but actually the [Diff] object sent from the server containing + * the [Doc]'s id and its new state + */ + let _RespondToUpdate = (diff: any) => { const id = diff.id; + // to be valid, the Diff object must reference + // a document's id if (id === undefined) { return; } - const field = _cache[id]; const update = (f: Opt<RefField>) => { + // if the RefField is absent from the cache or + // its promise in the cache resolves to undefined, there + // can't be anything to update if (f === undefined) { return; } + // extract this Doc's update handler const handler = f[HandleUpdate]; if (handler) { + // set the 'I'm currently updating this Doc' flag updatingId = id; handler.call(f, diff.diff); + // reset to indicate no ongoing updates updatingId = undefined; } }; + // check the cache for the field + const field = _cache[id]; if (field instanceof Promise) { + // if the field is still being retrieved, update when the promise is resolved field.then(update); } else { + // otherwise, just execute the update update(field); } }; - function respondToUpdate(diff: any) { - _respondToUpdate(diff); - } - - function connected() { - _socket.emit(MessageStore.Bar.Message, GUID); - } - Utils.AddServerHandler(_socket, MessageStore.Foo, connected); - Utils.AddServerHandler(_socket, MessageStore.UpdateField, respondToUpdate); }
\ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index d1c9feb32..31e7eef2c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -25,7 +25,7 @@ import { OmitKeys } from "../../Utils"; import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField"; import { HtmlField } from "../../new_fields/HtmlField"; import { List } from "../../new_fields/List"; -import { Cast, NumCast, StrCast } from "../../new_fields/Types"; +import { Cast, NumCast, StrCast, ToConstructor, InterfaceValue } from "../../new_fields/Types"; import { IconField } from "../../new_fields/IconField"; import { listSpec } from "../../new_fields/Schema"; import { DocServer } from "../DocServer"; @@ -34,6 +34,7 @@ import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; import { LinkManager } from "../util/LinkManager"; import { DocumentManager } from "../util/DocumentManager"; import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox"; @@ -43,18 +44,42 @@ var path = require('path'); export enum DocTypes { NONE = "none", - IMG = "image", - HIST = "histogram", - ICON = "icon", TEXT = "text", - PDF = "pdf", + HIST = "histogram", + IMG = "image", WEB = "web", COL = "collection", KVP = "kvp", VID = "video", AUDIO = "audio", - LINK = "link", - IMPORT = "import" + PDF = "pdf", + ICON = "icon", + IMPORT = "import", + LINK = "link" +} + +export namespace DocTypeUtils { + + export function values(includeNone: boolean = true): string[] { + let types = Object.values(DocTypes); + return includeNone ? types : types.filter(key => key !== DocTypes.NONE); + } + + export function keys(includeNone: boolean = true): string[] { + let types = Object.keys(DocTypes); + return includeNone ? types : types.filter(key => key !== DocTypes.NONE); + } + + export function toObject<T extends string>(o: Array<T>): { [K in T]: K } { + return o.reduce((res, key) => { + res[key] = key; + return res; + }, Object.create(null)); + } + + const Types = toObject(values()); + + export type All = keyof typeof Types; } export interface DocumentOptions { @@ -84,362 +109,474 @@ export interface DocumentOptions { dbDoc?: Doc; // [key: string]: Opt<Field>; } -const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; -export namespace DocUtils { - export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") { - if (LinkManager.Instance.doesLinkExist(source, target)) return; - let sv = DocumentManager.Instance.getDocumentView(source); - if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; - if (target === CurrentUserUtils.UserDocument) return; +export namespace Docs { - UndoManager.RunInBatch(() => { - let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: "100%" }); - linkDoc.type = DocTypes.LINK; - let linkDocProto = Doc.GetProto(linkDoc); + export namespace Prototypes { - linkDocProto.context = targetContext; - linkDocProto.title = title === "" ? source.title + " to " + target.title : title; - linkDocProto.linkDescription = description; - linkDocProto.linkTags = tags; - linkDocProto.type = DocTypes.LINK; + type PrototypeTemplate = { options?: Partial<DocumentOptions>, primary: string, background?: string }; + type TemplateMap = Map<DocTypeUtils.All, PrototypeTemplate>; + type PrototypeMap = Map<DocTypeUtils.All, Doc>; - linkDocProto.anchor1 = source; - linkDocProto.anchor1Page = source.curPage; - linkDocProto.anchor1Groups = new List<Doc>([]); - linkDocProto.anchor2 = target; - linkDocProto.anchor2Page = target.curPage; - linkDocProto.anchor2Groups = new List<Doc>([]); + const LayoutMap: TemplateMap = new Map([ + [DocTypes.TEXT, { + options: { height: 150, backgroundColor: "#f1efeb" }, + primary: FormattedTextBox.LayoutString() + }], + [DocTypes.HIST, { + options: { nativeWidth: 600, curPage: 0 }, + primary: CollectionView.LayoutString("annotations"), + background: HistogramBox.LayoutString() + }], + [DocTypes.IMG, { + options: { height: 300, backgroundColor: "black" }, + primary: CollectionView.LayoutString("annotations"), + background: ImageBox.LayoutString() + }], + [DocTypes.WEB, { + options: { height: 300 }, + primary: WebBox.LayoutString() + }], + [DocTypes.COL, { + options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }, + primary: CollectionView.LayoutString() + }], + [DocTypes.KVP, { + options: { height: 150 }, + primary: KeyValueBox.LayoutString() + }], + [DocTypes.VID, { + options: { nativeWidth: 600, curPage: 0 }, + primary: CollectionVideoView.LayoutString("annotations"), + background: VideoBox.LayoutString() + }], + [DocTypes.AUDIO, { + options: { height: 150 }, + primary: AudioBox.LayoutString() + }], + [DocTypes.PDF, { + options: { nativeWidth: 1200, curPage: 1 }, + primary: CollectionPDFView.LayoutString("annotations"), + background: PDFBox.LayoutString() + }], + [DocTypes.ICON, { + options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) }, + primary: IconBox.LayoutString() + }], + [DocTypes.IMPORT, { + options: { height: 150 }, + primary: DirectoryImportBox.LayoutString() + }] + ]); - LinkManager.Instance.addLink(linkDoc); + const PrototypeMap: PrototypeMap = new Map(); - return linkDoc; - }, "make link"); - } -} + /** + * This function loads or initializes the prototype for each docment type. + * + * This is an asynchronous function because it has to attempt + * to fetch the prototype documents from the server. + * + * Once we have this object that maps the prototype ids to a potentially + * undefined document, we either initialize our private prototype + * variables with the document returned from the server or, if prototypes + * haven't been initialized, the newly initialized prototype document. + */ + export async function initialize(): Promise<void> { + // non-guid string ids for each document prototype + let prototypeIds: string[] = DocTypeUtils.values(false).map(type => type + "Proto"); -export namespace Docs { - let textProto: Doc; - let histoProto: Doc; - let imageProto: Doc; - let webProto: Doc; - let collProto: Doc; - let kvpProto: Doc; - let videoProto: Doc; - let audioProto: Doc; - let pdfProto: Doc; - let iconProto: Doc; - let importProto: Doc; - // let linkProto: Doc; - const textProtoId = "textProto"; - const histoProtoId = "histoProto"; - const pdfProtoId = "pdfProto"; - const imageProtoId = "imageProto"; - const webProtoId = "webProto"; - const collProtoId = "collectionProto"; - const kvpProtoId = "kvpProto"; - const videoProtoId = "videoProto"; - const audioProtoId = "audioProto"; - const iconProtoId = "iconProto"; - const importProtoId = "importProto"; - // const linkProtoId = "linkProto"; - - export function initProtos(): Promise<void> { - return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => { - textProto = fields[textProtoId] as Doc || CreateTextPrototype(); - histoProto = fields[histoProtoId] as Doc || CreateHistogramPrototype(); - collProto = fields[collProtoId] as Doc || CreateCollectionPrototype(); - imageProto = fields[imageProtoId] as Doc || CreateImagePrototype(); - webProto = fields[webProtoId] as Doc || CreateWebPrototype(); - kvpProto = fields[kvpProtoId] as Doc || CreateKVPPrototype(); - videoProto = fields[videoProtoId] as Doc || CreateVideoPrototype(); - audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype(); - pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype(); - iconProto = fields[iconProtoId] as Doc || CreateIconPrototype(); - importProto = fields[importProtoId] as Doc || CreateImportPrototype(); - }); - } + let defaultOptions: DocumentOptions = { + x: 0, + y: 0, + width: 300 + }; - function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Doc { - return Doc.assign(new Doc(protoId, true), { ...options, title: title, layout: layout }); - } - function SetInstanceOptions<U extends Field>(doc: Doc, options: DocumentOptions, value: U) { - const deleg = Doc.MakeDelegate(doc); - deleg.data = value; - return Doc.assign(deleg, options); - } - function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) { - const deleg = Doc.MakeDelegate(doc, id); - return Doc.assign(deleg, options); - } + // fetch the actual prototype documents from the server + let actualProtos = await DocServer.GetRefFields(prototypeIds); + // initialize prototype documents + prototypeIds.map(id => { + let existing = actualProtos[id] as Doc; + if (existing) { + PrototypeMap.set(id, existing); + } else { + let template = LayoutMap.get(id.replace("Proto", "")); + if (template) { + PrototypeMap.set(id, buildPrototype(template, id, defaultOptions)); + } + } + }); + } - function CreateImagePrototype(): Doc { - let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: ImageBox.LayoutString(), curPage: 0, type: DocTypes.IMG }); - return imageProto; - } + export function get(type: string) { + return PrototypeMap.get(type)!; + } - function CreateImportPrototype(): Doc { - let importProto = setupPrototypeOptions(importProtoId, "IMPORT_PROTO", DirectoryImportBox.LayoutString(), { x: 0, y: 0, width: 600, height: 600, type: DocTypes.IMPORT }); - return importProto; - } + /** + * This is a convenience method that is used to initialize + * prototype documents for the first time. + * + * @param protoId the id of the prototype, indicating the specific prototype + * to initialize (see the *protoId list at the top of the namespace) + * @param title the prototype document's title, follows *-PROTO + * @param layout the layout key for this prototype and thus the + * layout key that all delegates will inherit + * @param options any value specified in the DocumentOptions object likewise + * becomes the default value for that key for all delegates + */ + function buildPrototype(template: PrototypeTemplate, prototypeId: string, defaultOptions: DocumentOptions): Doc { + let primary = template.primary; + let background = template.background; + let options = { ...defaultOptions, ...(template.options || {}), title: prototypeId.toUpperCase().replace("PROTO", "_PROTO") }; + background && (options = { ...options, backgroundLayout: background, }); + return Doc.assign(new Doc(prototypeId, true), { ...options, layout: primary, baseLayout: primary }); + } - function CreateHistogramPrototype(): Doc { - let histoProto = setupPrototypeOptions(histoProtoId, "HISTO PROTO", CollectionView.LayoutString("annotations"), - { x: 0, y: 0, width: 300, height: 300, backgroundColor: "black", backgroundLayout: HistogramBox.LayoutString(), type: DocTypes.HIST }); - return histoProto; - } - function CreateIconPrototype(): Doc { - let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), - { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), type: DocTypes.ICON }); - return iconProto; - } - function CreateTextPrototype(): Doc { - let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), - { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb", type: DocTypes.TEXT }); - return textProto; - } - function CreatePdfPrototype(): Doc { - let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), - { x: 0, y: 0, width: 300, height: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1, type: DocTypes.PDF }); - return pdfProto; - } - function CreateWebPrototype(): Doc { - let webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 300, type: DocTypes.WEB }); - return webProto; - } - function CreateCollectionPrototype(): Doc { - let collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("data"), - { panX: 0, panY: 0, scale: 1, width: 500, height: 500, type: DocTypes.COL }); - return collProto; } - function CreateKVPPrototype(): Doc { - let kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 150, type: DocTypes.KVP }); - return kvpProto; - } - function CreateVideoPrototype(): Doc { - let videoProto = setupPrototypeOptions(videoProtoId, "VIDEO_PROTO", CollectionVideoView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 600, width: 300, backgroundLayout: VideoBox.LayoutString(), curPage: 0, type: DocTypes.VID }); - return videoProto; - } - function CreateAudioPrototype(): Doc { - let audioProto = setupPrototypeOptions(audioProtoId, "AUDIO_PROTO", AudioBox.LayoutString(), - { x: 0, y: 0, width: 300, height: 150, type: DocTypes.AUDIO }); - return audioProto; - } + /** + * Encapsulates the factory used to create new document instances + * delegated from top-level prototypes + */ + export namespace Create { + + const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; + + /** + * This function receives the relevant document prototype and uses + * it to create a new of that base-level prototype, or the + * underlying data document, which it then delegates again + * to create the view document. + * + * It also takes the opportunity to register the user + * that created the document and the time of creation. + * + * @param proto the specific document prototype off of which to model + * this new instance (textProto, imageProto, etc.) + * @param data the Field to store at this new instance's data key + * @param options any initial values to provide for this new instance + * @param delegId if applicable, an existing document id. If undefined, Doc's + * constructor just generates a new GUID. This is currently used + * only when creating a DockDocument from the current user's already existing + * main document. + */ + export function InstanceFromProto(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { + const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); - export function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { - const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); - if (!("author" in protoProps)) { - protoProps.author = CurrentUserUtils.email; + if (!("author" in protoProps)) { + protoProps.author = CurrentUserUtils.email; + } + + if (!("creationDate" in protoProps)) { + protoProps.creationDate = new DateField; + } + + protoProps.isPrototype = true; + + let dataDoc = MakeDataDelegate(proto, protoProps, data); + let viewDoc = Doc.MakeDelegate(dataDoc, delegId); + + return Doc.assign(viewDoc, delegateProps); } - if (!("creationDate" in protoProps)) { - protoProps.creationDate = new DateField; + + /** + * This function receives the relevant top level document prototype + * and models a new instance by delegating from it. + * + * Note that it stores the data it recieves at the delegate's data key, + * and applies any document options to this new delegate / instance. + * @param proto the prototype from which to model this new delegate + * @param options initial values to apply to this new delegate + * @param value the data to store in this new delegate + */ + function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value: D) { + const deleg = Doc.MakeDelegate(proto); + deleg.data = value; + return Doc.assign(deleg, options); } - protoProps.isPrototype = true; - return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId); - } + export function ImageDocument(url: string, options: DocumentOptions = {}) { + let inst = InstanceFromProto(Prototypes.get(DocTypes.IMG), new ImageField(new URL(url)), { title: path.basename(url), ...options }); + requestImageSize(window.origin + RouteStore.corsProxy + "/" + url) + .then((size: any) => { + let aspect = size.height / size.width; + if (!inst.proto!.nativeWidth) { + inst.proto!.nativeWidth = size.width; + } + inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect; + inst.proto!.height = NumCast(inst.proto!.width) * aspect; + }) + .catch((err: any) => console.log(err)); + return inst; + } - export function ImageDocument(url: string, options: DocumentOptions = {}) { - let inst = CreateInstance(imageProto, new ImageField(new URL(url)), { title: path.basename(url), ...options }); - requestImageSize(window.origin + RouteStore.corsProxy + "/" + url) - .then((size: any) => { - let aspect = size.height / size.width; - if (!inst.proto!.nativeWidth) { - inst.proto!.nativeWidth = size.width; - } - inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect; - inst.height = NumCast(inst.width) * aspect; - }) - .catch((err: any) => console.log(err)); - return inst; - // let doc = SetInstanceOptions(GetImagePrototype(), { ...options, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }, - // [new URL(url), ImageField]); - // doc.SetText(KeyStore.Caption, "my caption..."); - // doc.SetText(KeyStore.BackgroundLayout, EmbeddedCaption()); - // doc.SetText(KeyStore.OverlayLayout, FixedCaption()); - // return doc; - } - export function VideoDocument(url: string, options: DocumentOptions = {}) { - return CreateInstance(videoProto, new VideoField(new URL(url)), options); - } - export function AudioDocument(url: string, options: DocumentOptions = {}) { - return CreateInstance(audioProto, new AudioField(new URL(url)), options); - } + export function VideoDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.VID), new VideoField(new URL(url)), options); + } - export function DirectoryImportDocument(options: DocumentOptions = {}) { - return CreateInstance(importProto, new List<Doc>(), options); - } + export function AudioDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.AUDIO), new AudioField(new URL(url)), options); + } - export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) { - return CreateInstance(histoProto, new HistogramField(histoOp), options); - } - export function TextDocument(options: DocumentOptions = {}) { - return CreateInstance(textProto, "", options); - } - export function IconDocument(icon: string, options: DocumentOptions = {}) { - return CreateInstance(iconProto, new IconField(icon), options); - } + export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.HIST), new HistogramField(histoOp), options); + } - export function PdfDocument(url: string, options: DocumentOptions = {}) { - return CreateInstance(pdfProto, new PdfField(new URL(url)), options); - } + export function TextDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.TEXT), "", options); + } + + export function IconDocument(icon: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.ICON), new IconField(icon), options); + } - export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) { - let schemaName = options.title ? options.title : "-no schema-"; - let ctlog = await Gateway.Instance.GetSchema(url, schemaName); - if (ctlog && ctlog.schemas) { - let schema = ctlog.schemas[0]; - let schemaDoc = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); - let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []); - if (!schemaDocuments) { - return; + export function PdfDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.PDF), new PdfField(new URL(url)), options); + } + + export async function DBDocument(url: string, options: DocumentOptions = {}, columnOptions: DocumentOptions = {}) { + let schemaName = options.title ? options.title : "-no schema-"; + let ctlog = await Gateway.Instance.GetSchema(url, schemaName); + if (ctlog && ctlog.schemas) { + let schema = ctlog.schemas[0]; + let schemaDoc = Docs.Create.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); + let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []); + if (!schemaDocuments) { + return; + } + CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc); + const docs = schemaDocuments; + CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { + DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { + if (field instanceof Doc) { + docs.push(field); + } else { + var atmod = new ColumnAttributeModel(attr); + let histoOp = new HistogramOperation(schema.displayName!, + new AttributeTransformationModel(atmod, AggregateFunction.None), + new AttributeTransformationModel(atmod, AggregateFunction.Count), + new AttributeTransformationModel(atmod, AggregateFunction.Count)); + docs.push(Docs.Create.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! })); + } + })); + }); + return schemaDoc; } - CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc); - const docs = schemaDocuments; - CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { - DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { - if (field instanceof Doc) { - docs.push(field); - } else { - var atmod = new ColumnAttributeModel(attr); - let histoOp = new HistogramOperation(schema.displayName!, - new AttributeTransformationModel(atmod, AggregateFunction.None), - new AttributeTransformationModel(atmod, AggregateFunction.Count), - new AttributeTransformationModel(atmod, AggregateFunction.Count)); - docs.push(Docs.HistogramDocument(histoOp, { ...columnOptions, width: 200, height: 200, title: attr.displayName! })); - } - })); - }); - return schemaDoc; + return Docs.Create.TreeDocument([], { width: 50, height: 100, title: schemaName }); } - return Docs.TreeDocument([], { width: 50, height: 100, title: schemaName }); - } - export function WebDocument(url: string, options: DocumentOptions = {}) { - return CreateInstance(webProto, new WebField(new URL(url)), options); - } - export function HtmlDocument(html: string, options: DocumentOptions = {}) { - return CreateInstance(webProto, new HtmlField(html), options); - } - export function KVPDocument(document: Doc, options: DocumentOptions = {}) { - return CreateInstance(kvpProto, document, { title: document.title + ".kvp", ...options }); - } - export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) { - if (!makePrototype) { - return SetInstanceOptions(collProto, { ...options, viewType: CollectionViewType.Freeform }, new List(documents)); + + export function WebDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.WEB), new WebField(new URL(url)), options); } - return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform }); - } - export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); - } - export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree }); - } - export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking }); - } - export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { - return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id); - } - export async function getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { - let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Docs.ImageDocument; + export function HtmlDocument(html: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.WEB), new HtmlField(html), options); } - if (type.indexOf("video") !== -1) { - ctor = Docs.VideoDocument; + + export function KVPDocument(document: Doc, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.KVP), document, { title: document.title + ".kvp", ...options }); } - if (type.indexOf("audio") !== -1) { - ctor = Docs.AudioDocument; + + export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, makePrototype: boolean = true) { + if (!makePrototype) { + return MakeDataDelegate(Prototypes.get(DocTypes.COL), { ...options, viewType: CollectionViewType.Freeform }, new List(documents)); + } + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform }); } - if (type.indexOf("pdf") !== -1) { - ctor = Docs.PdfDocument; - options.nativeWidth = 1200; + + export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); } - if (type.indexOf("excel") !== -1) { - ctor = Docs.DBDocument; - options.dropAction = "copy"; + + export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree }); + } + + export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Stacking }); } - if (type.indexOf("html") !== -1) { - if (path.includes(window.location.hostname)) { - let s = path.split('/'); - let id = s[s.length - 1]; - return DocServer.GetRefField(id).then(field => { - if (field instanceof Doc) { - let alias = Doc.MakeAlias(field); - alias.x = options.x || 0; - alias.y = options.y || 0; - alias.width = options.width || 300; - alias.height = options.height || options.width || 300; - return alias; + + export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id); + } + + export function DirectoryImportDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocTypes.COL), new List<Doc>(), options); + } + + export type DocConfig = { + doc: Doc, + initialWidth?: number + }; + + export function StandardCollectionDockingDocument(configs: Array<DocConfig>, options: DocumentOptions, id?: string, type: string = "row") { + let layoutConfig = { + content: [ + { + type: type, + content: [ + ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth)) + ] } - return undefined; - }); - } - ctor = Docs.WebDocument; - options = { height: options.width, ...options, title: path, nativeWidth: undefined }; + ] + }; + return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id); } - return ctor ? ctor(path, options) : undefined; } - export function CaptionDocument(doc: Doc) { - const captionDoc = Doc.MakeAlias(doc); - captionDoc.overlayLayout = FixedCaption(); - captionDoc.width = Cast(doc.width, "number", 0); - captionDoc.height = Cast(doc.height, "number", 0); - return captionDoc; - } + export namespace Get { - // example of custom display string for an image that shows a caption. - function EmbeddedCaption() { - return `<div style="height:100%"> - <div style="position:relative; margin:auto; height:85%; width:85%;" >` - + ImageBox.LayoutString() + - `</div> - <div style="position:relative; height:15%; text-align:center; ">` - + FormattedTextBox.LayoutString("caption") + - `</div> - </div>`; - } - export function FixedCaption(fieldName: string = "caption") { - return `<div style="position:absolute; height:30px; bottom:0; width:100%"> - <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">` - + FormattedTextBox.LayoutString(fieldName) + - `</div> - </div>`; - } + export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> { + let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Docs.Create.ImageDocument; + } + if (type.indexOf("video") !== -1) { + ctor = Docs.Create.VideoDocument; + } + if (type.indexOf("audio") !== -1) { + ctor = Docs.Create.AudioDocument; + } + if (type.indexOf("pdf") !== -1) { + ctor = Docs.Create.PdfDocument; + options.nativeWidth = 1200; + } + if (type.indexOf("excel") !== -1) { + ctor = Docs.Create.DBDocument; + options.dropAction = "copy"; + } + if (type.indexOf("html") !== -1) { + if (path.includes(window.location.hostname)) { + let s = path.split('/'); + let id = s[s.length - 1]; + return DocServer.GetRefField(id).then(field => { + if (field instanceof Doc) { + let alias = Doc.MakeAlias(field); + alias.x = options.x || 0; + alias.y = options.y || 0; + alias.width = options.width || 300; + alias.height = options.height || options.width || 300; + return alias; + } + return undefined; + }); + } + ctor = Docs.Create.WebDocument; + options = { height: options.width, ...options, title: path, nativeWidth: undefined }; + } + return ctor ? ctor(path, options) : undefined; + } - function OuterCaption() { - return (` -<div> - <div style="margin:auto; height:calc(100%); width:100%;"> - {layout} - </div> - <div style="height:(100% + 25px); width:100%; position:absolute"> - <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/> - </div> -</div> - `); } - function InnerCaption() { - return (` - <div> - <div style="margin:auto; height:calc(100% - 25px); width:100%;"> - {layout} - </div> - <div style="height:25px; width:100%; position:absolute"> - <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/> - </div> - </div> + + export namespace Templating { + + export function CaptionDocument(doc: Doc) { + const captionDoc = Doc.MakeAlias(doc); + captionDoc.overlayLayout = FixedCaption(); + captionDoc.width = Cast(doc.width, "number", 0); + captionDoc.height = Cast(doc.height, "number", 0); + return captionDoc; + } + + /** + * An example of custom display string for an image that shows a caption. + */ + export function EmbeddedCaption() { + return ( + `<div style="height:100%"> + <div style="position:relative; margin:auto; height:85%; width:85%;" >${ImageBox.LayoutString()}</div> + <div style="position:relative; height:15%; text-align:center; ">${FormattedTextBox.LayoutString("caption")}</div> + </div>` + ); + } + + export function FixedCaption(fieldName: string = "caption") { + return ( + `<div style="position:absolute; height:30px; bottom:0; width:100%"> + <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">${FormattedTextBox.LayoutString(fieldName)}</div> + </div>` + ); + } + + export function OuterCaption() { + return (` + <div> + <div style="margin:auto; height:calc(100%); width:100%;"> + {layout} + </div> + <div style="height:(100% + 25px); width:100%; position:absolute"> + <FormattedTextBox + doc={Document} + DocumentViewForField={DocumentView} + bindings={bindings} + fieldKey={"caption"} + isSelected={isSelected} + select={select} + selectOnLoad={SelectOnLoad} + renderDepth={renderDepth + /> + </div> + </div> + `); + } + + export function InnerCaption() { + return (` + <div> + <div style="margin:auto; height:calc(100% - 25px); width:100%;"> + {layout} + </div> + <div style="height:25px; width:100%; position:absolute"> + <FormattedTextBox + doc={Document} + DocumentViewForField={DocumentView} + bindings={bindings} + fieldKey={"caption"} + isSelected={isSelected} + select={select} + selectOnLoad={SelectOnLoad} + renderDepth={renderDepth + /> + </div> + </div> `); + } + } +} + +export namespace DocUtils { + + export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") { + if (LinkManager.Instance.doesLinkExist(source, target)) return; + let sv = DocumentManager.Instance.getDocumentView(source); + if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return; + if (target === CurrentUserUtils.UserDocument) return; + + UndoManager.RunInBatch(() => { + let linkDoc = Docs.Create.TextDocument({ width: 100, height: 30, borderRounding: "100%" }); + linkDoc.type = DocTypes.LINK; + let linkDocProto = Doc.GetProto(linkDoc); + + linkDocProto.context = targetContext; + linkDocProto.title = title === "" ? source.title + " to " + target.title : title; + linkDocProto.linkDescription = description; + linkDocProto.linkTags = tags; + linkDocProto.type = DocTypes.LINK; + + linkDocProto.anchor1 = source; + linkDocProto.anchor1Page = source.curPage; + linkDocProto.anchor1Groups = new List<Doc>([]); + linkDocProto.anchor2 = target; + linkDocProto.anchor2Page = target.curPage; + linkDocProto.anchor2Groups = new List<Doc>([]); + + LinkManager.Instance.addLink(linkDoc); + + return linkDoc; + }, "make link"); } } -Scripting.addGlobal("Docs", Docs);
\ No newline at end of file +Scripting.addGlobal("Docs", Docs); diff --git a/src/client/util/ClientUtils.ts b/src/client/util/ClientUtils.ts new file mode 100644 index 000000000..425bde14a --- /dev/null +++ b/src/client/util/ClientUtils.ts @@ -0,0 +1,4 @@ +//AUTO-GENERATED FILE: DO NOT EDIT +export namespace ClientUtils { + export const RELEASE = false; +}
\ No newline at end of file diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index ce95ba90e..a810db0fa 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -103,7 +103,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> body: formData }).then(async (res: Response) => { (await res.json()).map(action((file: any) => { - let docPromise = Docs.getDocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); + let docPromise = Docs.Get.DocumentFromType(type, DocServer.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName }); docPromise.then(doc => { doc && docs.push(doc) && runInAction(() => this.remaining--); }); @@ -136,7 +136,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps> }; let parent = this.props.ContainingCollectionView; if (parent) { - let importContainer = Docs.StackingDocument(docs, options); + let importContainer = Docs.Create.StackingDocument(docs, options); importContainer.singleColumn = false; Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer); !this.persistent && this.props.removeDocument && this.props.removeDocument(doc); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 944bc532f..d6ea6013b 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -234,9 +234,9 @@ export class LinkManager { //TODO This should also await the return value of the anchor so we don't filter out promises public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc { if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) { - return Cast(linkDoc.anchor2, Doc, null)!; + return Cast(linkDoc.anchor2, Doc, null); } else { - return Cast(linkDoc.anchor1, Doc, null)!; + return Cast(linkDoc.anchor1, Doc, null); } } }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index cb7ed976a..4fcc16ddd 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -448,7 +448,7 @@ export class TooltipTextMenu { let node = state.doc.nodeAt(from); node && node.marks.map(m => { m.type === markType && (curLink = m.attrs.href); - }) + }); //toggleMark(markType)(state, dispatch); //return true; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index d6cf793ab..2e26a2286 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -305,7 +305,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @undoBatch @action createIcon = (selected: DocumentView[], layoutString: string): Doc => { let doc = selected[0].props.Document; - let iconDoc = Docs.IconDocument(layoutString); + let iconDoc = Docs.Create.IconDocument(layoutString); iconDoc.isButton = true; iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon"; iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 281d9159b..932a6375f 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -5,7 +5,7 @@ import * as ReactDOM from 'react-dom'; import * as React from 'react'; (async () => { - await Docs.initProtos(); + await Docs.Prototypes.initialize(); await CurrentUserUtils.loadCurrentUser(); ReactDOM.render(<MainView />, document.getElementById('root')); })();
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index b37ba1cb0..1f628228a 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -1,5 +1,5 @@ import { IconName, library } from '@fortawesome/fontawesome-svg-core'; -import { faArrowDown, faArrowUp, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; +import { faArrowDown, faArrowUp, faClone, faCheck, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faThumbtack, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, configure, observable, runInAction, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; @@ -57,7 +57,7 @@ export class MainView extends React.Component { private set mainContainer(doc: Opt<Doc>) { if (doc) { if (!("presentationView" in doc)) { - doc.presentationView = new List<Doc>([Docs.TreeDocument([], { title: "Presentation" })]); + doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]); } CurrentUserUtils.UserDocument.activeWorkspace = doc; } @@ -94,11 +94,11 @@ export class MainView extends React.Component { // causes errors to be generated when modifying an observable outside of an action configure({ enforceActions: "observed" }); if (window.location.search.includes("readonly")) { - DocServer.makeReadOnly(); + DocServer.Util.makeReadOnly(); } if (window.location.search.includes("safe")) { if (!window.location.search.includes("nro")) { - DocServer.makeReadOnly(); + DocServer.Util.makeReadOnly(); } CollectionBaseView.SetSafeMode(true); } @@ -125,6 +125,7 @@ export class MainView extends React.Component { library.add(faFilm); library.add(faMusic); library.add(faTree); + library.add(faClone); library.add(faCut); library.add(faCommentAlt); library.add(faThumbtack); @@ -172,9 +173,9 @@ export class MainView extends React.Component { if (!(workspaces instanceof Doc)) return; const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc)); if (list) { - let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` }); + let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` }); var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] }; - let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); + let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); if (!CurrentUserUtils.UserDocument.linkManagerDoc) { let linkManagerDoc = new Doc(); linkManagerDoc.allLinks = new List<Doc>([]); @@ -355,15 +356,21 @@ export class MainView extends React.Component { let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let addColNode = action(() => Docs.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" })); + 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(["title"], [], { 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 addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); - let addImportCollectionNode = action(() => Docs.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })); + let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); + let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })); let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode], [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], + [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], + [React.createRef<HTMLDivElement>(), "clone", "Add Docking Frame", addDockingNode], [React.createRef<HTMLDivElement>(), "arrow-up", "Import Directory", addImportCollectionNode], ]; @@ -437,7 +444,7 @@ export class MainView extends React.Component { <PDFMenu /> <MainOverlayTextBox /> <OverlayView /> - </div> + </div > ); } } diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx new file mode 100644 index 000000000..6995e3c7d --- /dev/null +++ b/src/client/views/SearchBox.tsx @@ -0,0 +1,207 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import { Utils } from '../../Utils'; +import { MessageStore } from '../../server/Message'; +import "./SearchBox.scss"; +import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { library } from '@fortawesome/fontawesome-svg-core'; +// const app = express(); +// import * as express from 'express'; +import { Search } from '../../server/Search'; +import * as rp from 'request-promise'; +import { SearchItem } from './search/SearchItem'; +import { isString } from 'util'; +import { constant } from 'async'; +import { DocServer } from '../DocServer'; +import { Doc } from '../../new_fields/Doc'; +import { Id } from '../../new_fields/FieldSymbols'; +import { DocumentManager } from '../util/DocumentManager'; +import { SetupDrag } from '../util/DragManager'; +import { Docs } from '../documents/Documents'; +import { RouteStore } from '../../server/RouteStore'; +import { NumCast } from '../../new_fields/Types'; + +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 + getResults = async (query: string) => { + let response = await rp.get(DocServer.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; + } + public static async convertDataUri(imageUri: string, returnedFilename: string) { + try { + let posting = DocServer.prepend(RouteStore.dataUriToImage); + const returnedUri = await rp.post(posting, { + body: { + uri: imageUri, + name: returnedFilename + }, + json: true, + }); + return returnedUri; + + } catch (e) { + console.log(e); + } + } + + @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]} />)} + </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 e4f9b5058..e9a693f55 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -18,7 +18,7 @@ export enum CollectionViewType { Schema, Docking, Tree, - Stacking + Stacking, } export interface CollectionRenderProps { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index d477f96f0..f44ab50c7 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -426,6 +426,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp stackCreated = (stack: any) => { //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined; stack.header.controlsContainer.find('.lm_close') //get the close icon .off('click') //unbind the current click handler .click(action(function () { @@ -576,4 +577,4 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { {({ measureRef }) => <div ref={measureRef}> {theContent} </div>} </Measure>; } -} +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index b54e8aff0..cf39e26a8 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -263,7 +263,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { let dbName = StrCast(this.props.Document.title); let res = await Gateway.Instance.PostSchema(csv, dbName); if (self.props.CollectionView.props.addDocument) { - let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); + let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document }); if (schemaDoc) { //self.props.CollectionView.props.addDocument(schemaDoc, false); self.props.Document.schemaDoc = schemaDoc; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index aea74321e..416c536f6 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -98,7 +98,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { 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); } - docXfs: any[] = [] + docXfs: any[] = []; @computed get children() { this.docXfs.length = 0; @@ -184,7 +184,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; } - }) + }); } if (super.drop(e, de)) { let newDoc = de.data.droppedDocuments[0]; @@ -210,7 +210,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) { if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) { targInd = i; } - }) + }); super.onDrop(e, {}, () => { if (targInd !== -1) { let newDoc = this.childDocs[this.childDocs.length - 1]; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 873fb518c..a7614b605 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -144,10 +144,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - this.props.addDocument && this.props.addDocument(Docs.WebDocument(href, options)); + this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options)); } } else if (text) { - this.props.addDocument && this.props.addDocument(Docs.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); + this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false); } return; } @@ -157,7 +157,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : ""; if (img) { let split = img.split("src=\"")[1].split("\"")[0]; - let doc = Docs.ImageDocument(split, { ...options, width: 300 }); + let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 }); this.props.addDocument(doc, false); return; } else { @@ -171,7 +171,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } }); } else { - let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); + let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); this.props.addDocument(htmlDoc, false); } return; @@ -179,7 +179,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { } if (text && text.indexOf("www.youtube.com/watch") !== -1) { const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); - this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 })); + this.props.addDocument(Docs.Create.WebDocument(url, { ...options, width: 300, height: 300 })); return; } @@ -196,7 +196,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { .then(result => { let type = result["content-type"]; if (type) { - Docs.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) + Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) .then(doc => doc && this.props.addDocument(doc, false)); } }); @@ -219,7 +219,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { (await res.json()).map(action((file: any) => { let full = { ...options, nativeWidth: 300, width: 300, title: dropFileName }; let path = DocServer.prepend(file); - Docs.getDocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); + Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc)); })); }); promises.push(prom); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d7725f444..ef340d770 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -170,7 +170,7 @@ class TreeView extends React.Component<TreeViewProps> { SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true} OnFillDown={(value: string) => { Doc.GetProto(this.resolvedDataDoc)[key] = value; - let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + let 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); }} @@ -246,7 +246,7 @@ class TreeView extends React.Component<TreeViewProps> { ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)), icon: "caret-square-right" }); ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)), icon: "trash-alt" }); } - ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); + ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" }); ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15); e.stopPropagation(); e.preventDefault(); @@ -542,7 +542,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true} OnFillDown={(value: string) => { Doc.GetProto(this.props.Document).title = value; - let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) }); + let 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); }} /> diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 1984965ba..446f104d0 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -98,7 +98,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => { if (returnedFilename) { let url = DocServer.prepend(returnedFilename); - let imageSummary = Docs.ImageDocument(url, { + let imageSummary = Docs.Create.ImageDocument(url, { x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y), width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-" }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index cb55a0d5d..1777c287c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -465,11 +465,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { description: "Add freeform arrangement", event: () => { let addOverlay = (key: "arrangeScript" | "arrangeInit", options: OverlayElementOptions, params?: Record<string, string>, requiredType?: string) => { - let overlayDisposer: () => void; + let overlayDisposer: () => void = emptyFunction; const script = this.Document[key]; let originalText: string | undefined = undefined; if (script) originalText = script.script.originalScript; - let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => { + let scriptingBox = <ScriptBox initialText={originalText} onCancel={overlayDisposer} onSave={(text, onError) => { const script = CompileScript(text, { params, requiredType, diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 8a619bfae..a4a6881f8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -76,7 +76,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } ns.map(line => { let indent = line.search(/\S|$/); - let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); + let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); this.props.addDocument(newBox, false); y += 40 * this.props.getTransform().Scale; }); @@ -86,13 +86,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> navigator.clipboard.readText().then(text => { let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); if (ns.length === 1 && text.startsWith("http")) { - this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer + this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer } else { this.pasteTable(ns, x, y); } }); } else if (!e.ctrlKey) { - let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); newBox.proto!.autoHeight = true; this.props.addLiveTextDocument(newBox); } @@ -134,7 +134,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> doc.width = 200; docList.push(doc); } - let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + let newCol = Docs.Create.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); this.props.addDocument(newCol, false); } @@ -271,7 +271,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> }); } let inkData = this.ink ? this.ink.inkData : undefined; - let newCollection = Docs.FreeformDocument(selected, { + let newCollection = Docs.Create.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panX: 0, @@ -292,14 +292,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; summary.proto!.subBulletDocs = new List<Doc>(selected); //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" summary.templates = new List<string>([Templates.Bullet.Layout]); - let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); + let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" }); container.viewType = CollectionViewType.Stacking; this.props.addLiveTextDocument(container); // }); @@ -311,7 +311,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> d.page = -1; return d; }); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); + let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" }); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; newCollection.x = bounds.left + bounds.width; diff --git a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx new file mode 100644 index 000000000..f8104cef3 --- /dev/null +++ b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { FontWeightProperty, FontStyleProperty, FontSizeProperty, ColorProperty } from 'csstype'; +import { observer } from 'mobx-react'; +import { observable, action, runInAction } from 'mobx'; +import { FormattedTextBox, FormattedTextBoxProps } from '../../nodes/FormattedTextBox'; +import { FieldViewProps } from '../../nodes/FieldView'; + +interface DetailedCaptionDataProps { + captionFieldKey?: string; + detailsFieldKey?: string; +} + +interface DetailedCaptionStylingProps { + sharedFontColor?: ColorProperty; + captionFontStyle?: FontStyleProperty; + detailsFontStyle?: FontStyleProperty; + toggleSize?: number; +} + +@observer +export default class DetailedCaptionToggle extends React.Component<DetailedCaptionDataProps & DetailedCaptionStylingProps & FieldViewProps> { + @observable loaded: boolean = false; + @observable detailsExpanded: boolean = false; + + @action toggleDetails = (e: React.MouseEvent<HTMLDivElement>) => { + e.preventDefault(); + e.stopPropagation(); + this.detailsExpanded = !this.detailsExpanded; + } + + componentDidMount() { + runInAction(() => this.loaded = true); + } + + render() { + let size = this.props.toggleSize || 20; + return ( + <div style={{ + transition: "0.5s opacity ease", + opacity: this.loaded ? 1 : 0, + bottom: 0, + fontSize: 14, + width: "100%", + position: "absolute" + }}> + {/* caption */} + <div style={{ opacity: this.detailsExpanded ? 0 : 1, transition: "opacity 0.3s ease" }}> + <FormattedTextBox {...this.props} fieldKey={this.props.captionFieldKey || "caption"} /> + </div> + {/* details */} + <div style={{ opacity: this.detailsExpanded ? 1 : 0, transition: "opacity 0.3s ease" }}> + <FormattedTextBox {...this.props} fieldKey={this.props.detailsFieldKey || "captiondetails"} /> + </div> + {/* toggle */} + <div + style={{ + width: size, + height: size, + borderRadius: "50%", + backgroundColor: "red", + zIndex: 3, + cursor: "pointer" + }} + onClick={this.toggleDetails} + > + <span style={{ color: "white" }}></span> + </div> + </div> + ); + } + +} diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx new file mode 100644 index 000000000..9931515f3 --- /dev/null +++ b/src/client/views/document_templates/image_card/ImageCard.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { DocComponent } from '../../DocComponent'; +import { FieldViewProps } from '../../nodes/FieldView'; +import { createSchema, makeInterface } from '../../../../new_fields/Schema'; +import { createInterface } from 'readline'; +import { ImageBox } from '../../nodes/ImageBox'; + +export default class ImageCard extends React.Component<FieldViewProps> { + + render() { + return ( + <div style={{ padding: 30, borderRadius: 15 }}> + <ImageBox {...this.props} /> + </div> + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 430409ee3..5b5653309 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -394,7 +394,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); } @undoBatch - fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); } + fieldsClicked = (): void => { let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.dataDoc, "onRight"); } @undoBatch makeBtnClicked = (): void => { @@ -520,19 +520,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" }); cm.addItem({ description: "Make Portal", event: () => { - let portal = Docs.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" }); + 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.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" }); + 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); }, icon: "window-restore" - }) + }); cm.addItem({ description: "Find aliases", event: async () => { const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); - this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? + this.props.addDocTab && this.props.addDocTab(Docs.Create.SchemaDocument(["title"], aliases, {}), undefined, "onRight"); // bcz: dataDoc? }, icon: "search" }); cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" }); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index c5fc6c65a..ac9c42d05 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -18,9 +18,6 @@ import { ImageBox } from "./ImageBox"; import { PDFBox } from "./PDFBox"; import { VideoBox } from "./VideoBox"; import { Id } from "../../../new_fields/FieldSymbols"; -import { BoolCast, Cast } from "../../../new_fields/Types"; -import { DarpaDatasetDoc } from "../../northstar/model/idea/idea"; - // // these properties get assigned through the render() method of the DocumentView when it creates this node. diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index fd895507c..96ee6f200 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -302,7 +302,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe e.preventDefault(); } } else { - let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); + let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); this.props.addDocument && this.props.addDocument(webDoc); this._linkClicked = webDoc[Id]; } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 2f5a0f963..c40840227 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -159,7 +159,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } getTemplate = async () => { - let parent = Docs.StackingDocument([], { width: 800, height: 800, title: "Template" }); + let parent = Docs.Create.StackingDocument([], { width: 800, height: 800, title: "Template" }); parent.singleColumn = false; parent.columnWidth = 100; for (let row of this.rows.filter(row => row.isChecked)) { @@ -206,23 +206,23 @@ export class KeyValueBox extends React.Component<FieldViewProps> { inferType = async (data: FieldResult, metaKey: string) => { let options = { width: 300, height: 300, title: metaKey }; if (data instanceof RichTextField || typeof data === "string" || typeof data === "number") { - return Docs.TextDocument(options); + return Docs.Create.TextDocument(options); } else if (data instanceof List) { if (data.length === 0) { - return Docs.StackingDocument([], options); + return Docs.Create.StackingDocument([], options); } let first = await Cast(data[0], Doc); if (!first) { - return Docs.StackingDocument([], options); + return Docs.Create.StackingDocument([], options); } switch (first.type) { case "image": - return Docs.StackingDocument([], options); + return Docs.Create.StackingDocument([], options); case "text": - return Docs.TreeDocument([], options); + return Docs.Create.TreeDocument([], options); } } else if (data instanceof ImageField) { - return Docs.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); + return Docs.Create.ImageDocument("https://image.flaticon.com/icons/png/512/23/23765.png", options); } return new Doc; } diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx index a97ec8831..ec8cb33ab 100644 --- a/src/client/views/nodes/LinkEditor.tsx +++ b/src/client/views/nodes/LinkEditor.tsx @@ -242,7 +242,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> { if (index > -1) keys.splice(index, 1); let cols = ["anchor1", "anchor2", ...[...keys]]; let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; } diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx index e4cf56d20..be45c3e6e 100644 --- a/src/client/views/nodes/LinkMenuGroup.tsx +++ b/src/client/views/nodes/LinkMenuGroup.tsx @@ -64,7 +64,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { if (index > -1) keys.splice(index, 1); let cols = ["anchor1", "anchor2", ...[...keys]]; let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType); - let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); + let createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" })); let ref = React.createRef<HTMLDivElement>(); return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>; } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 8af29110f..e49611a5e 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -231,7 +231,7 @@ export class Viewer extends React.Component<IViewerProps> { makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { let annoDocs: Doc[] = []; - let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {}); + let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); mainAnnoDoc.pdfDoc = this.props.parent.Document; @@ -588,7 +588,7 @@ export class Viewer extends React.Component<IViewerProps> { } return true; }); - this.Index = Math.min(this.Index + 1, filtered.length - 1) + this.Index = Math.min(this.Index + 1, filtered.length - 1); } nextResult = () => { diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx index 49eac71c4..021841541 100644 --- a/src/client/views/pdf/Page.tsx +++ b/src/client/views/pdf/Page.tsx @@ -157,7 +157,7 @@ export default class Page extends React.Component<IPageProps> { e.stopPropagation(); let thisDoc = this.props.parent.Document; // document that this annotation is linked to - let targetDoc = Docs.TextDocument({ width: 200, height: 200, title: "New Annotation" }); + let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" }); targetDoc.targetPage = this.props.page; let annotationDoc = this.highlight(targetDoc, "red"); // create dragData and star tdrag diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx index a3fa553b7..edbbeb8f9 100644 --- a/src/client/views/presentationview/PresentationView.tsx +++ b/src/client/views/presentationview/PresentationView.tsx @@ -591,7 +591,7 @@ export class PresentationView extends React.Component<PresViewProps> { @action addNewPresentation = (presTitle: string) => { //creating a new presentation doc - let newPresentationDoc = Docs.TreeDocument([], { title: presTitle }); + let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle }); this.props.Documents.push(newPresentationDoc); //setting that new doc as current diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 1f6835c26..7dcfbe1ef 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -141,7 +141,7 @@ export class SearchBox extends React.Component { y += 300; } } - return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); + return Docs.Create.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, backgroundColor: "grey", title: `Search Docs: "${this._searchString}"` }); } @action.bound diff --git a/src/documentation/collection_hierarchies.txt b/src/documentation/collection_hierarchies.txt new file mode 100644 index 000000000..69e60c136 --- /dev/null +++ b/src/documentation/collection_hierarchies.txt @@ -0,0 +1,50 @@ +** When we drag and drop out a node from MainView.tsx's button menu, what actually happens? ** + +As you have probably already seen, MainView.tsx renders a line of circular buttons, each of wich can be dragged and dropped to +create new nodes in the collection acting as the drop target. + +These buttons are logically stored as an array of tuples, currently called 'btns'. Each tuple contains the React reference to +the actual HTMLDivElement around which the button is made, later used to set up dragging behavior, but most importantly contains the sort of factory function that creates a +new document (of the relevant type). This document underlies the view that will be added to the collection (something like addImageNode()). + +The SetupDrag() function in DragManager.ts creates new DragManager.DocumentDragData and, in it, embeds the newly created document (which may have, for example, an ImageField +at its data key, which will soon be used to render an ImageBox...). The DragManager then begins the dragging operation, which handles the display of the element as it's +dragged out onto the canvas and registers the desired drop operation, namely copying the document or creating an alias. + +When the document is dropped onto the target collection, the CollectionSubView superclass's drop() method is invoked. Typically, if dropping a single document from one +of the MainView.tsx node addition buttons, this iterates through the DragData's droppedDocuments and adds them to the collection via an addDocument() function this CollectionSubView +received with its props. In actuality, this addDocument() function is defined in and passed down from CollectionBaseView, and conditionally adds the document to the +underlying collection document's data (list of child documents). To actually be added, the document to add cannot create a cycle (for example, you cannot add a collection to one of +its own children that is itself a collection). + +Here is the sequence of function calls: + +MainView."round-button add-button" onPointerDown() => DragManager.SetupDrag() +DragManager.SetupDrag.onRowMove() => DragManager.StartDocumentDrag() +DragManager.StartDrag() + +... (USER IS DRAGGING DOCUMENT AROUND VIA BUTTON) +... (USER DROPS THE DOCUMENT IN THE TARGET COLLECTION) + +CollectionSubView.drop() + +<DocumentView> + <DocumentContentsView> { + Nodes themselves, both base types and collections, are actually always rendered by using a JSXParser to parse a stringified JSX element layout (see + FieldView.LayoutString()). Typically, way back in the initial drag phase, where the buttons maintained document creation + functions like Documents.ImageDocument(), the layout string will have always been set, because of the way that new node + documents are created. The ImageDocument() function creates a delegate from the imageProto (image document prototype) which is itself created at the time + Dash is loaded. Since the delegate inherits the prototype's layout string, the layoutKey field will be set and effectively always, the JSXParser will + parse the existing layout string to return the appropriate JSX element to be rendered as a child of the collection sub view. On the off chance that this + layout field has not been set, the layout() getter just returns a generic FieldView element to the JSXParser, and internally, this component decides based + on the nature of the document it receives, which node view to assign. This is basically a fallback. + } + <CollectionView> + <CollectionBaseView> + // all of the below extend <CollectionSubView> + <CollectionFreeFormView> + <CollectionSchemaView> + <CollectionDockingView> + <CollectionTreeView> + <CollectionStackingView> + <FieldView>
\ No newline at end of file diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index bfc1738fc..a8f94b746 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -33,7 +33,7 @@ class Uploader extends React.Component { onClick = async () => { try { this.status = "initializing protos"; - await Docs.initProtos(); + await Docs.Prototypes.initialize(); let imgPrev = document.getElementById("img_preview"); if (imgPrev) { let files: FileList | null = inputRef.current!.files; @@ -53,7 +53,7 @@ class Uploader extends React.Component { const json = await res.json(); json.map(async (file: any) => { let path = window.location.origin + file; - var doc = Docs.ImageDocument(path, { nativeWidth: 200, width: 200, title: name }); + var doc = Docs.Create.ImageDocument(path, { nativeWidth: 200, width: 200, title: name }); this.status = "getting user document"; diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index a07f56ca4..0fe3f76e3 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -202,6 +202,18 @@ export namespace Doc { } return protos; } + + /** + * This function is intended to model Object.assign({}, {}) [https://mzl.la/1Mo3l21], which copies + * the values of the properties of a source object into the target. + * + * This is just a specific, Dash-authored version that serves the same role for our + * Doc class. + * + * @param doc the target document into which you'd like to insert the new fields + * @param fields the fields to project onto the target. Its type signature defines a mapping from some string key + * to a potentially undefined field, where each entry in this mapping is optional. + */ export function assign<K extends string>(doc: Doc, fields: Partial<Record<K, Opt<Field>>>) { for (const key in fields) { if (fields.hasOwnProperty(key)) { diff --git a/src/scraping/acm/.gitignore b/src/scraping/acm/.gitignore new file mode 100644 index 000000000..caca8b99c --- /dev/null +++ b/src/scraping/acm/.gitignore @@ -0,0 +1,2 @@ +./citations.txt +./results.txt
\ No newline at end of file diff --git a/src/scraping/acm/debug.log b/src/scraping/acm/debug.log new file mode 100644 index 000000000..8c0a148f4 --- /dev/null +++ b/src/scraping/acm/debug.log @@ -0,0 +1,38 @@ +[0625/170004.768:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/170004.769:ERROR:exception_snapshot_win.cc(98)] thread ID 17604 not found in process +[0625/171124.644:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/171124.645:ERROR:exception_snapshot_win.cc(98)] thread ID 14348 not found in process +[0625/171853.989:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/171853.990:ERROR:exception_snapshot_win.cc(98)] thread ID 12080 not found in process +[0625/171947.744:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/171947.745:ERROR:exception_snapshot_win.cc(98)] thread ID 16160 not found in process +[0625/172007.424:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172007.425:ERROR:exception_snapshot_win.cc(98)] thread ID 13472 not found in process +[0625/172059.353:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172059.354:ERROR:exception_snapshot_win.cc(98)] thread ID 6396 not found in process +[0625/172402.795:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172402.796:ERROR:exception_snapshot_win.cc(98)] thread ID 10720 not found in process +[0625/172618.850:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172618.850:ERROR:exception_snapshot_win.cc(98)] thread ID 21136 not found in process +[0625/172819.875:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172819.876:ERROR:exception_snapshot_win.cc(98)] thread ID 17624 not found in process +[0625/172953.674:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/172953.675:ERROR:exception_snapshot_win.cc(98)] thread ID 15180 not found in process +[0625/173412.182:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173412.182:ERROR:exception_snapshot_win.cc(98)] thread ID 13952 not found in process +[0625/173447.806:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173447.807:ERROR:exception_snapshot_win.cc(98)] thread ID 1572 not found in process +[0625/173516.188:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173516.189:ERROR:exception_snapshot_win.cc(98)] thread ID 5472 not found in process +[0625/173528.446:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173528.447:ERROR:exception_snapshot_win.cc(98)] thread ID 20420 not found in process +[0625/173539.436:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173539.437:ERROR:exception_snapshot_win.cc(98)] thread ID 16192 not found in process +[0625/173643.139:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173643.140:ERROR:exception_snapshot_win.cc(98)] thread ID 15716 not found in process +[0625/173659.376:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/173659.377:ERROR:exception_snapshot_win.cc(98)] thread ID 11828 not found in process +[0625/201137.209:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/201137.210:ERROR:exception_snapshot_win.cc(98)] thread ID 7688 not found in process +[0625/210240.476:ERROR:process_reader_win.cc(123)] NtOpenThread: {Access Denied} A process has requested access to an object, but has not been granted those access rights. (0xc0000022) +[0625/210240.477:ERROR:exception_snapshot_win.cc(98)] thread ID 20828 not found in process diff --git a/src/scraping/acm/index.js b/src/scraping/acm/index.js index 51781dba8..b71d55226 100644 --- a/src/scraping/acm/index.js +++ b/src/scraping/acm/index.js @@ -276,4 +276,4 @@ log_read("target references"); readFile(target_source, { encoding: "utf8" -}, scrape_targets);
\ No newline at end of file +}, scrape_targets); diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index e328d6e5c..763693dd6 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -38,26 +38,26 @@ export class CurrentUserUtils { doc.xMargin = 5; doc.yMargin = 5; doc.excludeFromLibrary = true; - doc.optionalRightCollection = Docs.StackingDocument([], { title: "New mobile uploads" }); - // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); + 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; } static updateUserDocument(doc: Doc) { if (doc.workspaces === undefined) { - const workspaces = Docs.TreeDocument([], { title: "Workspaces", height: 100 }); + const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 }); workspaces.excludeFromLibrary = true; workspaces.workspaceLibrary = true; doc.workspaces = workspaces; } if (doc.recentlyClosed === undefined) { - const recentlyClosed = Docs.TreeDocument([], { title: "Recently Closed", height: 75 }); + const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 }); recentlyClosed.excludeFromLibrary = true; doc.recentlyClosed = recentlyClosed; } if (doc.sidebar === undefined) { - const sidebar = Docs.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); + const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" }); sidebar.excludeFromLibrary = true; sidebar.gridGap = 5; sidebar.xMargin = 5; @@ -128,12 +128,12 @@ export class CurrentUserUtils { // new AttributeTransformationModel(atmod, AggregateFunction.None), // new AttributeTransformationModel(atmod, AggregateFunction.Count), // new AttributeTransformationModel(atmod, AggregateFunction.Count)); - // schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! })); + // schemaDocuments.push(Docs.Create.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! })); // } // }))); // return promises; // }, [] as Promise<void>[])); - // return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })); + // return CurrentUserUtils._northstarSchemas.push(Docs.Create.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })); // }); // } } diff --git a/src/server/index.ts b/src/server/index.ts index 2073046ce..287ba6058 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -453,7 +453,7 @@ let clients: Map = {}; server.on("connection", function (socket: Socket) { console.log("a user has connected"); - Utils.Emit(socket, MessageStore.Foo, "handshooken"); + Utils.emit(socket, MessageStore.Foo, "handshooken"); Utils.AddServerHandler(socket, MessageStore.Bar, barReceived); Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); |