diff options
Diffstat (limited to 'src/client')
51 files changed, 4373 insertions, 1322 deletions
diff --git a/src/client/Server.ts b/src/client/Server.ts index 0cb6e17c2..f0cf0bb9b 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -1,40 +1,77 @@ -import { Field, FieldWaiting, FieldId, FIELD_WAITING, FieldValue, Opt } from "../fields/Field" -import { Key, KeyStore } from "../fields/Key" -import { ObservableMap, action } from "mobx"; +import { Key } from "../fields/Key" +import { ObservableMap, action, reaction } from "mobx"; +import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field" import { Document } from "../fields/Document" import { SocketStub } from "./SocketStub"; +import * as OpenSocket from 'socket.io-client'; +import { Utils } from "./../Utils"; +import { MessageStore, Types } from "./../server/Message"; export class Server { - private static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap(); + public static ClientFieldsCached: ObservableMap<FieldId, Field | FIELD_WAITING> = new ObservableMap(); + static Socket: SocketIOClient.Socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:1234`); + static GUID: string = Utils.GenerateGuid() + // Retrieves the cached value of the field and sends a request to the server for the real value (if it's not cached). // Call this is from within a reaction and test whether the return value is FieldWaiting. // 'hackTimeout' is here temporarily for simplicity when debugging things. - public static GetField(fieldid: FieldId, callback: (field: Field) => void = (f) => { }, hackTimeout: number = -1) { - if (!this.ClientFieldsCached.get(fieldid)) { + public static GetField(fieldid: FieldId, callback: (field: Opt<Field>) => void): Opt<Field> | FIELD_WAITING { + let cached = this.ClientFieldsCached.get(fieldid); + if (!cached) { this.ClientFieldsCached.set(fieldid, FieldWaiting); - //simulating a server call with a registered callback action - SocketStub.SEND_FIELD_REQUEST(fieldid, - action((field: Field) => callback(Server.cacheField(field))), - hackTimeout); - } else if (this.ClientFieldsCached.get(fieldid) != FieldWaiting) { - callback(this.ClientFieldsCached.get(fieldid) as Field); + SocketStub.SEND_FIELD_REQUEST(fieldid, action((field: Field | undefined) => { + let cached = this.ClientFieldsCached.get(fieldid); + if (cached != FieldWaiting) + callback(cached); + else { + if (field) { + this.ClientFieldsCached.set(fieldid, field); + } else { + this.ClientFieldsCached.delete(fieldid) + } + callback(field) + } + })); + } else if (cached != FieldWaiting) { + setTimeout(() => callback(cached as Field), 0); + } else { + reaction(() => { + return this.ClientFieldsCached.get(fieldid); + }, (field, reaction) => { + if (field !== "<Waiting>") { + callback(field) + reaction.dispose() + } + }) } - return this.ClientFieldsCached.get(fieldid); + return cached; } - static times = 0; // hack for testing - public static GetDocumentField(doc: Document, key: Key): FieldValue<Field> { - var hackTimeout: number = key == KeyStore.Data ? (this.times++ == 0 ? 5000 : 1000) : key == KeyStore.X ? 2500 : 500; + public static GetFields(fieldIds: FieldId[], callback: (fields: { [id: string]: Field }) => any) { + SocketStub.SEND_FIELDS_REQUEST(fieldIds, (fields) => { + for (let key in fields) { + let field = fields[key]; + if (!this.ClientFieldsCached.has(field.Id)) { + this.ClientFieldsCached.set(field.Id, field) + } + } + callback(fields) + }); + } - let fieldId = doc._proxies.get(key); - if (fieldId) { - return this.GetField(fieldId, - action((fieldfromserver: Field) => { - doc._proxies.delete(key); - doc.fields.set(key, fieldfromserver); - }) - , hackTimeout); + public static GetDocumentField(doc: Document, key: Key, callback?: (field: Field) => void) { + let field = doc._proxies.get(key.Id); + if (field) { + this.GetField(field, + action((fieldfromserver: Opt<Field>) => { + if (fieldfromserver) { + doc.fields.set(key.Id, { key, field: fieldfromserver }); + if (callback) { + callback(fieldfromserver); + } + } + })); } } @@ -42,13 +79,22 @@ export class Server { SocketStub.SEND_ADD_DOCUMENT(document); } public static AddDocumentField(doc: Document, key: Key, value: Field) { + console.log("Add doc field " + doc.Title + " " + key.Name + " fid " + value.Id + " " + value); SocketStub.SEND_ADD_DOCUMENT_FIELD(doc, key, value); } public static DeleteDocumentField(doc: Document, key: Key) { SocketStub.SEND_DELETE_DOCUMENT_FIELD(doc, key); } - public static SetFieldValue(field: Field, value: any) { - SocketStub.SEND_SET_FIELD(field, value); + + public static UpdateField(field: Field) { + if (!this.ClientFieldsCached.has(field.Id)) { + this.ClientFieldsCached.set(field.Id, field) + } + SocketStub.SEND_SET_FIELD(field); + } + + static connected(message: string) { + Server.Socket.emit(MessageStore.Bar.Message, Server.GUID); } @action @@ -61,4 +107,18 @@ export class Server { } return this.ClientFieldsCached.get(clientField.Id) as Field; } + + @action + static updateField(field: { _id: string, data: any, type: Types }) { + if (Server.ClientFieldsCached.has(field._id)) { + var f = Server.ClientFieldsCached.get(field._id); + if (f && f != FieldWaiting) { + f.UpdateFromServer(field.data); + f.init(() => { }); + } + } + } } + +Server.Socket.on(MessageStore.Foo.Message, Server.connected); +Server.Socket.on(MessageStore.SetField.Message, Server.updateField);
\ No newline at end of file diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts index cea30cb8b..18df4ca0a 100644 --- a/src/client/SocketStub.ts +++ b/src/client/SocketStub.ts @@ -1,7 +1,11 @@ -import { Field, FieldId } from "../fields/Field" -import { Key, KeyStore } from "../fields/Key" -import { ObservableMap, action } from "mobx"; +import { Key } from "../fields/Key" +import { Field, FieldId, Opt } from "../fields/Field" +import { ObservableMap } from "mobx"; import { Document } from "../fields/Document" +import { MessageStore, DocumentTransfer } from "../server/Message"; +import { Utils } from "../Utils"; +import { Server } from "./Server"; +import { ServerUtils } from "../server/ServerUtil"; export class SocketStub { @@ -12,33 +16,46 @@ export class SocketStub { // ...SOCKET(ADD_DOCUMENT, serialied document) // server stores each document field in its repository of stored fields - document.fields.forEach((f, key) => this.FieldStore.set((f as Field).Id, f as Field)); - - // server stores stripped down document (w/ only field id proxies) in the field store - this.FieldStore.set(document.Id, new Document(document.Id)); - document.fields.forEach((f, key) => (this.FieldStore.get(document.Id) as Document)._proxies.set(key, (f as Field).Id)); + // document.fields.forEach((f, key) => this.FieldStore.set((f as Field).Id, f as Field)); + // let strippedDoc = new Document(document.Id); + // document.fields.forEach((f, key) => { + // if (f) { + // // let args: SetFieldArgs = new SetFieldArgs(f.Id, f.GetValue()) + // let args: Transferable = f.ToJson() + // Utils.Emit(Server.Socket, MessageStore.SetField, args) + // } + // }) + + // // server stores stripped down document (w/ only field id proxies) in the field store + // this.FieldStore.set(document.Id, new Document(document.Id)); + // document.fields.forEach((f, key) => (this.FieldStore.get(document.Id) as Document)._proxies.set(key.Id, (f as Field).Id)); + + console.log("sending " + document.Title); + Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(document.ToJson())) } - public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Field) => void, timeout: number) { - - if (timeout < 0)// this is a hack to make things easier to setup until we have a server... won't be neededa fter that. - callback(this.FieldStore.get(fieldid) as Field); - else { // actual logic here... - - // Send a request for fieldid to the server - // ...SOCKET(RETRIEVE_FIELD, fieldid) - - // server responds (simulated with a timeout) and the callback is invoked - setTimeout(() => - - // when the field data comes back, call the callback() function - callback(this.FieldStore.get(fieldid) as Field), - - - timeout); + public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void) { + if (fieldid) { + Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => { + if (field) { + ServerUtils.FromJson(field).init(callback); + } else { + callback(undefined); + } + }) } } + public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: { [key: string]: Field }) => any) { + Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => { + let fieldMap: any = {}; + for (let field of fields) { + fieldMap[field._id] = ServerUtils.FromJson(field); + } + callback(fieldMap) + }); + } + public static SEND_ADD_DOCUMENT_FIELD(doc: Document, key: Key, value: Field) { // Send a serialized version of the field to the server along with the @@ -49,10 +66,11 @@ export class SocketStub { // server updates its document to hold a proxy mapping from key => fieldId var document = this.FieldStore.get(doc.Id) as Document; if (document) - document._proxies.set(key, value.Id); + document._proxies.set(key.Id, value.Id); // server adds the field to its repository of fields this.FieldStore.set(value.Id, value); + // Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(doc.ToJson())) } public static SEND_DELETE_DOCUMENT_FIELD(doc: Document, key: Key) { @@ -63,16 +81,15 @@ export class SocketStub { // Server removes the field id from the document's list of field proxies var document = this.FieldStore.get(doc.Id) as Document; if (document) - document._proxies.delete(key); + document._proxies.delete(key.Id); } - public static SEND_SET_FIELD(field: Field, value: any) { + public static SEND_SET_FIELD(field: Field) { // Send a request to set the value of a field // ...SOCKET(SET_FIELD, field id, serialized field value) // Server updates the value of the field in its fieldstore - if (this.FieldStore.get(field.Id)) - this.FieldStore.get(field.Id)!.TrySetValue(value); + Utils.Emit(Server.Socket, MessageStore.SetField, field.ToJson()) } } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 72fa608ad..fc210e9a2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,18 +1,24 @@ import { Document } from "../../fields/Document"; import { Server } from "../Server"; -import { KeyStore } from "../../fields/Key"; +import { KeyStore } from "../../fields/KeyStore"; import { TextField } from "../../fields/TextField"; import { NumberField } from "../../fields/NumberField"; import { ListField } from "../../fields/ListField"; import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; -import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { CollectionSchemaView } from "../views/collections/CollectionSchemaView"; import { ImageField } from "../../fields/ImageField"; import { ImageBox } from "../views/nodes/ImageBox"; -import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView"; -import { FieldId } from "../../fields/Field"; +import { WebField } from "../../fields/WebField"; +import { WebBox } from "../views/nodes/WebBox"; +import { CollectionView, CollectionViewType } from "../views/collections/CollectionView"; +import { HtmlField } from "../../fields/HtmlField"; +import { Key } from "../../fields/Key" +import { Field } from "../../fields/Field"; +import { KeyValueBox } from "../views/nodes/KeyValueBox" +import { PDFField } from "../../fields/PDFField"; +import { PDFBox } from "../views/nodes/PDFBox"; +import { CollectionPDFView } from "../views/collections/CollectionPDFView"; -interface DocumentOptions { +export interface DocumentOptions { x?: number; y?: number; width?: number; @@ -20,157 +26,151 @@ interface DocumentOptions { nativeWidth?: number; nativeHeight?: number; title?: string; + panx?: number; + pany?: number; + scale?: number; + layout?: string; + layoutKeys?: Key[]; + viewType?: number; } export namespace Documents { - function setupOptions(doc: Document, options: DocumentOptions): void { - if (options.x) { - doc.SetData(KeyStore.X, options.x, NumberField); - } - if (options.y) { - doc.SetData(KeyStore.Y, options.y, NumberField); - } - if (options.width) { - doc.SetData(KeyStore.Width, options.width, NumberField); - } - if (options.height) { - doc.SetData(KeyStore.Height, options.height, NumberField); - } - if (options.nativeWidth) { - doc.SetData(KeyStore.NativeWidth, options.nativeWidth, NumberField); - } - if (options.nativeHeight) { - doc.SetData(KeyStore.NativeHeight, options.nativeHeight, NumberField); - } - if (options.title) { - doc.SetData(KeyStore.Title, options.title, TextField); - } - doc.SetData(KeyStore.Scale, 1, NumberField); - doc.SetData(KeyStore.PanX, 0, NumberField); - doc.SetData(KeyStore.PanY, 0, NumberField); - } - let textProto: Document; - function GetTextPrototype(): Document { - if (!textProto) { - textProto = new Document(); - textProto.Set(KeyStore.X, new NumberField(0)); - textProto.Set(KeyStore.Y, new NumberField(0)); - textProto.Set(KeyStore.Width, new NumberField(300)); - textProto.Set(KeyStore.Height, new NumberField(150)); - textProto.Set(KeyStore.Layout, new TextField(FormattedTextBox.LayoutString())); - textProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return textProto; - } + let imageProto: Document; + let webProto: Document; + let collProto: Document; + let kvpProto: Document; + let pdfProto: Document; + const textProtoId = "textProto"; + const pdfProtoId = "pdfProto"; + const imageProtoId = "imageProto"; + const webProtoId = "webProto"; + const collProtoId = "collectionProto"; + const kvpProtoId = "kvpProto"; - export function TextDocument(options: DocumentOptions = {}): Document { - let doc = GetTextPrototype().MakeDelegate(); - setupOptions(doc, options); - // doc.SetField(KeyStore.Data, new RichTextField()); + export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { + Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { + collProto = fields[collProtoId] as Document; + imageProto = fields[imageProtoId] as Document; + textProto = fields[textProtoId] as Document; + webProto = fields[webProtoId] as Document; + kvpProto = fields[kvpProtoId] as Document; + callback(fields[mainDocId] as Document) + }); + } + function assignOptions(doc: Document, options: DocumentOptions): Document { + if (options.x !== undefined) { doc.SetNumber(KeyStore.X, options.x); } + if (options.y !== undefined) { doc.SetNumber(KeyStore.Y, options.y); } + if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); } + if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); } + if (options.nativeWidth !== undefined) { doc.SetNumber(KeyStore.NativeWidth, options.nativeWidth); } + if (options.nativeHeight !== undefined) { doc.SetNumber(KeyStore.NativeHeight, options.nativeHeight); } + if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); } + if (options.panx !== undefined) { doc.SetNumber(KeyStore.PanX, options.panx); } + if (options.pany !== undefined) { doc.SetNumber(KeyStore.PanY, options.pany); } + if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } + if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); } + if (options.layout !== undefined) { doc.SetText(KeyStore.Layout, options.layout); } + if (options.layoutKeys !== undefined) { doc.Set(KeyStore.LayoutKeys, new ListField(options.layoutKeys)); } return doc; } - - let schemaProto: Document; - function GetSchemaPrototype(): Document { - if (!schemaProto) { - schemaProto = new Document(); - schemaProto.Set(KeyStore.X, new NumberField(0)); - schemaProto.Set(KeyStore.Y, new NumberField(0)); - schemaProto.Set(KeyStore.Width, new NumberField(300)); - schemaProto.Set(KeyStore.Height, new NumberField(150)); - schemaProto.Set(KeyStore.Layout, new TextField(CollectionSchemaView.LayoutString())); - schemaProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return schemaProto; + function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { + return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); } - - export function SchemaDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetSchemaPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); - return doc; + function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: T, ctor: { new(): U }, id?: string) { + var deleg = doc.MakeDelegate(id); + deleg.SetData(KeyStore.Data, value, ctor); + return assignOptions(deleg, options); } - - let dockProto: Document; - function GetDockPrototype(): Document { - if (!dockProto) { - dockProto = new Document(); - dockProto.Set(KeyStore.X, new NumberField(0)); - dockProto.Set(KeyStore.Y, new NumberField(0)); - dockProto.Set(KeyStore.Width, new NumberField(300)); - dockProto.Set(KeyStore.Height, new NumberField(150)); - dockProto.Set(KeyStore.Layout, new TextField(CollectionDockingView.LayoutString())); - dockProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); + function GetImagePrototype(): Document { + if (!imageProto) { + imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"), + { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] }); + imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString()); } - return dockProto; + return imageProto; } - - export function DockDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetDockPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); - return doc; + function GetTextPrototype(): Document { + return textProto ? textProto : + textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); } - - - let imageProtoId: FieldId; - function GetImagePrototype(): Document { - if (imageProtoId === undefined) { - let imageProto = new Document(); - imageProtoId = imageProto.Id; - imageProto.Set(KeyStore.Title, new TextField("IMAGE PROTO")); - imageProto.Set(KeyStore.X, new NumberField(0)); - imageProto.Set(KeyStore.Y, new NumberField(0)); - imageProto.Set(KeyStore.NativeWidth, new NumberField(300)); - imageProto.Set(KeyStore.NativeHeight, new NumberField(300)); - imageProto.Set(KeyStore.Width, new NumberField(300)); - imageProto.Set(KeyStore.Height, new NumberField(300)); - imageProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString("AnnotationsKey"))); - imageProto.Set(KeyStore.BackgroundLayout, new TextField(ImageBox.LayoutString())); - // imageProto.SetField(KeyStore.Layout, new TextField('<div style={"background-image: " + {Data}} />')); - imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations])); - Server.AddDocument(imageProto); - return imageProto; + function GetPdfPrototype(): Document { + if (!pdfProto) { + pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("AnnotationsKey"), + { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations] }); + pdfProto.SetNumber(KeyStore.CurPage, 1); + pdfProto.SetText(KeyStore.BackgroundLayout, PDFBox.LayoutString()); } - return Server.GetField(imageProtoId) as Document; + return pdfProto; } - - export function ImageDocument(url: string, options: DocumentOptions = {}): Document { - let doc = GetImagePrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ImageField(new URL(url))); - - let annotation = Documents.TextDocument({ title: "hello" }); - Server.AddDocument(annotation); - doc.Set(KeyStore.Annotations, new ListField([annotation])); - Server.AddDocument(doc); - var sdoc = Server.GetField(doc.Id) as Document; - return sdoc; + function GetWebPrototype(): Document { + return webProto ? webProto : + webProto = setupPrototypeOptions(webProtoId, "WEB_PROTO", WebBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 300, layoutKeys: [KeyStore.Data] }); } - - let collectionProto: Document; function GetCollectionPrototype(): Document { - if (!collectionProto) { - collectionProto = new Document(); - collectionProto.Set(KeyStore.X, new NumberField(0)); - collectionProto.Set(KeyStore.Y, new NumberField(0)); - collectionProto.Set(KeyStore.Scale, new NumberField(1)); - collectionProto.Set(KeyStore.PanX, new NumberField(0)); - collectionProto.Set(KeyStore.PanY, new NumberField(0)); - collectionProto.Set(KeyStore.Width, new NumberField(300)); - collectionProto.Set(KeyStore.Height, new NumberField(300)); - collectionProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString("DataKey"))); - collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data])); - } - return collectionProto; + return collProto ? collProto : + collProto = setupPrototypeOptions(collProtoId, "COLLECTION_PROTO", CollectionView.LayoutString("DataKey"), + { panx: 0, pany: 0, scale: 1, layoutKeys: [KeyStore.Data] }); + } + + function GetKVPPrototype(): Document { + return kvpProto ? kvpProto : + kvpProto = setupPrototypeOptions(kvpProtoId, "KVP_PROTO", KeyValueBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }) } - export function CollectionDocument(documents: Array<Document>, options: DocumentOptions = {}): Document { - let doc = GetCollectionPrototype().MakeDelegate(); - setupOptions(doc, options); - doc.Set(KeyStore.Data, new ListField(documents)); + export function ImageDocument(url: string, options: DocumentOptions = {}) { + 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 TextDocument(options: DocumentOptions = {}) { + return SetInstanceOptions(GetTextPrototype(), options, "", TextField); + } + export function PdfDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetPdfPrototype(), options, new URL(url), PDFField); + } + export function WebDocument(url: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, new URL(url), WebField); + } + export function HtmlDocument(html: string, options: DocumentOptions = {}) { + return SetInstanceOptions(GetWebPrototype(), options, html, HtmlField); + } + export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Freeform }, documents, ListField, id) + } + export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Schema }, documents, ListField, id) + } + export function DockDocument(config: string, options: DocumentOptions, id?: string) { + return SetInstanceOptions(GetCollectionPrototype(), { ...options, viewType: CollectionViewType.Docking }, config, TextField, id) + } + export function KVPDocument(document: Document, options: DocumentOptions = {}, id?: string) { + var deleg = GetKVPPrototype().MakeDelegate(id); + deleg.Set(KeyStore.Data, document); + return assignOptions(deleg, options); + } + + // 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%;" >` + + ImageBox.LayoutString() + + `</div> + <div style="position:relative; height:15%; text-align:center; ">` + + FormattedTextBox.LayoutString("CaptionKey") + + `</div> + </div>` }; + function FixedCaption() { + 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("CaptionKey") + + `</div> + </div>` }; }
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f4dcce7c8..513a6ac9e 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,3 +1,39 @@ +import { DocumentDecorations } from "../views/DocumentDecorations"; +import { CollectionDockingView } from "../views/collections/CollectionDockingView"; +import { Document } from "../../fields/Document" +import { action } from "mobx"; +import { DocumentView } from "../views/nodes/DocumentView"; +import { ImageField } from "../../fields/ImageField"; +import { KeyStore } from "../../fields/KeyStore"; + +export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document) { + let onRowMove = action((e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + DragManager.StartDrag(_reference.current!, { document: docFunc() }); + }); + let onRowUp = action((e: PointerEvent): void => { + document.removeEventListener("pointermove", onRowMove); + document.removeEventListener('pointerup', onRowUp); + }); + let onItemDown = (e: React.PointerEvent) => { + // if (this.props.isSelected() || this.props.isTopMost) { + if (e.button == 0) { + e.stopPropagation(); + if (e.shiftKey) { + CollectionDockingView.Instance.StartOtherDrag(docFunc(), e); + } else { + document.addEventListener("pointermove", onRowMove); + document.addEventListener('pointerup', onRowUp); + } + } + //} + } + return onItemDown; +} export namespace DragManager { export function Root() { @@ -43,6 +79,7 @@ export namespace DragManager { drop: (e: Event, de: DropEvent) => void; } + export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer { if ("canDrop" in element.dataset) { throw new Error("Element is already droppable, can't make it droppable again"); @@ -59,10 +96,8 @@ export namespace DragManager { }; } - - let _lastPointerX: number = 0; - let _lastPointerY: number = 0; - export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options: DragOptions) { + export function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions) { + DocumentDecorations.Instance.Hidden = true; if (!dragDiv) { dragDiv = document.createElement("div"); DragManager.Root().appendChild(dragDiv); @@ -72,49 +107,83 @@ export namespace DragManager { const scaleX = rect.width / w, scaleY = rect.height / h; let x = rect.left, y = rect.top; // const offsetX = e.x - rect.left, offsetY = e.y - rect.top; + let dragElement = ele.cloneNode(true) as HTMLElement; dragElement.style.opacity = "0.7"; dragElement.style.position = "absolute"; + dragElement.style.bottom = ""; + dragElement.style.left = ""; dragElement.style.transformOrigin = "0 0"; dragElement.style.zIndex = "1000"; dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; + dragElement.style.width = `${rect.width / scaleX}px`; + dragElement.style.height = `${rect.height / scaleY}px`; + + // bcz: PDFs don't show up if you clone them because they contain a canvas. + // however, PDF's have a thumbnail field that contains an image of their canvas. + // So we replace the pdf's canvas with the image thumbnail + const docView: DocumentView = dragData["documentView"]; + const doc: Document = docView ? docView.props.Document : dragData["document"]; + var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement; + let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField); + if (pdfBox && pdfBox.childElementCount && thumbnail) { + let img = new Image(); + img!.src = thumbnail.toString(); + img!.style.position = "absolute"; + img!.style.width = `${rect.width / scaleX}px`; + img!.style.height = `${rect.height / scaleY}px`; + pdfBox.replaceChild(img!, pdfBox.children[0]) + } + dragDiv.appendChild(dragElement); - _lastPointerX = dragData["xOffset"] + rect.left; - _lastPointerY = dragData["yOffset"] + rect.top; let hideSource = false; - if (typeof options.hideSource === "boolean") { - hideSource = options.hideSource; - } else { - hideSource = options.hideSource(); + if (options) { + if (typeof options.hideSource === "boolean") { + hideSource = options.hideSource; + } else { + hideSource = options.hideSource(); + } } const wasHidden = ele.hidden; if (hideSource) { ele.hidden = true; } - const moveHandler = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); - x += e.clientX - _lastPointerX; _lastPointerX = e.clientX; - y += e.clientY - _lastPointerY; _lastPointerY = e.clientY; + x += e.movementX; + y += e.movementY; + if (e.shiftKey) { + abortDrag(); + CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }); + } dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`; }; - const upHandler = (e: PointerEvent) => { + + const abortDrag = () => { document.removeEventListener("pointermove", moveHandler, true); document.removeEventListener("pointerup", upHandler); - FinishDrag(dragElement, e, options, dragData); + dragDiv.removeChild(dragElement); if (hideSource && !wasHidden) { ele.hidden = false; } + } + const upHandler = (e: PointerEvent) => { + abortDrag(); + FinishDrag(ele, e, dragData, options); }; document.addEventListener("pointermove", moveHandler, true); document.addEventListener("pointerup", upHandler); } - function FinishDrag(dragEle: HTMLElement, e: PointerEvent, options: DragOptions, dragData: { [index: string]: any }) { - dragDiv.removeChild(dragEle); + function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions) { + let parent = dragEle.parentElement; + if (parent) + parent.removeChild(dragEle); const target = document.elementFromPoint(e.x, e.y); + if (parent) + parent.appendChild(dragEle); if (!target) { return; } @@ -126,6 +195,9 @@ export namespace DragManager { data: dragData } })); - options.handlers.dragComplete({}); + if (options) { + options.handlers.dragComplete({}); + } + DocumentDecorations.Instance.Hidden = false; } }
\ No newline at end of file diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx new file mode 100644 index 000000000..abf448c9f --- /dev/null +++ b/src/client/util/RichTextSchema.tsx @@ -0,0 +1,223 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Schema, NodeSpec, MarkSpec, DOMOutputSpecArray } from "prosemirror-model" +import { joinUp, lift, setBlockType, toggleMark, wrapIn } from 'prosemirror-commands' +import { redo, undo } from 'prosemirror-history' +import { orderedList, bulletList, listItem } from 'prosemirror-schema-list' + +const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], + preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0] + +// :: Object +// [Specs](#model.NodeSpec) for the nodes defined in this schema. +export const nodes: { [index: string]: NodeSpec } = { + // :: NodeSpec The top level document node. + doc: { + content: "block+" + }, + + // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // as a `<p>` element. + paragraph: { + content: "inline*", + group: "block", + parseDOM: [{ tag: "p" }], + toDOM() { return pDOM } + }, + + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. + blockquote: { + content: "block+", + group: "block", + defining: true, + parseDOM: [{ tag: "blockquote" }], + toDOM() { return blockquoteDOM } + }, + + // :: NodeSpec A horizontal rule (`<hr>`). + horizontal_rule: { + group: "block", + parseDOM: [{ tag: "hr" }], + toDOM() { return hrDOM } + }, + + // :: NodeSpec A heading textblock, with a `level` attribute that + // should hold the number 1 to 6. Parsed and serialized as `<h1>` to + // `<h6>` elements. + heading: { + attrs: { level: { default: 1 } }, + content: "inline*", + group: "block", + defining: true, + parseDOM: [{ tag: "h1", attrs: { level: 1 } }, + { tag: "h2", attrs: { level: 2 } }, + { tag: "h3", attrs: { level: 3 } }, + { tag: "h4", attrs: { level: 4 } }, + { tag: "h5", attrs: { level: 5 } }, + { tag: "h6", attrs: { level: 6 } }], + toDOM(node: any) { return ["h" + node.attrs.level, 0] } + }, + + // :: NodeSpec A code listing. Disallows marks or non-text inline + // nodes by default. Represented as a `<pre>` element with a + // `<code>` element inside of it. + code_block: { + content: "text*", + marks: "", + group: "block", + code: true, + defining: true, + parseDOM: [{ tag: "pre", preserveWhitespace: "full" }], + toDOM() { return preDOM } + }, + + // :: NodeSpec The text node. + text: { + group: "inline" + }, + + // :: NodeSpec An inline image (`<img>`) node. Supports `src`, + // `alt`, and `href` attributes. The latter two default to the empty + // string. + image: { + inline: true, + attrs: { + src: {}, + alt: { default: null }, + title: { default: null } + }, + group: "inline", + draggable: true, + parseDOM: [{ + tag: "img[src]", getAttrs(dom: any) { + return { + src: dom.getAttribute("src"), + title: dom.getAttribute("title"), + alt: dom.getAttribute("alt") + } + } + }], + toDOM(node: any) { return ["img", node.attrs] } + }, + + // :: NodeSpec A hard line break, represented in the DOM as `<br>`. + hard_break: { + inline: true, + group: "inline", + selectable: false, + parseDOM: [{ tag: "br" }], + toDOM() { return brDOM } + }, + + ordered_list: { + ...orderedList, + content: 'list_item+', + group: 'block' + }, + bullet_list: { + content: 'list_item+', + group: 'block', + parseDOM: [{ tag: "ul" }, { style: "list-style-type=disc;" }], + toDOM() { return ulDOM } + }, + list_item: { + ...listItem, + content: 'paragraph block*' + } +} + +const emDOM: DOMOutputSpecArray = ["em", 0]; +const strongDOM: DOMOutputSpecArray = ["strong", 0]; +const codeDOM: DOMOutputSpecArray = ["code", 0]; +const underlineDOM: DOMOutputSpecArray = ["underline", 0]; + +// :: Object [Specs](#model.MarkSpec) for the marks in the schema. +export const marks: { [index: string]: MarkSpec } = { + // :: MarkSpec A link. Has `href` and `title` attributes. `title` + // defaults to the empty string. Rendered and parsed as an `<a>` + // element. + link: { + attrs: { + href: {}, + title: { default: null } + }, + inclusive: false, + parseDOM: [{ + tag: "a[href]", getAttrs(dom: any) { + return { href: dom.getAttribute("href"), title: dom.getAttribute("title") } + } + }], + toDOM(node: any) { return ["a", node.attrs, 0] } + }, + + // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. + // Has parse rules that also match `<i>` and `font-style: italic`. + em: { + parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style=italic" }], + toDOM() { return emDOM } + }, + + // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules + // also match `<b>` and `font-weight: bold`. + strong: { + parseDOM: [{ tag: "strong" }, + { tag: "b" }, + { style: "font-weight" }], + toDOM() { return strongDOM } + }, + + underline: { + parseDOM: [ + { tag: 'u' }, + { style: 'text-decoration=underline' } + ], + toDOM: () => ['span', { + style: 'text-decoration:underline' + }] + }, + + strikethrough: { + parseDOM: [ + { tag: 'strike' }, + { style: 'text-decoration=line-through' }, + { style: 'text-decoration-line=line-through' } + ], + toDOM: () => ['span', { + style: 'text-decoration-line:line-through' + }] + }, + + subscript: { + excludes: 'superscript', + parseDOM: [ + { tag: 'sub' }, + { style: 'vertical-align=sub' } + ], + toDOM: () => ['sub'] + }, + + superscript: { + excludes: 'subscript', + parseDOM: [ + { tag: 'sup' }, + { style: 'vertical-align=super' } + ], + toDOM: () => ['sup'] + }, + + + // :: MarkSpec Code font mark. Represented as a `<code>` element. + code: { + parseDOM: [{ tag: "code" }], + toDOM() { return codeDOM } + } +} + +// :: Schema +// This schema rougly corresponds to the document schema used by +// [CommonMark](http://commonmark.org/), minus the list elements, +// which are defined in the [`prosemirror-schema-list`](#schema-list) +// module. +// +// To reuse elements from this schema, extend or read from its +// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec). +export const schema = new Schema({ nodes, marks })
\ No newline at end of file diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 6bc5fa412..46bd1a206 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -1,12 +1,21 @@ // import * as ts from "typescript" let ts = (window as any).ts; import { Opt, Field } from "../../fields/Field"; -import { Document as DocumentImport } from "../../fields/Document"; -import { NumberField as NumberFieldImport, NumberField } from "../../fields/NumberField"; -import { ImageField as ImageFieldImport } from "../../fields/ImageField"; -import { TextField as TextFieldImport, TextField } from "../../fields/TextField"; -import { RichTextField as RichTextFieldImport } from "../../fields/RichTextField"; -import { KeyStore as KeyStoreImport } from "../../fields/Key"; +import { Document } from "../../fields/Document"; +import { NumberField } from "../../fields/NumberField"; +import { ImageField } from "../../fields/ImageField"; +import { TextField } from "../../fields/TextField"; +import { RichTextField } from "../../fields/RichTextField"; +import { KeyStore } from "../../fields/KeyStore"; +import { ListField } from "../../fields/ListField"; +// // @ts-ignore +// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts' +// // @ts-ignore +// import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts' + +// @ts-ignore +import * as typescriptlib from '!!raw-loader!./type_decls.d' + export interface ExecutableScript { (): any; @@ -14,23 +23,25 @@ export interface ExecutableScript { compiled: boolean; } -function ExecScript(script: string, diagnostics: Opt<any[]>): ExecutableScript { +function Compile(script: string | undefined, diagnostics: Opt<any[]>, scope: { [name: string]: any }): ExecutableScript { const compiled = !(diagnostics && diagnostics.some(diag => diag.category == ts.DiagnosticCategory.Error)); let func: () => Opt<Field>; - if (compiled) { + if (compiled && script) { + let fieldTypes = [Document, NumberField, TextField, ImageField, RichTextField, ListField]; + let paramNames = ["KeyStore", ...fieldTypes.map(fn => fn.name)]; + let params: any[] = [KeyStore, ...fieldTypes] + for (let prop in scope) { + if (prop === "this") { + continue; + } + paramNames.push(prop); + params.push(scope[prop]); + } + let thisParam = scope["this"]; + let compiledFunction = new Function(...paramNames, script); func = function (): Opt<Field> { - let KeyStore = KeyStoreImport; - let Document = DocumentImport; - let NumberField = NumberFieldImport; - let TextField = TextFieldImport; - let ImageField = ImageFieldImport; - let RichTextField = RichTextFieldImport; - let window = undefined; - let document = undefined; - let retVal = eval(script); - - return retVal; + return compiledFunction.apply(thisParam, params) }; } else { func = () => undefined; @@ -42,10 +53,73 @@ function ExecScript(script: string, diagnostics: Opt<any[]>): ExecutableScript { }); } -export function CompileScript(script: string): ExecutableScript { - let result = (window as any).ts.transpileModule(script, {}) +interface File { + fileName: string; + content: string; +} + +// class ScriptingCompilerHost implements ts.CompilerHost { +class ScriptingCompilerHost { + files: File[] = []; + + // getSourceFile(fileName: string, languageVersion: ts.ScriptTarget, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): ts.SourceFile | undefined { + getSourceFile(fileName: string, languageVersion: any, onError?: ((message: string) => void) | undefined, shouldCreateNewSourceFile?: boolean | undefined): any | undefined { + let contents = this.readFile(fileName); + if (contents !== undefined) { + return ts.createSourceFile(fileName, contents, languageVersion, true); + } + return undefined; + } + // getDefaultLibFileName(options: ts.CompilerOptions): string { + getDefaultLibFileName(options: any): string { + return 'node_modules/typescript/lib/lib.d.ts' // No idea what this means... + } + writeFile(fileName: string, content: string) { + const file = this.files.find(file => file.fileName === fileName); + if (file) { + file.content = content; + } else { + this.files.push({ fileName, content }) + } + } + getCurrentDirectory(): string { + return ''; + } + getCanonicalFileName(fileName: string): string { + return this.useCaseSensitiveFileNames() ? fileName : fileName.toLowerCase(); + } + useCaseSensitiveFileNames(): boolean { + return true; + } + getNewLine(): string { + return '\n'; + } + fileExists(fileName: string): boolean { + return this.files.some(file => file.fileName === fileName); + } + readFile(fileName: string): string | undefined { + let file = this.files.find(file => file.fileName === fileName); + if (file) { + return file.content; + } + return undefined; + } +} + +export function CompileScript(script: string, scope?: { [name: string]: any }, addReturn: boolean = false): ExecutableScript { + let host = new ScriptingCompilerHost; + let funcScript = `(function() { + ${addReturn ? `return ${script};` : script} + })()` + host.writeFile("file.ts", funcScript); + host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib); + let program = ts.createProgram(["file.ts"], {}, host); + let testResult = program.emit(); + let outputText = "return " + host.readFile("file.js"); + + let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); - return ExecScript(result.outputText, result.diagnostics); + return Compile(outputText, diagnostics, scope || {}); } export function ToField(data: any): Opt<Field> { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 0759ae110..1a711ae64 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,13 +1,13 @@ -import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView"; import { observable, action } from "mobx"; +import { DocumentView } from "../views/nodes/DocumentView"; export namespace SelectionManager { class Manager { @observable - SelectedDocuments: Array<CollectionFreeFormDocumentView> = []; + SelectedDocuments: Array<DocumentView> = []; @action - SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void { + SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it if (!ctrlPressed) { manager.SelectedDocuments = []; @@ -21,11 +21,11 @@ export namespace SelectionManager { const manager = new Manager; - export function SelectDoc(doc: CollectionFreeFormDocumentView, ctrlPressed: boolean): void { + export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void { manager.SelectDoc(doc, ctrlPressed) } - export function IsSelected(doc: CollectionFreeFormDocumentView): boolean { + export function IsSelected(doc: DocumentView): boolean { return manager.SelectedDocuments.indexOf(doc) !== -1; } @@ -33,7 +33,7 @@ export namespace SelectionManager { manager.SelectedDocuments = [] } - export function SelectedDocuments(): Array<CollectionFreeFormDocumentView> { + export function SelectedDocuments(): Array<DocumentView> { return manager.SelectedDocuments; } }
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss new file mode 100644 index 000000000..fa43f5326 --- /dev/null +++ b/src/client/util/TooltipTextMenu.scss @@ -0,0 +1,54 @@ + +.tooltipMenu { + position: absolute; + z-index: 20; + background: rgb(19, 18, 18); + border: 1px solid silver; + border-radius: 4px; + padding: 2px 10px; + margin-bottom: 7px; + -webkit-transform: translateX(-50%); + transform: translateX(-50%); +} + +.tooltipMenu:before { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -6px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: silver; + } + .tooltipMenu:after { + content: ""; + height: 0; width: 0; + position: absolute; + left: 50%; + margin-left: -5px; + bottom: -4.5px; + border: 5px solid transparent; + border-bottom-width: 0; + border-top-color: black; + } + + .menuicon { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + //color: rgb(19, 18, 18); + color: white; + line-height: 1; + padding: 0px 2px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 10px; + } + .strong, .heading { font-weight: bold; } + .em { font-style: italic; } + .underline {text-decoration: underline} + .superscript {vertical-align:super} + .subscript { vertical-align:sub } + .strikethrough {text-decoration-line:line-through}
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx new file mode 100644 index 000000000..3b87fe9de --- /dev/null +++ b/src/client/util/TooltipTextMenu.tsx @@ -0,0 +1,125 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +import { baseKeymap } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +const { exampleSetup } = require("prosemirror-example-setup") +import { EditorState, Transaction, } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "./RichTextSchema"; +import React = require("react") +import "./TooltipTextMenu.scss"; +const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +import { library } from '@fortawesome/fontawesome-svg-core' +import { wrapInList, bulletList } from 'prosemirror-schema-list' +import { + faListUl, +} from '@fortawesome/free-solid-svg-icons'; + + + +export class TooltipTextMenu { + + private tooltip: HTMLElement; + + constructor(view: EditorView) { + this.tooltip = document.createElement("div"); + this.tooltip.className = "tooltipMenu"; + + //add the div which is the tooltip + view.dom.parentNode!.appendChild(this.tooltip); + + //add additional icons + library.add(faListUl); + + //add the buttons to the tooltip + let items = [ + { command: toggleMark(schema.marks.strong), dom: this.icon("B", "strong") }, + { command: toggleMark(schema.marks.em), dom: this.icon("i", "em") }, + { command: toggleMark(schema.marks.underline), dom: this.icon("U", "underline") }, + { command: toggleMark(schema.marks.strikethrough), dom: this.icon("S", "strikethrough") }, + { command: toggleMark(schema.marks.superscript), dom: this.icon("s", "superscript") }, + { command: toggleMark(schema.marks.subscript), dom: this.icon("s", "subscript") }, + { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") } + ] + items.forEach(({ dom }) => this.tooltip.appendChild(dom)); + + //pointer down handler to activate button effects + this.tooltip.addEventListener("pointerdown", e => { + e.preventDefault(); + view.focus(); + items.forEach(({ command, dom }) => { + if (dom.contains(e.srcElement)) { + command(view.state, view.dispatch, view) + } + }) + }) + + this.update(view, undefined); + } + + // Helper function to create menu icons + icon(text: string, name: string) { + let span = document.createElement("span"); + span.className = "menuicon " + name; + span.title = name; + span.textContent = text; + return span; + } + + blockActive(view: EditorView) { + const { $from, to } = view.state.selection + + return to <= $from.end() && $from.parent.hasMarkup(schema.nodes.bulletList); + } + + //this doesn't currently work but hopefully will soon + unorderedListIcon(): HTMLSpanElement { + let span = document.createElement("span"); + let icon = document.createElement("FontAwesomeIcon"); + icon.className = "menuicon fa fa-smile-o"; + span.appendChild(icon); + return span; + } + + // Create an icon for a heading at the given level + heading(level: number) { + return { + command: setBlockType(schema.nodes.heading, { level }), + dom: this.icon("H" + level, "heading") + } + } + + //updates the tooltip menu when the selection changes + update(view: EditorView, lastState: EditorState | undefined) { + let state = view.state + // Don't do anything if the document/selection didn't change + if (lastState && lastState.doc.eq(state.doc) && + lastState.selection.eq(state.selection)) return + + // Hide the tooltip if the selection is empty + if (state.selection.empty) { + this.tooltip.style.display = "none" + return + } + + // Otherwise, reposition it and update its content + this.tooltip.style.display = "" + let { from, to } = state.selection + // These are in screen coordinates + //check this - tranform + let start = view.coordsAtPos(from), end = view.coordsAtPos(to) + // The box in which the tooltip is positioned, to use as base + let box = this.tooltip.offsetParent!.getBoundingClientRect() + // Find a center-ish x position from the selection endpoints (when + // crossing lines, end may be more to the left) + let left = Math.max((start.left + end.left) / 2, start.left + 3) + this.tooltip.style.left = (left - box.left) + "px" + let width = Math.abs(start.left - end.left) / 2; + let mid = Math.min(start.left, end.left) + width; + //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 + this.tooltip.style.width = 120 + "px"; + this.tooltip.style.bottom = (box.bottom - start.top) + "px"; + } + + destroy() { this.tooltip.remove() } +}
\ No newline at end of file diff --git a/src/client/util/Transform.ts b/src/client/util/Transform.ts index 7861ed308..3e1039166 100644 --- a/src/client/util/Transform.ts +++ b/src/client/util/Transform.ts @@ -7,6 +7,10 @@ export class Transform { return new Transform(0, 0, 1); } + get TranslateX(): number { return this._translateX; } + get TranslateY(): number { return this._translateY; } + get Scale(): number { return this._scale; } + constructor(x: number, y: number, scale: number) { this._translateX = x; this._translateY = y; @@ -19,56 +23,67 @@ export class Transform { return this; } - translated = (x: number, y: number): Transform => { - return this.copy().translate(x, y); - } - - preTranslate = (x: number, y: number): Transform => { - this._translateX += x * this._scale; - this._translateY += y * this._scale; + scale = (scale: number): Transform => { + this._scale *= scale; + this._translateX *= scale; + this._translateY *= scale; return this; } - preTranslated = (x: number, y: number): Transform => { - return this.copy().preTranslate(x, y); + scaleAbout = (scale: number, x: number, y: number): Transform => { + this._translateX += x * this._scale - x * this._scale * scale; + this._translateY += y * this._scale - y * this._scale * scale; + this._scale *= scale; + return this; } - scale = (scale: number): Transform => { - this._scale *= scale; + transform = (transform: Transform): Transform => { + this._translateX = transform._translateX + transform._scale * this._translateX; + this._translateY = transform._translateY + transform._scale * this._translateY; + this._scale *= transform._scale; return this; } - scaled = (scale: number): Transform => { - return this.copy().scale(scale); + preTranslate = (x: number, y: number): Transform => { + this._translateX += this._scale * x; + this._translateY += this._scale * y; + return this; } preScale = (scale: number): Transform => { this._scale *= scale; - this._translateX *= scale; - this._translateY *= scale; return this; } - preScaled = (scale: number): Transform => { - return this.copy().preScale(scale); - } - - transform = (transform: Transform): Transform => { + preTransform = (transform: Transform): Transform => { this._translateX += transform._translateX * this._scale; this._translateY += transform._translateY * this._scale; this._scale *= transform._scale; return this; } - transformed = (transform: Transform): Transform => { - return this.copy().transform(transform); + translated = (x: number, y: number): Transform => { + return this.copy().translate(x, y); } - preTransform = (transform: Transform): Transform => { - this._translateX = transform._translateX + this._translateX * transform._scale; - this._translateY = transform._translateY + this._translateY * transform._scale; - this._scale *= transform._scale; - return this; + preTranslated = (x: number, y: number): Transform => { + return this.copy().preTranslate(x, y); + } + + scaled = (scale: number): Transform => { + return this.copy().scale(scale); + } + + scaledAbout = (scale: number, x: number, y: number): Transform => { + return this.copy().scaleAbout(scale, x, y); + } + + preScaled = (scale: number): Transform => { + return this.copy().preScale(scale); + } + + transformed = (transform: Transform): Transform => { + return this.copy().transform(transform); } preTransformed = (transform: Transform): Transform => { @@ -83,6 +98,16 @@ export class Transform { return [x, y]; } + transformDirection = (x: number, y: number): [number, number] => { + return [x * this._scale, y * this._scale]; + } + + transformBounds(x: number, y: number, width: number, height: number): { x: number, y: number, width: number, height: number } { + [x, y] = this.transformPoint(x, y); + [width, height] = this.transformDirection(width, height); + return { x, y, width, height }; + } + inverse = () => { return new Transform(-this._translateX / this._scale, -this._translateY / this._scale, 1 / this._scale) } diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts new file mode 100644 index 000000000..46ad558f3 --- /dev/null +++ b/src/client/util/UndoManager.ts @@ -0,0 +1,133 @@ +import { observable, action } from "mobx"; + +function propertyDecorator(target: any, key: string | symbol) { + Object.defineProperty(target, key, { + configurable: true, + enumerable: false, + get: function () { + return 5; + }, + set: function (value: any) { + Object.defineProperty(this, key, { + enumerable: false, + writable: true, + configurable: true, + value: function (...args: any[]) { + try { + UndoManager.StartBatch(); + return value.apply(this, args); + } finally { + UndoManager.EndBatch(); + } + } + }) + } + }) +} +export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { + if (!descriptor) { + propertyDecorator(target, key); + return; + } + const oldFunction = descriptor.value; + + descriptor.value = function (...args: any[]) { + try { + UndoManager.StartBatch() + return oldFunction.apply(this, args) + } finally { + UndoManager.EndBatch() + } + } + + return descriptor; +} + +export namespace UndoManager { + export interface UndoEvent { + undo: () => void; + redo: () => void; + } + type UndoBatch = UndoEvent[]; + + let undoStack: UndoBatch[] = observable([]); + let redoStack: UndoBatch[] = observable([]); + let currentBatch: UndoBatch | undefined; + let batchCounter = 0; + let undoing = false; + + export function AddEvent(event: UndoEvent): void { + if (currentBatch && batchCounter && !undoing) { + currentBatch.push(event); + } + } + + export function CanUndo(): boolean { + return undoStack.length > 0; + } + + export function CanRedo(): boolean { + return redoStack.length > 0; + } + + export function StartBatch(): void { + batchCounter++; + if (batchCounter > 0) { + currentBatch = []; + } + } + + export const EndBatch = action(() => { + batchCounter--; + if (batchCounter === 0 && currentBatch && currentBatch.length) { + undoStack.push(currentBatch); + redoStack.length = 0; + currentBatch = undefined; + } + }) + + export function RunInBatch(fn: () => void) { + StartBatch(); + fn(); + EndBatch(); + } + + export const Undo = action(() => { + if (undoStack.length === 0) { + return; + } + + let commands = undoStack.pop(); + if (!commands) { + return; + } + + undoing = true; + for (let i = commands.length - 1; i >= 0; i--) { + commands[i].undo(); + } + undoing = false; + + redoStack.push(commands); + }) + + export const Redo = action(() => { + if (redoStack.length === 0) { + return; + } + + let commands = redoStack.pop(); + if (!commands) { + return; + } + + undoing = true; + for (let i = 0; i < commands.length; i++) { + commands[i].redo(); + } + undoing = false; + + undoStack.push(commands); + }) + +}
\ No newline at end of file diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d new file mode 100644 index 000000000..679f73f42 --- /dev/null +++ b/src/client/util/type_decls.d @@ -0,0 +1,215 @@ +//@ts-ignore +declare type PropertyKey = string | number | symbol; +interface Array<T> { + length: number; + toString(): string; + toLocaleString(): string; + pop(): T | undefined; + push(...items: T[]): number; + concat(...items: ConcatArray<T>[]): T[]; + concat(...items: (T | ConcatArray<T>)[]): T[]; + join(separator?: string): string; + reverse(): T[]; + shift(): T | undefined; + slice(start?: number, end?: number): T[]; + sort(compareFn?: (a: T, b: T) => number): this; + splice(start: number, deleteCount?: number): T[]; + splice(start: number, deleteCount: number, ...items: T[]): T[]; + unshift(...items: T[]): number; + indexOf(searchElement: T, fromIndex?: number): number; + lastIndexOf(searchElement: T, fromIndex?: number): number; + every(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; + some(callbackfn: (value: T, index: number, array: T[]) => boolean, thisArg?: any): boolean; + forEach(callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any): void; + map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; + filter<S extends T>(callbackfn: (value: T, index: number, array: T[]) => value is S, thisArg?: any): S[]; + filter(callbackfn: (value: T, index: number, array: T[]) => any, thisArg?: any): T[]; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduce(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduce<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T): T; + reduceRight(callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T): T; + reduceRight<U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U): U; + + [n: number]: T; +} + +interface Function { + apply(this: Function, thisArg: any, argArray?: any): any; + call(this: Function, thisArg: any, ...argArray: any[]): any; + bind(this: Function, thisArg: any, ...argArray: any[]): any; + toString(): string; + + prototype: any; + readonly length: number; + + // Non-standard extensions + arguments: any; + caller: Function; +} +interface Boolean { + valueOf(): boolean; +} +interface Number { + toString(radix?: number): string; + toFixed(fractionDigits?: number): string; + toExponential(fractionDigits?: number): string; + toPrecision(precision?: number): string; + valueOf(): number; +} +interface IArguments { + [index: number]: any; + length: number; + callee: Function; +} +interface RegExp { + readonly flags: string; + readonly sticky: boolean; + readonly unicode: boolean; +} +interface String { + codePointAt(pos: number): number | undefined; + includes(searchString: string, position?: number): boolean; + endsWith(searchString: string, endPosition?: number): boolean; + normalize(form: "NFC" | "NFD" | "NFKC" | "NFKD"): string; + normalize(form?: string): string; + repeat(count: number): string; + startsWith(searchString: string, position?: number): boolean; + anchor(name: string): string; + big(): string; + blink(): string; + bold(): string; + fixed(): string; + fontcolor(color: string): string; + fontsize(size: number): string; + fontsize(size: string): string; + italics(): string; + link(url: string): string; + small(): string; + strike(): string; + sub(): string; + sup(): string; +} +interface Object { + constructor: Function; + toString(): string; + toLocaleString(): string; + valueOf(): Object; + hasOwnProperty(v: PropertyKey): boolean; + isPrototypeOf(v: Object): boolean; + propertyIsEnumerable(v: PropertyKey): boolean; +} +interface ConcatArray<T> { + readonly length: number; + readonly [n: number]: T; + join(separator?: string): string; + slice(start?: number, end?: number): T[]; +} +interface URL { + hash: string; + host: string; + hostname: string; + href: string; + readonly origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + username: string; + toJSON(): string; +} + +declare type FieldId = string; + +declare abstract class Field { + Id: FieldId; + abstract ToScriptString(): string; + abstract TrySetValue(value: any): boolean; + abstract GetValue(): any; + abstract Copy(): Field; +} + +declare abstract class BasicField<T> extends Field { + constructor(data: T); + Data: T; + TrySetValue(value: any): boolean; + GetValue(): any; +} + +declare class TextField extends BasicField<string>{ + constructor(); + constructor(data: string); + ToScriptString(): string; + Copy(): Field; +} +declare class ImageField extends BasicField<URL>{ + constructor(); + constructor(data: URL); + ToScriptString(): string; + Copy(): Field; +} +declare class HtmlField extends BasicField<string>{ + constructor(); + constructor(data: string); + ToScriptString(): string; + Copy(): Field; +} +declare class NumberField extends BasicField<number>{ + constructor(); + constructor(data: number); + ToScriptString(): string; + Copy(): Field; +} +declare class WebField extends BasicField<URL>{ + constructor(); + constructor(data: URL); + ToScriptString(): string; + Copy(): Field; +} +declare class ListField<T> extends BasicField<T[]>{ + constructor(); + constructor(data: T[]); + ToScriptString(): string; + Copy(): Field; +} +declare class Key extends Field { + Name: string; + TrySetValue(value: any): boolean; + GetValue(): any; + Copy(): Field; + ToScriptString(): string; +} +declare type FIELD_WAITING = "<Waiting>"; +declare type Opt<T> = T | undefined; +declare type FieldValue<T> = Opt<T> | FIELD_WAITING; +// @ts-ignore +declare class Document extends Field { + TrySetValue(value: any): boolean; + GetValue(): any; + Copy(): Field; + ToScriptString(): string; + + Width(): number; + Height(): number; + Scale(): number; + Title: string; + + Get(key: Key): FieldValue<Field>; + GetAsync(key: Key, callback: (field: Field) => void): boolean; + GetOrCreateAsync<T extends Field>(key: Key, ctor: { new(): T }, callback: (field: T) => void): void; + GetT<T extends Field>(key: Key, ctor: { new(): T }): FieldValue<T>; + GetOrCreate<T extends Field>(key: Key, ctor: { new(): T }): T; + GetData<T, U extends Field & { Data: T }>(key: Key, ctor: { new(): U }, defaultVal: T): T; + GetHtml(key: Key, defaultVal: string): string; + GetNumber(key: Key, defaultVal: number): number; + GetText(key: Key, defaultVal: string): string; + GetList<T extends Field>(key: Key, defaultVal: T[]): T[]; + Set(key: Key, field: Field | undefined): void; + SetData<T, U extends Field & { Data: T }>(key: Key, value: T, ctor: { new(): U }): void; + SetText(key: Key, value: string): void; + SetNumber(key: Key, value: number): void; + GetPrototype(): FieldValue<Document>; + GetAllPrototypes(): Document[]; + MakeDelegate(): Document; +} diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index 234f82eb9..ea40c8e99 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -3,16 +3,16 @@ display: flex; z-index: 1000; box-shadow: #AAAAAA .2vw .2vw .4vw; + flex-direction: column; } .contextMenu-item { - width: 10vw; - height: 4vh; - background: #DDDDDD; + width: auto; + height: auto; + background: #F0F8FF; display: flex; - justify-content: center; + justify-content: left; align-items: center; - flex-direction: column; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -20,11 +20,18 @@ -ms-user-select: none; user-select: none; transition: all .1s; + border-width: .11px; + border-style: none; + border-color: rgb(187, 186, 186); + border-bottom-style: solid; + padding: 10px; + white-space: nowrap; + font-size: 1.5vw; } .contextMenu-item:hover { transition: all .1s; - background: #AAAAAA + background: #B0E0E6; } .contextMenu-description { diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 4f26a75d2..fcb934860 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -1,6 +1,6 @@ import React = require("react"); import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem"; -import { observable } from "mobx"; +import { observable, action } from "mobx"; import { observer } from "mobx-react"; import "./ContextMenu.scss" @@ -12,6 +12,8 @@ export class ContextMenu extends React.Component { @observable private _pageX: number = 0; @observable private _pageY: number = 0; @observable private _display: string = "none"; + @observable private _searchString: string = ""; + private ref: React.RefObject<HTMLDivElement>; @@ -23,11 +25,13 @@ export class ContextMenu extends React.Component { ContextMenu.Instance = this; } + @action clearItems() { this._items = [] this._display = "none" } + @action addItem(item: ContextMenuProps) { if (this._items.indexOf(item) === -1) { this._items.push(item); @@ -38,10 +42,13 @@ export class ContextMenu extends React.Component { return this._items; } + @action displayMenu(x: number, y: number) { this._pageX = x this._pageY = y + this._searchString = ""; + this._display = "flex" } @@ -59,10 +66,18 @@ export class ContextMenu extends React.Component { render() { return ( <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}> - {this._items.map(prop => { + <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input> + {this._items.filter(prop => { + return prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1; + }).map(prop => { return <ContextMenuItem {...prop} key={prop.description} /> })} </div> ) } + + @action + onChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this._searchString = e.target.value; + } }
\ No newline at end of file diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 8f00f8b3d..4801c1555 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,11 +1,19 @@ import React = require("react"); -import { ContextMenu } from "./ContextMenu"; export interface ContextMenuProps { description: string; event: (e: React.MouseEvent<HTMLDivElement>) => void; } +export interface SubmenuProps { + description: string; + subitems: ContextMenuProps[]; +} + +export interface ContextMenuItemProps { + type: ContextMenuProps | SubmenuProps +} + export class ContextMenuItem extends React.Component<ContextMenuProps> { render() { return ( diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 4f16453df..0a126de1f 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -3,17 +3,26 @@ import React = require("react"); import { SelectionManager } from "../util/SelectionManager"; import { observer } from "mobx-react"; import './DocumentDecorations.scss' +<<<<<<< HEAD import { CollectionFreeFormView } from "./collections/CollectionFreeFormView"; import ContentEditable from 'react-contenteditable' import { KeyStore } from '../../fields/Key' +======= +import { KeyStore } from '../../fields/KeyStore' +import { NumberField } from "../../fields/NumberField"; +>>>>>>> 96eede5f7d1706a3f7ac6ee02a85bb3da217f467 @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations private _resizer = "" private _isPointerDown = false; +<<<<<<< HEAD @observable private _opacity = 1; private keyinput: React.RefObject<HTMLInputElement>; +======= + @observable private _hidden = false; +>>>>>>> 96eede5f7d1706a3f7ac6ee02a85bb3da217f467 constructor(props: Readonly<{}>) { super(props) @@ -43,28 +52,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @computed get Bounds(): { x: number, y: number, b: number, r: number } { - return SelectionManager.SelectedDocuments().reduce((bounds, element) => { - if (element.props.ContainingCollectionView != undefined && - !(element.props.ContainingCollectionView instanceof CollectionFreeFormView)) { + return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => { + if (documentView.props.isTopMost) { return bounds; } - var spt = element.TransformToScreenPoint(0, 0); - var bpt = element.TransformToScreenPoint(element.width, element.height); + let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight()); return { - x: Math.min(spt.ScreenX, bounds.x), y: Math.min(spt.ScreenY, bounds.y), - r: Math.max(bpt.ScreenX, bounds.r), b: Math.max(bpt.ScreenY, bounds.b) + x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), + r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b) } }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); } - @computed - get opacity(): number { - return this._opacity - } - set opacity(o: number) { - this._opacity = Math.min(Math.max(0, o), 1) - } + @computed + public get Hidden() { return this._hidden; } + public set Hidden(value: boolean) { this._hidden = value; } onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); @@ -128,17 +133,29 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } SelectionManager.SelectedDocuments().forEach(element => { - const rect = element.screenRect; + const rect = element.screenRect(); if (rect.width !== 0) { - let scale = element.width / rect.width; - let actualdW = Math.max(element.width + (dW * scale), 20); - let actualdH = Math.max(element.height + (dH * scale), 20); - element.props.Document.SetNumber(KeyStore.X, element.props.Document.GetNumber(KeyStore.X, 0) + dX * (actualdW - element.width)); - element.props.Document.SetNumber(KeyStore.Y, element.props.Document.GetNumber(KeyStore.Y, 0) + dY * (actualdH - element.height)); - if (Math.abs(dW) > Math.abs(dH)) - element.width = actualdW; - else - element.height = actualdH; + let doc = element.props.Document; + let width = doc.GetNumber(KeyStore.Width, 0); + let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0); + let nheight = doc.GetNumber(KeyStore.NativeHeight, 0); + let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0); + let x = doc.GetOrCreate(KeyStore.X, NumberField); + let y = doc.GetOrCreate(KeyStore.Y, NumberField); + let scale = width / rect.width; + let actualdW = Math.max(width + (dW * scale), 20); + let actualdH = Math.max(height + (dH * scale), 20); + x.Data += dX * (actualdW - width); + y.Data += dY * (actualdH - height); + var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0); + if (nativeWidth > 0 && nativeHeight > 0) { + if (Math.abs(dW) > Math.abs(dH)) + actualdH = nativeHeight / nativeWidth * actualdW; + else actualdW = nativeWidth / nativeHeight * actualdH; + } + doc.SetNumber(KeyStore.Width, actualdW); + doc.SetNumber(KeyStore.Height, actualdH); } }) } @@ -155,13 +172,24 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> render() { var bounds = this.Bounds; + if (this.Hidden) { + return (null); + } + if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + console.log("DocumentDecorations: Bounds Error") + return (null); + } return ( <div id="documentDecorations-container" style={{ width: (bounds.r - bounds.x + 20 + 20) + "px", height: (bounds.b - bounds.y + 40 + 20) + "px", left: bounds.x - 20, +<<<<<<< HEAD top: bounds.y - 20 - 20, opacity: this.opacity +======= + top: bounds.y - 20, +>>>>>>> 96eede5f7d1706a3f7ac6ee02a85bb3da217f467 }}> <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.state.value} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} /> {/* <div className="title" onPointerDown={this.onPointerDown}>{document.title}</div> */} diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 3d1c2ebf4..84b1b91c3 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -3,12 +3,30 @@ import { observer } from 'mobx-react'; import { observable, action } from 'mobx'; export interface EditableProps { + /** + * Called to get the initial value for editing + * */ GetValue(): string; + + /** + * Called to apply changes + * @param value - The string entered by the user to set the value to + * @returns `true` if setting the value was successful, `false` otherwise + * */ SetValue(value: string): boolean; + + /** + * The contents to render when not editing + */ contents: any; height: number } +/** + * Customizable view that can be given an arbitrary view to render normally, + * but can also be edited with customizable functions to get a string version + * of the content, and set the value based on the entered string. + */ @observer export class EditableView extends React.Component<EditableProps> { @observable @@ -17,8 +35,9 @@ export class EditableView extends React.Component<EditableProps> { @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key == "Enter" && !e.ctrlKey) { - this.props.SetValue(e.currentTarget.value); - this.editing = false; + if (this.props.SetValue(e.currentTarget.value)) { + this.editing = false; + } } else if (e.key == "Escape") { this.editing = false; } @@ -27,12 +46,11 @@ export class EditableView extends React.Component<EditableProps> { render() { if (this.editing) { return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)} - style={{ width: "100%" }}></input> + style={{ display: "inline" }}></input> } else { return ( - <div style={{ alignItems: "center", display: "flex", height: "100%", maxHeight: `${this.props.height}` }} - onClick={action(() => this.editing = true)} - > + <div className="editableView-container-editing" style={{ display: "inline", height: "100%", maxHeight: `${this.props.height}` }} + onClick={action(() => this.editing = true)}> {this.props.contents} </div> ) diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss new file mode 100644 index 000000000..f654b194b --- /dev/null +++ b/src/client/views/InkingCanvas.scss @@ -0,0 +1,32 @@ +.inking-canvas { + position: fixed; + top: -50000px; + left: -50000px; // z-index: 99; //overlays ink on top of everything + svg { + width: 100000px; + height: 100000px; + .highlight { + mix-blend-mode: multiply; + } + } +} + +.inking-control { + position: absolute; + right: 0; + bottom: 75px; + text-align: right; + .ink-panel { + margin-top: 12px; + &:first { + margin-top: 0; + } + } + .ink-size { + display: flex; + justify-content: space-between; + input { + width: 85%; + } + } +}
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx new file mode 100644 index 000000000..0d87c1239 --- /dev/null +++ b/src/client/views/InkingCanvas.tsx @@ -0,0 +1,171 @@ +import { observer } from "mobx-react"; +import { action, computed } from "mobx"; +import { InkingControl } from "./InkingControl"; +import React = require("react"); +import { Transform } from "../util/Transform"; +import { Document } from "../../fields/Document"; +import { KeyStore } from "../../fields/KeyStore"; +import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField"; +import { JsxArgs } from "./nodes/DocumentView"; +import { InkingStroke } from "./InkingStroke"; +import "./InkingCanvas.scss" +import { CollectionDockingView } from "./collections/CollectionDockingView"; +import { Utils } from "../../Utils"; +import { FieldWaiting } from "../../fields/Field"; +import { getMapLikeKeys } from "mobx/lib/internal"; + + +interface InkCanvasProps { + getScreenTransform: () => Transform; + Document: Document; +} + +@observer +export class InkingCanvas extends React.Component<InkCanvasProps> { + + private _isDrawing: boolean = false; + private _idGenerator: string = ""; + + constructor(props: Readonly<InkCanvasProps>) { + super(props); + } + + @computed + get inkData(): StrokeMap { + let map = this.props.Document.GetT(KeyStore.Ink, InkField); + if (!map || map === FieldWaiting) { + return new Map; + } + return new Map(map.Data); + } + + set inkData(value: StrokeMap) { + this.props.Document.SetData(KeyStore.Ink, value, InkField); + } + + componentDidMount() { + document.addEventListener("mouseup", this.handleMouseUp); + } + + componentWillUnmount() { + document.removeEventListener("mouseup", this.handleMouseUp); + } + + + @action + handleMouseDown = (e: React.PointerEvent): void => { + if (e.button != 0 || + InkingControl.Instance.selectedTool === InkTool.None) { + return; + } + e.stopPropagation() + if (InkingControl.Instance.selectedTool === InkTool.Eraser) { + return + } + e.stopPropagation() + const point = this.relativeCoordinatesForEvent(e); + + // start the new line, saves a uuid to represent the field of the stroke + this._idGenerator = Utils.GenerateGuid(); + let data = this.inkData; + data.set(this._idGenerator, + { + pathData: [point], + color: InkingControl.Instance.selectedColor, + width: InkingControl.Instance.selectedWidth, + tool: InkingControl.Instance.selectedTool, + page: this.props.Document.GetNumber(KeyStore.CurPage, 0) + }); + this.inkData = data; + this._isDrawing = true; + } + + @action + handleMouseMove = (e: React.PointerEvent): void => { + if (!this._isDrawing || + InkingControl.Instance.selectedTool === InkTool.None) { + return; + } + e.stopPropagation() + if (InkingControl.Instance.selectedTool === InkTool.Eraser) { + return + } + const point = this.relativeCoordinatesForEvent(e); + + // add points to new line as it is being drawn + let data = this.inkData; + let strokeData = data.get(this._idGenerator); + if (strokeData) { + strokeData.pathData.push(point); + data.set(this._idGenerator, strokeData); + } + + this.inkData = data; + } + + @action + handleMouseUp = (e: MouseEvent): void => { + this._isDrawing = false; + } + + relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => { + let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY); + x += 50000 + y += 50000 + return { x, y }; + } + + @action + removeLine = (id: string): void => { + let data = this.inkData; + data.delete(id); + this.inkData = data; + } + + render() { + // styling for cursor + let canvasStyle = {}; + if (InkingControl.Instance.selectedTool === InkTool.None) { + canvasStyle = { pointerEvents: "none" }; + } else { + canvasStyle = { pointerEvents: "auto", cursor: "crosshair" }; + } + + // get data from server + // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField); + // if (!inkField || inkField == "<Waiting>") { + // return (<div className="inking-canvas" style={canvasStyle} + // onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} > + // <svg> + // </svg> + // </div >) + // } + + let lines = this.inkData; + + // parse data from server + let paths: Array<JSX.Element> = [] + let curPage = this.props.Document.GetNumber(KeyStore.CurPage, 0) + Array.from(lines).map(item => { + let id = item[0]; + let strokeData = item[1]; + if (strokeData.page == 0 || strokeData.page == curPage) + paths.push(<InkingStroke key={id} id={id} + line={strokeData.pathData} + color={strokeData.color} + width={strokeData.width} + tool={strokeData.tool} + deleteCallback={this.removeLine} />) + }) + + return ( + + <div className="inking-canvas" style={canvasStyle} + onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} > + <svg> + {paths} + </svg> + </div > + ) + } +}
\ No newline at end of file diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx new file mode 100644 index 000000000..929fb42a1 --- /dev/null +++ b/src/client/views/InkingControl.tsx @@ -0,0 +1,77 @@ +import { observable, action, computed } from "mobx"; +import { CirclePicker, ColorResult } from 'react-color' +import React = require("react"); +import "./InkingCanvas.scss" +import { InkTool } from "../../fields/InkField"; +import { observer } from "mobx-react"; + +@observer +export class InkingControl extends React.Component { + static Instance: InkingControl = new InkingControl({}); + @observable private _selectedTool: InkTool = InkTool.None; + @observable private _selectedColor: string = "#f44336"; + @observable private _selectedWidth: string = "25"; + + constructor(props: Readonly<{}>) { + super(props); + InkingControl.Instance = this + } + + @action + switchTool = (tool: InkTool): void => { + this._selectedTool = tool; + } + + @action + switchColor = (color: ColorResult): void => { + this._selectedColor = color.hex; + } + + @action + switchWidth = (width: string): void => { + this._selectedWidth = width; + } + + @computed + get selectedTool() { + return this._selectedTool; + } + + @computed + get selectedColor() { + return this._selectedColor; + } + + @computed + get selectedWidth() { + return this._selectedWidth; + } + + selected = (tool: InkTool) => { + if (this._selectedTool === tool) { + return { backgroundColor: "black", color: "white" } + } + return {} + } + + render() { + return ( + <div className="inking-control"> + <div className="ink-tools ink-panel"> + <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button> + <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button> + <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button> + <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button> + </div> + <div className="ink-size ink-panel"> + <label htmlFor="stroke-width">Size</label> + <input type="range" min="1" max="100" defaultValue="25" name="stroke-width" + onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} /> + </div> + <div className="ink-color ink-panel"> + <CirclePicker onChange={this.switchColor} /> + </div> + </div> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx new file mode 100644 index 000000000..d724421d3 --- /dev/null +++ b/src/client/views/InkingStroke.tsx @@ -0,0 +1,66 @@ +import { observer } from "mobx-react"; +import { observable } from "mobx"; +import { InkingControl } from "./InkingControl"; +import { InkTool } from "../../fields/InkField"; +import React = require("react"); + + +interface StrokeProps { + id: string; + line: Array<{ x: number, y: number }>; + color: string; + width: string; + tool: InkTool; + deleteCallback: (index: string) => void; +} + +@observer +export class InkingStroke extends React.Component<StrokeProps> { + + @observable private _strokeTool: InkTool = this.props.tool; + @observable private _strokeColor: string = this.props.color; + @observable private _strokeWidth: string = this.props.width; + + private _canvasColor: string = "#cdcdcd"; + + deleteStroke = (e: React.MouseEvent): void => { + if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) { + this.props.deleteCallback(this.props.id); + } + } + + parseData = (line: Array<{ x: number, y: number }>): string => { + if (line.length === 0) { + return ""; + } + const pathData = "M " + + line.map(p => { + return p.x + " " + p.y; + }).join(" L "); + return pathData; + } + + createStyle() { + switch (this._strokeTool) { + // add more tool styles here + default: + return { + fill: "none", + stroke: this._strokeColor, + strokeWidth: this._strokeWidth + "px", + } + } + } + + + render() { + let pathStyle = this.createStyle(); + let pathData = this.parseData(this.props.line); + + return ( + <path className={(this._strokeTool === InkTool.Highlighter) ? "highlight" : ""} + d={pathData} style={pathStyle} strokeLinejoin="round" strokeLinecap="round" + onMouseOver={this.deleteStroke} onMouseDown={this.deleteStroke} /> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index e73f62904..4334ed299 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -28,4 +28,24 @@ h1 { p { margin: 0px; padding: 0px; -}
\ No newline at end of file +} +::-webkit-scrollbar { + -webkit-appearance: none; + height:5px; + width:5px; +} +::-webkit-scrollbar-thumb { + border-radius: 2px; + background-color: rgba(0,0,0,.5); +} + +.main-buttonDiv { + position: absolute; + width: 150px; + left: 0px; +} +.main-undoButtons { + position: absolute; + width: 150px; + right: 0px; +} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 287fbc025..4a3710e74 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -2,35 +2,27 @@ import { action, configure } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; -import { DocumentDecorations } from './DocumentDecorations'; -import { Documents } from '../documents/Documents'; import { Document } from '../../fields/Document'; -import { KeyStore, KeyStore as KS } from '../../fields/Key'; -import { ListField } from '../../fields/ListField'; -import { NumberField } from '../../fields/NumberField'; -import { TextField } from '../../fields/TextField'; +import { KeyStore } from '../../fields/KeyStore'; import "./Main.scss"; +import { MessageStore } from '../../server/Message'; +import { Utils } from '../../Utils'; +import { Documents } from '../documents/Documents'; +import { Server } from '../Server'; +import { setupDrag } from '../util/DragManager'; +import { Transform } from '../util/Transform'; +import { UndoManager } from '../util/UndoManager'; +import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; +import { DocumentDecorations } from './DocumentDecorations'; import { DocumentView } from './nodes/DocumentView'; -import { ImageField } from '../../fields/ImageField'; -import { Transform } from '../util/Transform'; - +import "./Main.scss"; +import { InkingControl } from './InkingControl'; -configure({ - enforceActions: "observed" -}); -const mainNodeCollection = new Array<Document>(); -let mainContainer = Documents.DockDocument(mainNodeCollection, { - x: 0, y: 0, title: "main container" -}) - -window.addEventListener("drop", function (e) { - e.preventDefault(); -}, false) -window.addEventListener("dragover", function (e) { - e.preventDefault(); -}, false) +configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action +window.addEventListener("drop", (e) => e.preventDefault(), false) +window.addEventListener("dragover", (e) => e.preventDefault(), false) document.addEventListener("pointerdown", action(function (e: PointerEvent) { if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { ContextMenu.Instance.clearItems() @@ -38,6 +30,7 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) { }), true) +<<<<<<< HEAD //runInAction(() => { let doc1 = Documents.TextDocument({ title: "hello", width: 400, height: 300 }); @@ -70,27 +63,81 @@ document.addEventListener("pointerdown", action(function (e: PointerEvent) { let mainNodes = null;// mainContainer.GetFieldT(KeyStore.Data, ListField); if (!mainNodes) { mainNodes = new ListField<Document>(); +======= +const mainDocId = "mainDoc"; +let mainContainer: Document; +let mainfreeform: Document; +Documents.initProtos(mainDocId, (res?: Document) => { + if (res instanceof Document) { + mainContainer = res; + mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); + } + else { + mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); + + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] }; + mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); + mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); + }, 0); +>>>>>>> 96eede5f7d1706a3f7ac6ee02a85bb3da217f467 } - // mainNodes.Data.push(doc6); - // mainNodes.Data.push(doc2); - mainNodes.Data.push(doc4); - mainNodes.Data.push(doc3); - // mainNodes.Data.push(doc5); - // mainNodes.Data.push(doc1); - // mainNodes.Data.push(doc2); - mainNodes.Data.push(doc6); - mainContainer.Set(KeyStore.Data, mainNodes); -} -//} -//); -ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} GetTransform={() => Transform.Identity} - ParentScaling={1} - ContainingCollectionView={undefined} DocumentView={undefined} /> - <DocumentDecorations /> - <ContextMenu /> - </div>), - document.getElementById('root'));
\ No newline at end of file + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" + let weburl = "https://cs.brown.edu/courses/cs166/"; + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) + let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); + let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); + let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + + let addClick = (creator: () => Document) => action(() => + mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) + ); + + let imgRef = React.createRef<HTMLDivElement>(); + let pdfRef = React.createRef<HTMLDivElement>(); + let webRef = React.createRef<HTMLDivElement>(); + let textRef = React.createRef<HTMLDivElement>(); + let schemaRef = React.createRef<HTMLDivElement>(); + let colRef = React.createRef<HTMLDivElement>(); + + ReactDOM.render(( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <DocumentView Document={mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => 0} + PanelHeight={() => 0} + isTopMost={true} + SelectOnLoad={false} + focus={() => { }} + ContainingCollectionView={undefined} /> + <DocumentDecorations /> + <ContextMenu /> + <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > + <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> + <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > + <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> + <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> + <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> + <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> + <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> + <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> + <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> + <div className="main-buttonDiv" style={{ bottom: '125px' }} > + <button onClick={clearDatabase}>Clear Database</button></div> + <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> + <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> + <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> + <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + <InkingControl /> + </div>), + document.getElementById('root')); +}) diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 7c0b512a7..2706c3272 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,3 +1,7 @@ +.collectiondockingview-content { + height: 100%; +} + .collectiondockingview-container { position: relative; top: 0; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 1653994cf..f01c538e6 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,190 +1,94 @@ -import FlexLayout from "flexlayout-react"; import * as GoldenLayout from "golden-layout"; import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, computed, reaction, observable } from "mobx"; +import { action, observable, reaction } from "mobx"; import { observer } from "mobx-react"; +import * as ReactDOM from 'react-dom'; import { Document } from "../../../fields/Document"; -import { KeyStore } from "../../../fields/Key"; -import { ListField } from "../../../fields/ListField"; -import { DragManager } from "../../util/DragManager"; -import { Transform } from "../../util/Transform"; +import { KeyStore } from "../../../fields/KeyStore"; +import Measure from "react-measure"; +import { FieldId, Opt, Field } from "../../../fields/Field"; +import { Utils } from "../../../Utils"; +import { Server } from "../../Server"; +import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; -import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; import React = require("react"); -import * as ReactDOM from 'react-dom'; -import Measure from "react-measure"; +import { SubCollectionViewProps } from "./CollectionViewBase"; @observer -export class CollectionDockingView extends CollectionViewBase { - - private static UseGoldenLayout = true; - public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); } - private _containerRef = React.createRef<HTMLDivElement>(); - @computed - private get modelForFlexLayout() { - const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - var docs = value.map(doc => { - return { type: 'tabset', weight: 50, selected: 0, children: [{ type: "tab", name: doc.Title, component: doc.Id }] }; - }); - return FlexLayout.Model.fromJson({ - global: {}, borders: [], - layout: { - "type": "row", - "weight": 100, - "children": docs +export class CollectionDockingView extends React.Component<SubCollectionViewProps> { + public static Instance: CollectionDockingView; + public static makeDocumentConfig(document: Document) { + return { + type: 'react-component', + component: 'DocumentFrameRenderer', + title: document.Title, + props: { + documentId: document.Id, + //collectionDockingView: CollectionDockingView.Instance } - }); - } - @computed - private get modelForGoldenLayout(): any { - const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - var docs = value.map(doc => { - return { type: 'component', componentName: 'documentViewComponent', componentState: { doc: doc, scaling: 1 } }; - }); - return new GoldenLayout({ - settings: { - selectionEnabled: true - }, content: [{ type: 'row', content: docs }] - }); - } - constructor(props: CollectionViewProps) { - super(props); - } - - componentDidMount: () => void = () => { - if (this._containerRef.current && CollectionDockingView.UseGoldenLayout) { - this.goldenLayoutFactory(); - window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } } - componentWillUnmount: () => void = () => { - window.removeEventListener('resize', this.onResize); - } - private nextId = (function () { var _next_id = 0; return function () { return _next_id++; } })(); - @action - onResize = (event: any) => { - var cur = this.props.ContainingDocumentView!.MainContent.current; + private _goldenLayout: any = null; + private _containerRef = React.createRef<HTMLDivElement>(); + private _fullScreen: any = null; + private _flush: boolean = false; - // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - CollectionDockingView.myLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + constructor(props: SubCollectionViewProps) { + super(props); + CollectionDockingView.Instance = this; + (window as any).React = React; + (window as any).ReactDOM = ReactDOM; + } + public StartOtherDrag(dragDoc: Document, e: any) { + this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: e.button }) } @action - onPointerDown = (e: React.PointerEvent): void => { - if (e.button === 2 && this.active) { - e.stopPropagation(); - e.preventDefault(); - } else { - if (e.buttons === 1 && this.active) { - e.stopPropagation(); - } + public OpenFullScreen(document: Document) { + let newItemStackConfig = { + type: 'stack', + content: [CollectionDockingView.makeDocumentConfig(document)] } + var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); + this._goldenLayout.root.contentItems[0].addChild(docconfig); + docconfig.callDownwards('_$init'); + this._goldenLayout._$maximiseItem(docconfig); + this._fullScreen = docconfig; + this.stateChanged(); } - - flexLayoutFactory = (node: any): any => { - var component = node.getComponent(); - if (component === "button") { - return <button>{node.getName()}</button>; - } - const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - for (var i: number = 0; i < value.length; i++) { - if (value[i].Id === component) { - return (<DocumentView key={value[i].Id} Document={value[i]} - AddDocument={this.addDocument} RemoveDocument={this.removeDocument} - GetTransform={() => Transform.Identity} - ParentScaling={1} - ContainingCollectionView={this} DocumentView={undefined} />); - } - } - if (component === "text") { - return (<div className="panel">Panel {node.getName()}</div>); + @action + public CloseFullScreen() { + if (this._fullScreen) { + this._goldenLayout._$minimiseItem(this._fullScreen); + this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen); + this._fullScreen = null; + this.stateChanged(); } } - public static myLayout: any = null; - - private static _dragDiv: any = null; - private static _dragParent: HTMLElement | null = null; - private static _dragElement: HTMLDivElement; - private static _dragFakeElement: HTMLDivElement; - public static StartOtherDrag(dragElement: HTMLDivElement, dragDoc: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: dragDoc, scaling: 1 } - }; - this._dragElement = dragElement; - this._dragParent = dragElement.parentElement; - // bcz: we want to copy this document into the header, not move it there. - // However, GoldenLayout is setup to move things, so we have to do some kludgy stuff: - - // - create a temporary invisible div and register that as a DragSource with GoldenLayout - this._dragDiv = document.createElement("div"); - this._dragDiv.style.opacity = 0; - DragManager.Root().appendChild(this._dragDiv); - CollectionDockingView.myLayout.createDragSource(this._dragDiv, newItemConfig); - - // - add our document to that div so that GoldenLayout will get the move events its listening for - this._dragDiv.appendChild(this._dragElement); - - // - add a duplicate of our document to the original document's container - // (GoldenLayout will be removing our original one) - this._dragFakeElement = dragElement.cloneNode(true) as HTMLDivElement; - this._dragParent!.appendChild(this._dragFakeElement); - - // all of this must be undone when the document has been dropped (see tabCreated) - } - - _makeFullScreen: boolean = false; - _maximizedStack: any = null; - public static OpenFullScreen(document: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: document } - }; - CollectionDockingView.myLayout._makeFullScreen = true; - CollectionDockingView.myLayout.root.contentItems[0].addChild(newItemConfig); - } - public static CloseFullScreen() { - if (CollectionDockingView.myLayout._maximizedStack != null) { - CollectionDockingView.myLayout._maximizedStack.header.controlsContainer.find('.lm_close').click(); - CollectionDockingView.myLayout._maximizedStack = null; - } - } // // Creates a vertical split on the right side of the docking view, and then adds the Document to that split // - public static AddRightSplit(document: Document) { - var newItemConfig = { - type: 'component', - componentName: 'documentViewComponent', - componentState: { doc: document } - } + @action + public AddRightSplit(document: Document, minimize: boolean = false) { + this._goldenLayout.emit('stateChanged'); let newItemStackConfig = { type: 'stack', - content: [newItemConfig] - }; - var newContentItem = new CollectionDockingView.myLayout._typeToItem[newItemStackConfig.type](CollectionDockingView.myLayout, newItemStackConfig, parent); + content: [CollectionDockingView.makeDocumentConfig(document)] + } - if (CollectionDockingView.myLayout.root.contentItems[0].isRow) { - var rowlayout = CollectionDockingView.myLayout.root.contentItems[0]; - var lastRowItem = rowlayout.contentItems[rowlayout.contentItems.length - 1]; + var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout); - lastRowItem.config["width"] *= 0.5; - newContentItem.config["width"] = lastRowItem.config["width"]; - rowlayout.addChild(newContentItem, rowlayout.contentItems.length, true); - rowlayout.callDownwards('setSize'); + if (this._goldenLayout.root.contentItems[0].isRow) { + this._goldenLayout.root.contentItems[0].addChild(newContentItem); } else { - var collayout = CollectionDockingView.myLayout.root.contentItems[0]; - var newRow = collayout.layoutManager.createContentItem({ type: "row" }, CollectionDockingView.myLayout); + var collayout = this._goldenLayout.root.contentItems[0]; + var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout); collayout.parent.replaceChild(collayout, newRow); newRow.addChild(newContentItem, undefined, true); @@ -192,131 +96,185 @@ export class CollectionDockingView extends CollectionViewBase { collayout.config["width"] = 50; newContentItem.config["width"] = 50; - collayout.parent.callDownwards('setSize'); } + if (minimize) { + newContentItem.config["width"] = 10; + newContentItem.config["height"] = 10; + } + newContentItem.callDownwards('_$init'); + this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); + this._goldenLayout.emit('stateChanged'); + this.stateChanged(); + return newContentItem; } - goldenLayoutFactory() { - CollectionDockingView.myLayout = this.modelForGoldenLayout; - var layout = CollectionDockingView.myLayout; - CollectionDockingView.myLayout.on('tabCreated', function (tab: any) { - if (CollectionDockingView._dragDiv) { - CollectionDockingView._dragDiv.removeChild(CollectionDockingView._dragElement); - CollectionDockingView._dragParent!.removeChild(CollectionDockingView._dragFakeElement); - CollectionDockingView._dragParent!.appendChild(CollectionDockingView._dragElement); - DragManager.Root().removeChild(CollectionDockingView._dragDiv); - CollectionDockingView._dragDiv = null; + setupGoldenLayout() { + var config = this.props.Document.GetText(KeyStore.Data, ""); + if (config) { + if (!this._goldenLayout) { + this._goldenLayout = new GoldenLayout(JSON.parse(config)); } - tab.setTitle(tab.contentItem.config.componentState.doc.Title); - tab.closeElement.off('click') //unbind the current click handler - .click(function () { - tab.contentItem.remove(); - }); - }); - - CollectionDockingView.myLayout.on('stackCreated', function (stack: any) { - if (CollectionDockingView.myLayout._makeFullScreen) { - CollectionDockingView.myLayout._maximizedStack = stack; - CollectionDockingView.myLayout._maxstack = stack.header.controlsContainer.find('.lm_maximise'); + else { + if (config == JSON.stringify(this._goldenLayout.toConfig())) + return; + try { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + } catch (e) { } + this._goldenLayout.destroy(); + this._goldenLayout = new GoldenLayout(JSON.parse(config)); } - //stack.header.controlsContainer.find('.lm_popout').hide(); - stack.header.controlsContainer.find('.lm_close') //get the close icon - .off('click') //unbind the current click handler - .click(function () { - //if (confirm('really close this?')) { - stack.remove(); - //} - }); - }); - - var me = this; - CollectionDockingView.myLayout.registerComponent('documentViewComponent', function (container: any, state: any) { - // bcz: this is crufty - // calling html() causes a div tag to be added in the DOM with id 'containingDiv'. - // Apparently, we need to wait to allow a live html div element to actually be instantiated. - // After a timeout, we lookup the live html div element and add our React DocumentView to it. - var containingDiv = "component_" + me.nextId(); - container.getElement().html("<div id='" + containingDiv + "'></div>"); - setTimeout(function () { - let divContainer = document.getElementById(containingDiv); - if (divContainer) { - let props: DockingProps = { - ContainingDiv: containingDiv, - Document: state.doc, - Container: container, - CollectionDockingView: me, - HtmlElement: divContainer - } - ReactDOM.render((<RenderClass {...props} />), divContainer); - if (CollectionDockingView.myLayout._maxstack) { - CollectionDockingView.myLayout._maxstack.click(); - } + this._goldenLayout.on('itemDropped', this.itemDropped); + this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('stackCreated', this.stackCreated); + this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); + this._goldenLayout.container = this._containerRef.current; + if (this._goldenLayout.config.maximisedItemId === '__glMaximised') { + try { + this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise(); + } catch (e) { + this._goldenLayout.config.maximisedItemId = null; } - }, 0); - }); - CollectionDockingView.myLayout.container = this._containerRef.current; - CollectionDockingView.myLayout.init(); + } + this._goldenLayout.init(); + } } + componentDidMount: () => void = () => { + if (this._containerRef.current) { + reaction( + () => this.props.Document.GetText(KeyStore.Data, ""), + () => this.setupGoldenLayout(), { fireImmediately: true }); + window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + } + } + componentWillUnmount: () => void = () => { + this._goldenLayout.unbind('itemDropped', this.itemDropped); + this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('stackCreated', this.stackCreated); + this._goldenLayout.destroy(); + this._goldenLayout = null; + window.removeEventListener('resize', this.onResize); + } + @action + onResize = (event: any) => { + var cur = this._containerRef.current; - render() { - const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; - const value: Document[] = Document.GetData(fieldKey, ListField, []); - // bcz: not sure why, but I need these to force the flexlayout to update when the collection size changes. - var s = this.props.ContainingDocumentView != undefined ? this.props.ContainingDocumentView!.ScalingToScreenSpace : 1; - var w = Document.GetNumber(KeyStore.Width, 0) / s; - var h = Document.GetNumber(KeyStore.Height, 0) / s; + // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed + this._goldenLayout.updateSize(cur!.getBoundingClientRect().width, cur!.getBoundingClientRect().height); + } - var chooseLayout = () => { - if (!CollectionDockingView.UseGoldenLayout) - return <FlexLayout.Layout model={this.modelForFlexLayout} factory={this.flexLayoutFactory} />; + @action + onPointerUp = (e: React.PointerEvent): void => { + if (this._flush) { + this._flush = false; + setTimeout(() => this.stateChanged(), 10); } + } + @action + onPointerDown = (e: React.PointerEvent): void => { + if (e.button === 2 && this.props.active()) { + e.stopPropagation(); + e.preventDefault(); + } else { + var className = (e.target as any).className; + if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") { + this._flush = true; + } + if (e.buttons === 1 && this.props.active()) { + e.stopPropagation(); + } + } + } + + @undoBatch + stateChanged = () => { + var json = JSON.stringify(this._goldenLayout.toConfig()); + this.props.Document.SetText(KeyStore.Data, json) + } + + itemDropped = () => { + this.stateChanged(); + } + tabCreated = (tab: any) => { + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + tab.contentItem.remove(); + }); + } + + stackCreated = (stack: any) => { + //stack.header.controlsContainer.find('.lm_popout').hide(); + stack.header.controlsContainer.find('.lm_close') //get the close icon + .off('click') //unbind the current click handler + .click(action(function () { + //if (confirm('really close this?')) { + stack.remove(); + //} + })); + } + render() { return ( <div className="collectiondockingview-container" id="menuContainer" - onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} ref={this._containerRef} + onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} style={{ - width: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : w - 2 * COLLECTION_BORDER_WIDTH, - height: CollectionDockingView.UseGoldenLayout || s > 1 ? "100%" : h - 2 * COLLECTION_BORDER_WIDTH, + width: "100%", + height: "100%", borderStyle: "solid", borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }} > - {chooseLayout()} - </div> + }} /> ); } } -interface DockingProps { - ContainingDiv: string, - Document: Document, - Container: any, - HtmlElement: HTMLElement, - CollectionDockingView: CollectionDockingView +interface DockedFrameProps { + documentId: FieldId, + //collectionDockingView: CollectionDockingView } @observer -export class RenderClass extends React.Component<DockingProps> { - @observable - private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView +export class DockedFrameRenderer extends React.Component<DockedFrameProps> { + + private _mainCont = React.createRef<HTMLDivElement>(); + @observable private _panelWidth = 0; + @observable private _panelHeight = 0; + @observable private _document: Opt<Document>; + + constructor(props: any) { + super(props); + Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document)); + } + + private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); } + private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); } + private _contentScaling = () => { return this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); } + + ScreenToLocalTransform = () => { + let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); + return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()) + } render() { - let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var layout = this.props.Document.GetText(KeyStore.Layout, ""); + if (!this._document) + return (null); var content = - <DocumentView key={this.props.Document.Id} Document={this.props.Document} - AddDocument={this.props.CollectionDockingView.addDocument} - RemoveDocument={this.props.CollectionDockingView.removeDocument} - GetTransform={() => Transform.Identity} - ParentScaling={this._parentScaling} - ContainingCollectionView={this.props.CollectionDockingView} DocumentView={undefined} /> + <div className="collectionDockingView-content" ref={this._mainCont}> + <DocumentView key={this._document.Id} Document={this._document} + AddDocument={undefined} + RemoveDocument={undefined} + ContentScaling={this._contentScaling} + PanelWidth={this._nativeWidth} + PanelHeight={this._nativeHeight} + ScreenToLocalTransform={this.ScreenToLocalTransform} + isTopMost={true} + SelectOnLoad={false} + focus={(doc: Document) => { }} + ContainingCollectionView={undefined} /> + </div> - if (nativeWidth > 0 && (layout.indexOf("CollectionFreeForm") == -1 || layout.indexOf("AnnotationsKey") != -1)) { - return <Measure onResize={ - action((r: any) => this._parentScaling = nativeWidth > 0 ? r.entry.width / nativeWidth : 1)} - > - {({ measureRef }) => <div ref={measureRef}> {content} </div>} - </Measure> - } - return <div> {content} </div> + return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> + {({ measureRef }) => <div ref={measureRef}> {content} </div>} + </Measure> } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index 4cf474f77..b059163ed 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,4 +1,11 @@ .collectionfreeformview-container { + + .collectionfreeformview > .jsx-parser{ + position:absolute; + height: 100%; + width: 100%; + } + border-style: solid; box-sizing: border-box; position: relative; @@ -11,5 +18,51 @@ position: absolute; top: 0; left: 0; + width:100%; + height: 100%; + } +} +.collectionfreeformview-overlay { + + .collectionfreeformview > .jsx-parser{ + position:absolute; + height: 100%; + } + .formattedTextBox-cont { + background:yellow; } + + border-style: solid; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + .collectionfreeformview { + position: absolute; + top: 0; + left: 0; + width:100%; + height: 100%; + } +} + +.border { + border-style: solid; + box-sizing: border-box; + width: 100%; + height: 100%; +} + +//this is an animation for the blinking cursor! +@keyframes blink { + 0% {opacity: 0} + 49%{opacity: 0} + 50% {opacity: 1} +} + +#prevCursor { + animation: blink 1s infinite; }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 04373df12..16002ad9f 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -1,90 +1,85 @@ +import { action, computed, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; -import React = require("react"); -import { action, observable, computed } from "mobx"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; -import { DragManager } from "../../util/DragManager"; -import "./CollectionFreeFormView.scss"; -import { Utils } from "../../../Utils"; -import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Key, KeyStore } from "../../../fields/Key"; import { Document } from "../../../fields/Document"; +import { FieldWaiting } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { NumberField } from "../../../fields/NumberField"; +import { TextField } from "../../../fields/TextField"; import { Documents } from "../../documents/Documents"; -import { FieldWaiting } from "../../../fields/Field"; +import { DragManager } from "../../util/DragManager"; import { Transform } from "../../util/Transform"; +import { undoBatch } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionSchemaView } from "../collections/CollectionSchemaView"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { InkingCanvas } from "../InkingCanvas"; +import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { DocumentView } from "../nodes/DocumentView"; +import { FormattedTextBox } from "../nodes/FormattedTextBox"; +import { ImageBox } from "../nodes/ImageBox"; +import { KeyValueBox } from "../nodes/KeyValueBox"; +import { PDFBox } from "../nodes/PDFBox"; +import { WebBox } from "../nodes/WebBox"; +import "./CollectionFreeFormView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionViewBase } from "./CollectionViewBase"; +import React = require("react"); +import { render } from "pug"; +const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? @observer export class CollectionFreeFormView extends CollectionViewBase { - public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionFreeFormView", fieldKey); } - private _containerRef = React.createRef<HTMLDivElement>(); private _canvasRef = React.createRef<HTMLDivElement>(); private _lastX: number = 0; private _lastY: number = 0; + private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) + + @observable private _downX: number = 0; + @observable private _downY: number = 0; - constructor(props: CollectionViewProps) { - super(props); - } + //determines whether the blinking cursor for indicating whether a text will be made on key down is visible + @observable + private _previewCursorVisible: boolean = false; - @computed - get isAnnotationOverlay() { return this.props.CollectionFieldKey == KeyStore.Annotations; } - - @computed - get nativeWidth() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeWidth, 0); } - @computed - get nativeHeight() { return this.props.DocumentForCollection.GetNumber(KeyStore.NativeHeight, 0); } - - @computed - get zoomScaling() { return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1); } - - @computed - get resizeScaling() { return this.isAnnotationOverlay ? this.props.DocumentForCollection.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; } + @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) } + @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) } + @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? + @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); } + @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections + @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections + @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - const doc = de.data["document"]; - var me = this; - if (doc instanceof CollectionFreeFormDocumentView) { - if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) { - doc.props.ContainingCollectionView.removeDocument(doc.props.Document); - this.addDocument(doc.props.Document); - } - const xOffset = de.data["xOffset"] as number || 0; - const yOffset = de.data["yOffset"] as number || 0; - const { translateX, translateY } = Utils.GetScreenTransform(this._canvasRef.current!); - const currScale = this.resizeScaling * this.zoomScaling * this.props.ContainingDocumentView!.ScalingToScreenSpace; - const screenX = de.x - xOffset; - const screenY = de.y - yOffset; - doc.props.Document.SetNumber(KeyStore.X, (screenX - translateX) / currScale); - doc.props.Document.SetNumber(KeyStore.Y, (screenY - translateY) / currScale); - this.bringToFront(doc); - } - e.stopPropagation(); - } - - componentDidMount() { - if (this._containerRef.current) { - DragManager.MakeDropTarget(this._containerRef.current, { - handlers: { - drop: this.drop - } - }); - } + super.drop(e, de); + const docView: DocumentView = de.data["documentView"]; + let doc: Document = docView ? docView.props.Document : de.data["document"]; + let screenX = de.x - (de.data["xOffset"] as number || 0); + let screenY = de.y - (de.data["yOffset"] as number || 0); + const [x, y] = this.getTransform().transformPoint(screenX, screenY); + doc.SetNumber(KeyStore.X, x); + doc.SetNumber(KeyStore.Y, y); + this.bringToFront(doc); } @action onPointerDown = (e: React.PointerEvent): void => { - if ((e.button === 2 && this.active) || - !e.defaultPrevented) { + if (((e.button === 2 && this.props.active()) || + !e.defaultPrevented) && (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) { document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; + this._lastX = e.pageX; + this._lastY = e.pageY; + this._downX = e.pageX; + this._downY = e.pageY; } } @@ -94,22 +89,26 @@ export class CollectionFreeFormView extends CollectionViewBase { document.removeEventListener("pointerup", this.onPointerUp); e.stopPropagation(); if (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) { - if (!SelectionManager.IsSelected(this.props.ContainingDocumentView as CollectionFreeFormDocumentView)) { - SelectionManager.SelectDoc(this.props.ContainingDocumentView as CollectionFreeFormDocumentView, false); + //show preview text cursor on tap + this._previewCursorVisible = true; + //select is not already selected + if (!this.props.isSelected()) { + this.props.select(false); } } + } @action onPointerMove = (e: PointerEvent): void => { - if (!e.cancelBubble && this.active) { - e.preventDefault(); + if (!e.cancelBubble && this.props.active()) { e.stopPropagation(); - let currScale: number = this.props.ContainingDocumentView!.ScalingToScreenSpace; - let x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0); - let y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0); - - this.SetPan(x + (e.pageX - this._lastX) / currScale, y + (e.pageY - this._lastY) / currScale); + e.preventDefault(); + let x = this.props.Document.GetNumber(KeyStore.PanX, 0); + let y = this.props.Document.GetNumber(KeyStore.PanY, 0); + let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + this._previewCursorVisible = false; + this.SetPan(x - dx, y - dy); } this._lastX = e.pageX; this._lastY = e.pageY; @@ -119,134 +118,205 @@ export class CollectionFreeFormView extends CollectionViewBase { onPointerWheel = (e: React.WheelEvent): void => { e.stopPropagation(); e.preventDefault(); - let modes = ['pixels', 'lines', 'page']; let coefficient = 1000; - // if (modes[e.deltaMode] == 'pixels') coefficient = 50; - // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? - let { LocalX, LocalY, ContainerX, ContainerY } = this.props.ContainingDocumentView!.TransformToLocalPoint(e.pageX, e.pageY); - var Xx = this.props.ContainingDocumentView!.LeftCorner(); - var Yy = this.props.ContainingDocumentView!.TopCorner(); - var deltaScale = (1 - (e.deltaY / coefficient)) * this.props.ContainingDocumentView!.props.Document.GetNumber(KeyStore.Scale, 1); - var newDeltaScale = this.isAnnotationOverlay ? Math.max(1, deltaScale) : deltaScale; + if (e.ctrlKey) { + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); + const coefficient = 1000; + let deltaScale = (1 - (e.deltaY / coefficient)); + this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale); + this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale); + e.stopPropagation(); + e.preventDefault(); + } else { + // if (modes[e.deltaMode] == 'pixels') coefficient = 50; + // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height?? + let transform = this.getTransform(); + + let deltaScale = (1 - (e.deltaY / coefficient)); + if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay) + deltaScale = 1 / this.zoomScaling; + let [x, y] = transform.transformPoint(e.clientX, e.clientY); - this.props.DocumentForCollection.SetNumber(KeyStore.Scale, newDeltaScale); - this.SetPan(ContainerX - (LocalX * newDeltaScale + Xx), ContainerY - (LocalY * newDeltaScale + Yy)); + let localTransform = this.getLocalTransform() + localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y) + // console.log(localTransform) + + this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); + this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); + } } @action private SetPan(panX: number, panY: number) { - const newPanX = Math.max((1 - this.zoomScaling) * this.nativeWidth, Math.min(0, panX)); - const newPanY = Math.max((1 - this.zoomScaling) * this.nativeHeight, Math.min(0, panY)); - this.props.DocumentForCollection.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); - this.props.DocumentForCollection.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); + var x1 = this.getLocalTransform().inverse().Scale; + var x2 = this.getTransform().inverse().Scale; + const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX)); + const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY)); + this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX); + this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY); } @action onDrop = (e: React.DragEvent): void => { - e.stopPropagation() - e.preventDefault() - let fReader = new FileReader() - let file = e.dataTransfer.items[0].getAsFile(); - let that = this; - const panx: number = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0); - const pany: number = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0); - let x = e.pageX - panx - let y = e.pageY - pany - - fReader.addEventListener("load", action("drop", (event) => { - if (fReader.result) { - let url = "" + fReader.result; - let doc = Documents.ImageDocument(url, { - x: x, y: y - }) - let docs = that.props.DocumentForCollection.GetT(KeyStore.Data, ListField); - if (docs != FieldWaiting) { - if (!docs) { - docs = new ListField<Document>(); - that.props.DocumentForCollection.Set(KeyStore.Data, docs) - } - docs.Data.push(doc); - } - } - }), false) + var pt = this.getTransform().transformPoint(e.pageX, e.pageY); + super.onDrop(e, { x: pt[0], y: pt[1] }); + } - if (file) { - fReader.readAsDataURL(file) - } + onDragOver = (): void => { } - onDragOver = (e: React.DragEvent): void => { + @action + onKeyDown = (e: React.KeyboardEvent<Element>) => { + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey) { + if (this._previewCursorVisible) { + //make textbox and add it to this collection + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); + // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + this._selectOnLoaded = newBox.Id; + //set text to be the typed key and get focus on text box + this.props.CollectionView.addDocument(newBox); + //remove cursor from screen + this._previewCursorVisible = false; + } + } } @action - bringToFront(doc: CollectionFreeFormDocumentView) { - const { CollectionFieldKey: fieldKey, DocumentForCollection: Document } = this.props; - - const value: Document[] = Document.GetList<Document>(fieldKey, []); - var topmost = value.reduce((topmost, d) => Math.max(d.GetNumber(KeyStore.ZIndex, 0), topmost), -1000); - value.map(d => { - var zind = d.GetNumber(KeyStore.ZIndex, 0); - if (zind != topmost - 1 - (topmost - zind) && d != doc.props.Document) { - d.SetData(KeyStore.ZIndex, topmost - 1 - (topmost - zind), NumberField); + bringToFront(doc: Document) { + const { fieldKey: fieldKey, Document: Document } = this.props; + + const value: Document[] = Document.GetList<Document>(fieldKey, []).slice(); + value.sort((doc1, doc2) => { + if (doc1 === doc) { + return 1; + } + if (doc2 === doc) { + return -1; } - }) + return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0); + }).map((doc, index) => { + doc.SetNumber(KeyStore.ZIndex, index + 1) + }); + } - if (doc.props.Document.GetNumber(KeyStore.ZIndex, 0) != 0) { - doc.props.Document.SetData(KeyStore.ZIndex, 0, NumberField); + @computed get backgroundLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); + if (field && field !== "<Waiting>") { + return field.Data; } } + @computed get overlayLayout(): string | undefined { + let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); + if (field && field !== "<Waiting>") { + return field.Data; + } + } + + focusDocument = (doc: Document) => { + let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2; + let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2; + this.SetPan(x, y); + this.props.focus(this.props.Document); + } + @computed - get translate(): [number, number] { - const x = this.props.DocumentForCollection.GetNumber(KeyStore.PanX, 0); - const y = this.props.DocumentForCollection.GetNumber(KeyStore.PanY, 0); - return [x, y]; + get views() { + var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1); + const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField); + if (lvalue && lvalue != FieldWaiting) { + return lvalue.Data.map(doc => { + var page = doc.GetNumber(KeyStore.Page, 0); + return (page != curPage && page != 0) ? (null) : + (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} + AddDocument={this.props.addDocument} + RemoveDocument={this.props.removeDocument} + ScreenToLocalTransform={this.getTransform} + isTopMost={false} + SelectOnLoad={doc.Id === this._selectOnLoaded} + ContentScaling={this.noScaling} + PanelWidth={doc.Width} + PanelHeight={doc.Height} + ContainingCollectionView={this.props.CollectionView} + focus={this.focusDocument} + />); + }) + } + return null; } @computed - get scale(): number { - return this.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1); + get backgroundView() { + return !this.backgroundLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + bindings={this.props.bindings} + jsx={this.backgroundLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); + } + @computed + get overlayView() { + return !this.overlayLayout ? (null) : + (<JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + bindings={this.props.bindings} + jsx={this.overlayLayout} + showWarnings={true} + onError={(test: any) => console.log(test)} + />); } - getTransform = (): Transform => { - const [x, y] = this.translate; - return this.props.GetTransform().scaled(this.scale).translate(x, y); + getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()) + getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY); + noScaling = () => 1; + + //when focus is lost, this will remove the preview cursor + @action + onBlur = (e: React.FocusEvent<HTMLDivElement>): void => { + this._previewCursorVisible = false; } render() { - const Document: Document = this.props.DocumentForCollection; - const value: Document[] = Document.GetList<Document>(this.props.CollectionFieldKey, []); - const panx: number = Document.GetNumber(KeyStore.PanX, 0); - const pany: number = Document.GetNumber(KeyStore.PanY, 0); - var me = this; + //determines whether preview text cursor should be visible (ie when user taps this collection it should) + let cursor = null; + if (this._previewCursorVisible) { + //get local position and place cursor there! + let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); + cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div> + } + + let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; + + const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); + const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); return ( - <div className="collectionfreeformview-container" + <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} onPointerDown={this.onPointerDown} + onKeyPress={this.onKeyDown} onWheel={this.onPointerWheel} - onContextMenu={(e) => e.preventDefault()} - onDrop={this.onDrop} + onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} - style={{ - borderWidth: `${COLLECTION_BORDER_WIDTH}px`, - }} - ref={this._containerRef}> + onBlur={this.onBlur} + style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} + tabIndex={0} + ref={this.createDropTarget}> <div className="collectionfreeformview" - style={{ width: "100%", transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }} + style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }} ref={this._canvasRef}> - - {this.props.BackgroundView} - {value.map(doc => { - return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc} - AddDocument={this.addDocument} - RemoveDocument={this.removeDocument} - GetTransform={this.getTransform} - ParentScaling={1} - ContainingCollectionView={this} DocumentView={undefined} />); - })} + {this.backgroundView} + <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} /> + {cursor} + {this.views} </div> + {this.overlayView} </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx new file mode 100644 index 000000000..7fd9f0f11 --- /dev/null +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -0,0 +1,55 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ContextMenu } from "../ContextMenu"; +import { CollectionView, CollectionViewType } from "./CollectionView"; +import { CollectionViewProps } from "./CollectionViewBase"; +import React = require("react"); + + +@observer +export class CollectionPDFView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<${CollectionPDFView.name} Document={Document} + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; + } + + @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0; + @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0; + + @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); } + @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); } + @computed private get uIButtons() { + return ( + <div className="pdfBox-buttonTray" key="tray"> + <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button> + <button className="pdfButton" onClick={this.onPageForward}>{">"}</button> + </div>); + } + + // "inherited" CollectionView API starts here... + + public active: () => boolean = () => CollectionView.Active(this); + + addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } + removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } + + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { } }); + } + } + + get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; } + get subView(): any { return CollectionView.SubView(this); } + + render() { + return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> + {this.subView} + {this.props.isSelected() ? this.uIButtons : (null)} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index 633e3ca1b..d40e6d314 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,58 +1,98 @@ + .collectionSchemaView-container { border-style: solid; box-sizing: border-box; position: absolute; width: 100%; height: 100%; - .collectionfreeformview-container { - border-width: 0px; - .collectionfreeformview > .jsx-parser{ - position:absolute - } + .collectionSchemaView-previewRegion { + position: relative; + background: black; + float: left; + height: 100%; + } + .collectionSchemaView-previewHandle { + position: absolute; + height: 37px; + width: 20px; + z-index: 20; + right: 0; + top: 0; + background: Black ; + } + .collectionSchemaView-dividerDragger{ + position: relative; + background: black; + float: left; + height: 100%; } - .imageBox-cont { - position:relative; - max-height:100%; + .collectionSchemaView-tableContainer { + position: relative; + float: left; + height: 100%; } + .ReactTable { position: absolute; - display: inline-block; + // display: inline-block; + // overflow: auto; width: 100%; - overflow: auto; height: 100%; background: white; box-sizing: border-box; + .rt-table { + overflow-y: auto; + overflow-x: auto; + height: 100%; + + display: -webkit-inline-box; + direction: ltr; + // direction:rtl; + // display:block; + } + .rt-tbody { + //direction: ltr; + direction: rtl; + } + .rt-tr-group { + direction: ltr; + max-height: 44px; + } + .rt-td { + border-width: 1; + border-right-color: #aaa; + .imageBox-cont { + position:relative; + max-height:100%; + } + .imageBox-cont img { + object-fit: contain; + max-width: 100%; + height: 100% + } + } + .rt-tr-group { + border-width: 1; + border-bottom-color: #aaa + } } .ReactTable .rt-thead.-header { background:grey; } .ReactTable .rt-th, .ReactTable .rt-td { - max-height: 75px; + max-height: 44; + padding: 3px 7px; } .ReactTable .rt-tbody .rt-tr-group:last-child { border-bottom: grey; border-bottom-style: solid; border-bottom-width: 1; } - .ReactTable .rt-td { - border-width: 1; - border-right-color: #aaa - } - .ReactTable .rt-tr-group { - border-width: 1; - border-bottom-color: #aaa - } - .imageBox-cont img { - object-fit: contain; - height: 100% - } .documentView-node:first-child { background: grey; .imageBox-cont img { object-fit: contain; - max-width: 100%; - height: 100% } } } diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 76706f520..49f95c014 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -1,63 +1,83 @@ import React = require("react") -import ReactTable, { ReactTableDefaults, CellInfo, ComponentPropsGetterRC, ComponentPropsGetterR } from "react-table"; +import { action, observable } from "mobx"; import { observer } from "mobx-react"; -import { FieldView, FieldViewProps } from "../nodes/FieldView"; -import "react-table/react-table.css" -import { observable, action, computed } from "mobx"; -import SplitPane from "react-split-pane" -import "./CollectionSchemaView.scss" -import { ScrollBox } from "../../util/ScrollBox"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase"; -import { DocumentView } from "../nodes/DocumentView"; -import { EditableView } from "../EditableView"; -import { CompileScript, ToField } from "../../util/Scripting"; -import { KeyStore as KS, Key, KeyStore } from "../../../fields/Key"; +import Measure from "react-measure"; +import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import "react-table/react-table.css"; import { Document } from "../../../fields/Document"; import { Field } from "../../../fields/Field"; +import { KeyStore } from "../../../fields/KeyStore"; +import { CompileScript, ToField } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; -import Measure from "react-measure"; +import { ContextMenu } from "../ContextMenu"; +import { EditableView } from "../EditableView"; +import { DocumentView } from "../nodes/DocumentView"; +import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import "./CollectionSchemaView.scss"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { setupDrag } from "../../util/DragManager"; + +// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 + @observer export class CollectionSchemaView extends CollectionViewBase { - public static LayoutString() { return CollectionViewBase.LayoutString("CollectionSchemaView"); } + private _mainCont = React.createRef<HTMLDivElement>(); + private DIVIDER_WIDTH = 5; - @observable - selectedIndex = 0; + @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView + @observable _dividerX = 0; + @observable _panelWidth = 0; + @observable _panelHeight = 0; + @observable _selectedIndex = 0; + @observable _splitPercentage: number = 50; renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { doc: rowProps.value[0], fieldKey: rowProps.value[1], - DocumentViewForField: undefined, + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, } let contents = ( <FieldView {...props} /> ) + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => props.doc); return ( - <EditableView contents={contents} height={36} GetValue={() => { - let field = props.doc.Get(props.fieldKey); - if (field && field instanceof Field) { - return field.ToScriptString(); - } - return field || ""; - }} SetValue={(value: string) => { - let script = CompileScript(value); - if (!script.compiled) { - return false; - } - let field = script(); - if (field instanceof Field) { - props.doc.Set(props.fieldKey, field); - return true; - } else { - let dataField = ToField(field); - if (dataField) { - props.doc.Set(props.fieldKey, dataField); - return true; - } - } - return false; - }}></EditableView> + <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> + <EditableView contents={contents} + height={36} GetValue={() => { + let field = props.doc.Get(props.fieldKey); + if (field && field instanceof Field) { + return field.ToScriptString(); + } + return field || ""; + }} + SetValue={(value: string) => { + let script = CompileScript(value, undefined, true); + if (!script.compiled) { + return false; + } + let field = script(); + if (field instanceof Field) { + props.doc.Set(props.fieldKey, field); + return true; + } else { + let dataField = ToField(field); + if (dataField) { + props.doc.Set(props.fieldKey, dataField); + return true; + } + } + return false; + }}> + </EditableView> + </div> ) } @@ -68,96 +88,157 @@ export class CollectionSchemaView extends CollectionViewBase { } return { onClick: action((e: React.MouseEvent, handleOriginal: Function) => { - that.selectedIndex = rowInfo.index; - const doc: Document = rowInfo.original; - console.log("Row clicked: ", doc.Title) + that._selectedIndex = rowInfo.index; + this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize if (handleOriginal) { handleOriginal() } }), style: { - background: rowInfo.index == this.selectedIndex ? "#00afec" : "white", - color: rowInfo.index == this.selectedIndex ? "white" : "black" + background: rowInfo.index == this._selectedIndex ? "lightGray" : "white", + //color: rowInfo.index == this._selectedIndex ? "white" : "black" } }; } - onPointerDown = (e: React.PointerEvent) => { - let target = e.target as HTMLElement; - if (target.tagName == "SPAN" && target.className.includes("Resizer")) { - e.stopPropagation(); + _startSplitPercent = 0; + @action + onDividerMove = (e: PointerEvent): void => { + let nativeWidth = this._mainCont.current!.getBoundingClientRect(); + this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100); + } + @action + onDividerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onDividerMove); + document.removeEventListener('pointerup', this.onDividerUp); + if (this._startSplitPercent == this._splitPercentage) { + this._splitPercentage = this._splitPercentage == 1 ? 66 : 100; } + } + onDividerDown = (e: React.PointerEvent) => { + this._startSplitPercent = this._splitPercentage; + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onDividerMove); + document.addEventListener('pointerup', this.onDividerUp); + } + @action + onExpanderMove = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + } + @action + onExpanderUp = (e: PointerEvent): void => { + e.stopPropagation(); + e.preventDefault(); + document.removeEventListener("pointermove", this.onExpanderMove); + document.removeEventListener('pointerup', this.onExpanderUp); + if (this._startSplitPercent == this._splitPercentage) { + this._splitPercentage = this._splitPercentage == 100 ? 66 : 100; + } + } + onExpanderDown = (e: React.PointerEvent) => { + this._startSplitPercent = this._splitPercentage; + e.stopPropagation(); + e.preventDefault(); + document.addEventListener("pointermove", this.onExpanderMove); + document.addEventListener('pointerup', this.onExpanderUp); + } + + onPointerDown = (e: React.PointerEvent) => { // if (e.button === 2 && this.active) { // e.stopPropagation(); // e.preventDefault(); // } else { - if (e.buttons === 1 && this.active) { - e.stopPropagation(); + if (e.buttons === 1) { + if (this.props.isSelected()) { + e.stopPropagation(); + } } } } + @action + setScaling = (r: any) => { + const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); + const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; + this._panelWidth = r.entry.width; + this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight; + this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width); + } + + getContentScaling = (): number => this._contentScaling; + getPanelWidth = (): number => this._panelWidth; + getPanelHeight = (): number => this._panelHeight; + getTransform = (): Transform => { + return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); + } + + focusDocument = (doc: Document) => { } - @observable - private _parentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ParentScaling prop of the DocumentView render() { - const { DocumentForCollection: Document, CollectionFieldKey: fieldKey } = this.props; - const children = Document.GetList<Document>(fieldKey, []); - const columns = Document.GetList(KS.ColumnsKey, - [KS.Title, KS.Data, KS.Author]) - let content; - var me = this; - if (this.selectedIndex != -1) { - content = ( - <Measure onResize={action((r: any) => { - var doc = children[this.selectedIndex]; - var n = doc.GetNumber(KeyStore.NativeWidth, 0); - if (n > 0 && r.entry.width > 0) { - this._parentScaling = r.entry.width / n; - } - })}> - {({ measureRef }) => - <div ref={measureRef}> - <DocumentView Document={children[this.selectedIndex]} - AddDocument={this.addDocument} RemoveDocument={this.removeDocument} - GetTransform={() => Transform.Identity}//TODO This should probably be an actual transform - ParentScaling={this._parentScaling} - DocumentView={undefined} ContainingCollectionView={me} /> - </div> - } - </Measure> - ) - } else { - content = <div /> - } + const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author]) + const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); + const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; + let content = this._selectedIndex == -1 || !selected ? (null) : ( + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="collectionSchemaView-content" ref={measureRef}> + <DocumentView Document={selected} + AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument} + isTopMost={false} + SelectOnLoad={false} + ScreenToLocalTransform={this.getTransform} + ContentScaling={this.getContentScaling} + PanelWidth={this.getPanelWidth} + PanelHeight={this.getPanelHeight} + ContainingCollectionView={this.props.CollectionView} + focus={this.focusDocument} + /> + </div> + } + </Measure> + ) + let previewHandle = !this.props.active() ? (null) : ( + <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />); return ( - <div onPointerDown={this.onPointerDown} className="collectionSchemaView-container" - style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }} > - <SplitPane split={"vertical"} defaultSize="60%" style={{ height: "100%", position: "relative", overflow: "none" }}> - <ReactTable - data={children} - pageSize={children.length} - page={0} - showPagination={false} - columns={columns.map(col => { - return ( - { - Header: col.Name, - accessor: (doc: Document) => [doc, col], - id: col.Id - }) - })} - column={{ - ...ReactTableDefaults.column, - Cell: this.renderCell - }} - getTrProps={this.getTrProps} - /> - {content} - </SplitPane> - </div> + <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} > + <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <Measure onResize={action((r: any) => { + this._dividerX = r.entry.width; + this._panelHeight = r.entry.height; + })}> + {({ measureRef }) => + <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}> + <ReactTable + data={children} + pageSize={children.length} + page={0} + showPagination={false} + columns={columns.map(col => ({ + Header: col.Name, + accessor: (doc: Document) => [doc, col], + id: col.Id + }))} + column={{ + ...ReactTableDefaults.column, + Cell: this.renderCell, + + }} + getTrProps={this.getTrProps} + /> + </div> + } + </Measure> + <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} /> + <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}> + {content} + </div> + {previewHandle} + </div> + </div > ) } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss new file mode 100644 index 000000000..f8d580a7b --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.scss @@ -0,0 +1,37 @@ +#body { + padding: 20px; + background: #bbbbbb; +} + +ul { + list-style: none; +} + +li { + margin: 5px 0; +} + +.no-indent { + padding-left: 0; +} + +.bullet { + width: 1.5em; + display: inline-block; +} + +.collectionTreeView-dropTarget { + border-style: solid; + box-sizing: border-box; + height: 100%; +} + +.docContainer { + display: inline-table; +} + +.delete-button { + color: #999999; + float: right; + margin-left: 1em; +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx new file mode 100644 index 000000000..8b06d9ac4 --- /dev/null +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -0,0 +1,175 @@ +import { observer } from "mobx-react"; +import { CollectionViewBase } from "./CollectionViewBase"; +import { Document } from "../../../fields/Document"; +import { KeyStore } from "../../../fields/KeyStore"; +import { ListField } from "../../../fields/ListField"; +import React = require("react") +import { TextField } from "../../../fields/TextField"; +import { observable, action } from "mobx"; +import "./CollectionTreeView.scss"; +import { EditableView } from "../EditableView"; +import { setupDrag } from "../../util/DragManager"; +import { FieldWaiting } from "../../../fields/Field"; +import { COLLECTION_BORDER_WIDTH } from "./CollectionView"; + +export interface TreeViewProps { + document: Document; + deleteDoc: (doc: Document) => void; +} + +export enum BulletType { + Collapsed, + Collapsible, + List +} + +@observer +/** + * Component that takes in a document prop and a boolean whether it's collapsed or not. + */ +class TreeView extends React.Component<TreeViewProps> { + + @observable + collapsed: boolean = false; + + delete = () => { + this.props.deleteDoc(this.props.document); + } + + + @action + remove = (document: Document) => { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + if (children && children !== FieldWaiting) { + children.Data.splice(children.Data.indexOf(document), 1); + } + } + + renderBullet(type: BulletType) { + let onClicked = action(() => this.collapsed = !this.collapsed); + + switch (type) { + case BulletType.Collapsed: + return <div className="bullet" onClick={onClicked}>▶</div> + case BulletType.Collapsible: + return <div className="bullet" onClick={onClicked}>▼</div> + case BulletType.List: + return <div className="bullet">—</div> + } + } + + /** + * Renders the EditableView title element for placement into the tree. + */ + renderTitle() { + let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); + + // if the title hasn't loaded, immediately return the div + if (!title || title === "<Waiting>") { + return <div key={this.props.document.Id}></div>; + } + + return <div className="docContainer"> <EditableView contents={title.Data} + height={36} GetValue={() => { + let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== "<Waiting>") + return title.Data; + return ""; + }} SetValue={(value: string) => { + this.props.document.SetData(KeyStore.Title, value, TextField); + return true; + }} /> + <div className="delete-button" onClick={this.delete}>x</div> + </div > + } + + render() { + var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField); + + let reference = React.createRef<HTMLDivElement>(); + let onItemDown = setupDrag(reference, () => this.props.document); + let titleElement = this.renderTitle(); + + // check if this document is a collection + if (children && children !== FieldWaiting) { + let subView; + + // if uncollapsed, then add the children elements + if (!this.collapsed) { + // render all children elements + let childrenElement = (children.Data.map(value => + <TreeView document={value} deleteDoc={this.remove} />) + ) + subView = + <li key={this.props.document.Id} > + {this.renderBullet(BulletType.Collapsible)} + {titleElement} + <ul key={this.props.document.Id}> + {childrenElement} + </ul> + </li> + } else { + subView = <li key={this.props.document.Id}> + {this.renderBullet(BulletType.Collapsed)} + {titleElement} + </li> + } + + return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}> + {subView} + </div> + } + + // otherwise this is a normal leaf node + else { + return <li key={this.props.document.Id}> + {this.renderBullet(BulletType.List)} + {titleElement} + </li>; + } + } +} + + +@observer +export class CollectionTreeView extends CollectionViewBase { + + @action + remove = (document: Document) => { + var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); + if (children && children !== FieldWaiting) { + children.Data.splice(children.Data.indexOf(document), 1); + } + } + + render() { + let titleStr = ""; + let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField); + if (title && title !== FieldWaiting) { + titleStr = title.Data; + } + + var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField); + let childrenElement = !children || children === FieldWaiting ? (null) : + (children.Data.map(value => + <TreeView document={value} key={value.Id} deleteDoc={this.remove} />) + ) + + return ( + <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> + <h3> + <EditableView contents={titleStr} + height={72} GetValue={() => { + return this.props.Document.Title; + }} SetValue={(value: string) => { + this.props.Document.SetData(KeyStore.Title, value, TextField); + return true; + }} /> + </h3> + <ul className="no-indent"> + {childrenElement} + </ul> + </div > + ); + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx new file mode 100644 index 000000000..49df04163 --- /dev/null +++ b/src/client/views/collections/CollectionView.tsx @@ -0,0 +1,119 @@ +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from "../../../fields/Document"; +import { ListField } from "../../../fields/ListField"; +import { SelectionManager } from "../../util/SelectionManager"; +import { ContextMenu } from "../ContextMenu"; +import React = require("react"); +import { KeyStore } from "../../../fields/KeyStore"; +import { NumberField } from "../../../fields/NumberField"; +import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionViewProps } from "./CollectionViewBase"; +import { CollectionTreeView } from "./CollectionTreeView"; +import { Field } from "../../../fields/Field"; + +export enum CollectionViewType { + Invalid, + Freeform, + Schema, + Docking, + Tree +} + +export const COLLECTION_BORDER_WIDTH = 2; + +@observer +export class CollectionView extends React.Component<CollectionViewProps> { + + public static LayoutString(fieldKey: string = "DataKey") { + return `<${CollectionView.name} Document={Document} + ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings} + isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`; + } + + public active: () => boolean = () => CollectionView.Active(this); + addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); } + removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); } + get subView() { return CollectionView.SubView(this); } + + public static Active(self: CollectionView): boolean { + var isSelected = self.props.isSelected(); + var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == self); + var topMost = self.props.isTopMost; + return isSelected || childSelected || topMost; + } + + @action + public static AddDocument(props: CollectionViewProps, doc: Document) { + doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0)); + if (props.Document.Get(props.fieldKey) instanceof Field) { + //TODO This won't create the field if it doesn't already exist + const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) + value.push(doc); + } else { + props.Document.SetData(props.fieldKey, [doc], ListField); + } + } + + @action + public static RemoveDocument(props: CollectionViewProps, doc: Document): boolean { + //TODO This won't create the field if it doesn't already exist + const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>()) + let index = -1; + for (let i = 0; i < value.length; i++) { + if (value[i].Id == doc.Id) { + index = i; + break; + } + } + + if (index !== -1) { + value.splice(index, 1) + + SelectionManager.DeselectAll() + ContextMenu.Instance.clearItems() + return true; + } + return false + } + + get collectionViewType(): CollectionViewType { + let Document = this.props.Document; + let viewField = Document.GetT(KeyStore.ViewType, NumberField); + if (viewField === "<Waiting>") { + return CollectionViewType.Invalid; + } else if (viewField) { + return viewField.Data; + } else { + return CollectionViewType.Freeform; + } + } + + specificContextMenu = (e: React.MouseEvent): void => { + if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 + ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) }) + ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) }) + ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) }) + ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + } + } + + public static SubView(self: CollectionView) { + let subProps = { ...self.props, addDocument: self.addDocument, removeDocument: self.removeDocument, active: self.active, CollectionView: self } + switch (self.collectionViewType) { + case CollectionViewType.Freeform: return (<CollectionFreeFormView {...subProps} />) + case CollectionViewType.Schema: return (<CollectionSchemaView {...subProps} />) + case CollectionViewType.Docking: return (<CollectionDockingView {...subProps} />) + case CollectionViewType.Tree: return (<CollectionTreeView {...subProps} />) + } + return (null); + } + + render() { + return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}> + {this.subView} + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 1cf07ce05..0a3b965f2 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -1,64 +1,121 @@ -import { action, computed } from "mobx"; -import { observer } from "mobx-react"; +import { action } from "mobx"; import { Document } from "../../../fields/Document"; -import { Opt } from "../../../fields/Field"; -import { Key, KeyStore } from "../../../fields/Key"; import { ListField } from "../../../fields/ListField"; -import { SelectionManager } from "../../util/SelectionManager"; -import { ContextMenu } from "../ContextMenu"; import React = require("react"); +import { KeyStore } from "../../../fields/KeyStore"; +import { FieldWaiting } from "../../../fields/Field"; +import { undoBatch } from "../../util/UndoManager"; +import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; -import { CollectionDockingView } from "./CollectionDockingView"; -import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; +import { Documents, DocumentOptions } from "../../documents/Documents"; +import { Key } from "../../../fields/Key"; import { Transform } from "../../util/Transform"; - +import { CollectionView } from "./CollectionView"; export interface CollectionViewProps { - CollectionFieldKey: Key; - DocumentForCollection: Document; - ContainingDocumentView: Opt<DocumentView>; - GetTransform: () => Transform; - BackgroundView: Opt<DocumentView>; - ParentScaling: number; + fieldKey: Key; + Document: Document; + ScreenToLocalTransform: () => Transform; + isSelected: () => boolean; + isTopMost: boolean; + select: (ctrlPressed: boolean) => void; + bindings: any; + panelWidth: () => number; + panelHeight: () => number; + focus: (doc: Document) => void; +} +export interface SubCollectionViewProps extends CollectionViewProps { + active: () => boolean; + addDocument: (doc: Document) => void; + removeDocument: (doc: Document) => boolean; + CollectionView: CollectionView; } -export const COLLECTION_BORDER_WIDTH = 2; - -@observer -export class CollectionViewBase extends React.Component<CollectionViewProps> { - - public static LayoutString(collectionType: string, fieldKey: string = "DataKey") { - return `<${collectionType} ParentScaling={ParentScaling} DocumentForCollection={Document} CollectionFieldKey={${fieldKey}} ContainingDocumentView={DocumentView} BackgroundView={BackgroundView} />`; - } - @computed - public get active(): boolean { - var isSelected = (this.props.ContainingDocumentView instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.ContainingDocumentView)); - var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this); - var topMost = this.props.ContainingDocumentView != undefined && ( - this.props.ContainingDocumentView.props.ContainingCollectionView == undefined || - this.props.ContainingDocumentView.props.ContainingCollectionView instanceof CollectionDockingView); - return isSelected || childSelected || topMost; +export class CollectionViewBase extends React.Component<SubCollectionViewProps> { + private dropDisposer?: DragManager.DragDropDisposer; + protected createDropTarget = (ele: HTMLDivElement) => { + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } } + + @undoBatch @action - addDocument = (doc: Document): void => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>()) - value.push(doc); + protected drop(e: Event, de: DragManager.DropEvent) { + const docView: DocumentView = de.data["documentView"]; + const doc: Document = de.data["document"]; + if (docView && docView.props.ContainingCollectionView && docView.props.ContainingCollectionView !== this.props.CollectionView) { + if (docView.props.RemoveDocument) { + docView.props.RemoveDocument(docView.props.Document); + } + this.props.addDocument(docView.props.Document); + } else if (doc) { + this.props.removeDocument(doc); + this.props.addDocument(doc); + } + e.stopPropagation(); } @action - removeDocument = (doc: Document): boolean => { - //TODO This won't create the field if it doesn't already exist - const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>()) - let index = value.indexOf(doc); - if (index !== -1) { - value.splice(index, 1) + protected onDrop(e: React.DragEvent, options: DocumentOptions): void { + e.stopPropagation() + e.preventDefault() + let that = this; - SelectionManager.DeselectAll() - ContextMenu.Instance.clearItems() - return true; + let html = e.dataTransfer.getData("text/html"); + let text = e.dataTransfer.getData("text/plain"); + if (html && html.indexOf("<img") != 0) { + let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 }); + htmlDoc.SetText(KeyStore.DocumentText, text); + this.props.addDocument(htmlDoc); + return; } - return false - } -}
\ No newline at end of file + for (let i = 0; i < e.dataTransfer.items.length; i++) { + let item = e.dataTransfer.items[i]; + if (item.kind === "string" && item.type.indexOf("uri") != -1) { + e.dataTransfer.items[i].getAsString(function (s) { + action(() => { + var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, width: 300, }) + + let docs = that.props.Document.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.Document.Set(KeyStore.Data, docs) + } + docs.Data.push(img); + } + })() + + }) + } + if (item.kind == "file" && item.type.indexOf("image")) { + let fReader = new FileReader() + let file = item.getAsFile(); + + fReader.addEventListener("load", action("drop", () => { + if (fReader.result) { + let url = "" + fReader.result; + let doc = Documents.ImageDocument(url, options) + let docs = that.props.Document.GetT(KeyStore.Data, ListField); + if (docs != FieldWaiting) { + if (!docs) { + docs = new ListField<Document>(); + that.props.Document.Set(KeyStore.Data, docs) + } + docs.Data.push(doc); + } + } + }), false) + + if (file) { + fReader.readAsDataURL(file) + } + } + } + } +} diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx new file mode 100644 index 000000000..a2c7be1a8 --- /dev/null +++ b/src/client/views/nodes/Annotation.tsx @@ -0,0 +1,117 @@ +import "./ImageBox.scss"; +import React = require("react") +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps{ + Span: HTMLSpanElement; + X: number; + Y: number; + Highlights: any[]; + Annotations: any[]; + CurrAnno: any[]; + +} + +/** + * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color + * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). + * Also need to support multiline highlighting + * + * Written by: Andrew Kim + */ +@observer +export class Annotation extends React.Component<IProps> { + + /** + * changes color of the span (highlighted section) + */ + onColorChange = (e:React.PointerEvent) => { + if (e.currentTarget.innerHTML == "r"){ + this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)" + } else if (e.currentTarget.innerHTML == "b"){ + this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)" + } else if (e.currentTarget.innerHTML == "y"){ + this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)" + } else if (e.currentTarget.innerHTML == "g"){ + this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)" + } + + } + + /** + * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this + */ + @action + onRemove = (e:any) => { + let index:number = -1; + //finding the highlight in the highlight array + this.props.Highlights.forEach((e) => { + for (let i = 0; i < e.spans.length; i++){ + if (e.spans[i] == this.props.Span){ + index = this.props.Highlights.indexOf(e); + this.props.Highlights.splice(index, 1); + } + } + }) + + //removing from CurrAnno and Annotation array + this.props.Annotations.splice(index, 1); + this.props.CurrAnno.pop() + + //removing span from div + if(this.props.Span.parentElement){ + let nodesArray = this.props.Span.parentElement.childNodes; + nodesArray.forEach((e) => { + if (e == this.props.Span){ + if (this.props.Span.parentElement){ + this.props.Highlights.forEach((item) => { + if (item == e){ + item.remove(); + } + }) + e.remove(); + } + } + }) + } + + + } + + render() { + return ( + <div + style = {{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + + }}> + <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}> + <button + style = {{borderRadius: "25px", width:"25%", height:"100%"}} + onClick = {this.onRemove} + >x</button> + <div style = {{width:"75%", height: "100%" , display:"inline-block"}}> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button> + <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button> + </div> + + </div> + <div style = {{width:"200px", height:"200"}}> + <textarea style = {{width: "100%", height: "100%"}} + defaultValue = "Enter Text Here..." + + ></textarea> + </div> + </div> + + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a2fbe96d2..50dc5a619 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,24 +1,16 @@ -import { action, computed } from "mobx"; +import { computed, trace } from "mobx"; import { observer } from "mobx-react"; -import { Key, KeyStore } from "../../../fields/Key"; +import { KeyStore } from "../../../fields/KeyStore"; import { NumberField } from "../../../fields/NumberField"; -import { DragManager } from "../../util/DragManager"; -import { SelectionManager } from "../../util/SelectionManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; -import { ContextMenu } from "../ContextMenu"; +import { Transform } from "../../util/Transform"; +import { DocumentView, DocumentViewProps } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { DocumentView, DocumentViewProps } from "./DocumentView"; -import { Transform } from "../../util/Transform"; @observer -export class CollectionFreeFormDocumentView extends DocumentView { - private _contextMenuCanOpen = false; - private _downX: number = 0; - private _downY: number = 0; - // private _mainCont = React.createRef<HTMLDivElement>(); +export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); constructor(props: DocumentViewProps) { super(props); @@ -30,206 +22,64 @@ export class CollectionFreeFormDocumentView extends DocumentView { return new DOMRect(); } - public LeftCorner(): number { - return this.props.Document.GetNumber(KeyStore.X, 0) + super.LeftCorner(); - } - - public TopCorner(): number { - return this.props.Document.GetNumber(KeyStore.Y, 0) + super.TopCorner(); - } - @computed get transform(): string { - return `scale(${this.props.ParentScaling}, ${this.props.ParentScaling}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; } - @computed - get width(): number { - return this.props.Document.GetNumber(KeyStore.Width, 0); - } - - @computed - get nativeWidth(): number { - return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - } + @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } + @computed get width(): number { return this.props.Document.Width(); } + @computed get height(): number { return this.props.Document.Height(); } + @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } + @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } set width(w: number) { this.props.Document.SetData(KeyStore.Width, w, NumberField) - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) } } - @computed - get height(): number { - return this.props.Document.GetNumber(KeyStore.Height, 0); - } - @computed - get nativeHeight(): number { - return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - } - set height(h: number) { this.props.Document.SetData(KeyStore.Height, h, NumberField); - if (this.nativeWidth > 0 && this.nativeHeight > 0) { + if (this.nativeWidth && this.nativeHeight) { this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) } } - @computed - get zIndex(): number { - return this.props.Document.GetNumber(KeyStore.ZIndex, 0); - } - set zIndex(h: number) { this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) } - @action - dragComplete = (e: DragManager.DragCompleteEvent) => { + contentScaling = () => { + return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; } - @computed - get active(): boolean { - return SelectionManager.IsSelected(this) || this.props.ContainingCollectionView === undefined || - this.props.ContainingCollectionView.active; + getTransform = (): Transform => { + return this.props.ScreenToLocalTransform(). + translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); } @computed - get topMost(): boolean { - return this.props.ContainingCollectionView == undefined || this.props.ContainingCollectionView instanceof CollectionDockingView; - } - - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - var me = this; - if (e.shiftKey && e.buttons === 1) { - CollectionDockingView.StartOtherDrag(this._mainCont.current!, this.props.Document); - e.stopPropagation(); - return; - } - this._contextMenuCanOpen = e.button == 2; - if (this.active && !e.isDefaultPrevented()) { - e.stopPropagation(); - if (e.buttons === 2) { - e.preventDefault(); - } - document.removeEventListener("pointermove", this.onPointerMove) - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp) - document.addEventListener("pointerup", this.onPointerUp); - } - } - - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) { - this._contextMenuCanOpen = false; - return; - } - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - this._contextMenuCanOpen = false; - if (this._mainCont.current != null && !this.topMost) { - this._contextMenuCanOpen = false; - const rect = this.screenRect; - let dragData: { [id: string]: any } = {}; - dragData["document"] = this; - dragData["xOffset"] = e.x - rect.left; - dragData["yOffset"] = e.y - rect.top; - DragManager.StartDrag(this._mainCont.current, dragData, { - handlers: { - dragComplete: this.dragComplete, - }, - hideSource: true - }) - } - } - e.stopPropagation(); - e.preventDefault(); - } - - onPointerUp = (e: PointerEvent): void => { - document.removeEventListener("pointermove", this.onPointerMove) - document.removeEventListener("pointerup", this.onPointerUp) - e.stopPropagation(); - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { - SelectionManager.SelectDoc(this, e.ctrlKey); - } - } - - openRight = (e: React.MouseEvent): void => { - CollectionDockingView.AddRightSplit(this.props.Document); - } - - deleteClicked = (e: React.MouseEvent): void => { - if (this.props.ContainingCollectionView instanceof CollectionFreeFormView) { - this.props.ContainingCollectionView.removeDocument(this.props.Document) - } - } - @action - fullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.OpenFullScreen(this.props.Document); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - @action - closeFullScreenClicked = (e: React.MouseEvent): void => { - CollectionDockingView.CloseFullScreen(); - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - - @action - onContextMenu = (e: React.MouseEvent): void => { - if (!SelectionManager.IsSelected(this)) { - return; - } - e.preventDefault() - - if (!this._contextMenuCanOpen) { - return; - } - - if (this.topMost) { - ContextMenu.Instance.clearItems() - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - } - else { - // DocumentViews should stop propogation of this event - e.stopPropagation(); - - ContextMenu.Instance.clearItems(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) - ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight }) - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) - ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) - SelectionManager.SelectDoc(this, e.ctrlKey); - } - } - - getTransform = (): Transform => { - return this.props.GetTransform().translated(this.props.Document.GetNumber(KeyStore.X, 0), this.props.Document.GetNumber(KeyStore.Y, 0)); + get docView() { + return <DocumentView {...this.props} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + /> } render() { - var parentScaling = this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; return ( - <div className="documentView-node" ref={this._mainCont} - onContextMenu={this.onContextMenu} - onPointerDown={this.onPointerDown} - style={{ - transformOrigin: "left top", - transform: this.transform, - width: this.width, - height: this.height, - position: "absolute", - zIndex: this.zIndex, - }}> - - <DocumentView {...this.props} ref={this._renderDoc} ParentScaling={parentScaling} GetTransform={this.getTransform} DocumentView={this} /> + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ + transformOrigin: "left top", + transform: this.transform, + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.zIndex, + backgroundColor: "transparent" + }} > + {this.docView} </div> ); } diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 8e2ebd690..ab913897b 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,7 +1,7 @@ .documentView-node { position: absolute; background: #cdcdcd; - overflow: hidden; + //overflow: hidden; &.minimized { width: 30px; height: 30px; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c5270e0cd..41e93df35 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,193 +1,252 @@ import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { Document } from "../../../fields/Document"; -import { Opt, FieldWaiting } from "../../../fields/Field"; -import { Key, KeyStore } from "../../../fields/Key"; +import { Field, FieldWaiting, Opt } from "../../../fields/Field"; +import { Key } from "../../../fields/Key"; +import { KeyStore } from "../../../fields/KeyStore"; import { ListField } from "../../../fields/ListField"; -import { Utils } from "../../../Utils"; +import { DragManager } from "../../util/DragManager"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Transform } from "../../util/Transform"; import { CollectionDockingView } from "../collections/CollectionDockingView"; import { CollectionFreeFormView } from "../collections/CollectionFreeFormView"; import { CollectionSchemaView } from "../collections/CollectionSchemaView"; -import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase"; +import { CollectionView, CollectionViewType } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { ContextMenu } from "../ContextMenu"; import { FormattedTextBox } from "../nodes/FormattedTextBox"; import { ImageBox } from "../nodes/ImageBox"; +import { Documents } from "../../documents/Documents" +import { KeyValueBox } from "./KeyValueBox" +import { WebBox } from "../nodes/WebBox"; +import { PDFBox } from "../nodes/PDFBox"; import "./DocumentView.scss"; import React = require("react"); -import { Transform } from "../../util/Transform"; -const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this? +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? -export interface DocumentViewProps { - DocumentView: Opt<DocumentView> // needed only to set ContainingDocumentView on CollectionViewProps when invoked from JsxParser -- is there a better way? - ContainingCollectionView: Opt<CollectionViewBase>; +export interface DocumentViewProps { + ContainingCollectionView: Opt<CollectionView>; Document: Document; AddDocument?: (doc: Document) => void; RemoveDocument?: (doc: Document) => boolean; - GetTransform: () => Transform; - ParentScaling: number; + ScreenToLocalTransform: () => Transform; + isTopMost: boolean; + ContentScaling: () => number; + PanelWidth: () => number; + PanelHeight: () => number; + focus: (doc: Document) => void; + SelectOnLoad: boolean; +} +export interface JsxArgs extends DocumentViewProps { + Keys: { [name: string]: Key } + Fields: { [name: string]: Field } } -@observer -export class DocumentView extends React.Component<DocumentViewProps> { - - protected _renderDoc = React.createRef<any>(); - protected _mainCont = React.createRef<any>(); - get MainContent() { - return this._mainCont; +/* +This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that +jsx-to-string can recover the jsx from +Example usage of this function: + public static LayoutString() { + let args = FakeJsxArgs(["Data"]); + return jsxToString( + <CollectionFreeFormView + doc={args.Document} + fieldKey={args.Keys.Data} + DocumentViewForField={args.DocumentView} />, + { useFunctionCode: true, functionNameOnly: true } + ) } - - @computed - get parentScaling(): number { - return this._renderDoc.current ? this._renderDoc.current.props.ParentScaling : this.props.ParentScaling > 0 ? this.props.ParentScaling : 1; +*/ +export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs { + let Keys: { [name: string]: any } = {} + let Fields: { [name: string]: any } = {} + for (const key of keys) { + let fn = () => { } + Object.defineProperty(fn, "name", { value: key + "Key" }) + Keys[key] = fn; } - - @computed - get layout(): string { - return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); + for (const field of fields) { + let fn = () => { } + Object.defineProperty(fn, "name", { value: field }) + Fields[field] = fn; } + let args: JsxArgs = { + Document: function Document() { }, + DocumentView: function DocumentView() { }, + Keys, + Fields + } as any; + return args; +} - @computed - get backgroundLayout(): string { - return this.props.Document.GetText(KeyStore.BackgroundLayout, ""); +@observer +export class DocumentView extends React.Component<DocumentViewProps> { + private _mainCont = React.createRef<HTMLDivElement>(); + private _documentBindings: any = null; + private _downX: number = 0; + private _downY: number = 0; + @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); } + @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; } + @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); } + @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); } + @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); } + screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect(); + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (e.shiftKey && e.buttons === 1) { + CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e); + e.stopPropagation(); + } else { + if (this.active && !e.isDefaultPrevented()) { + e.stopPropagation(); + if (e.buttons === 2) { + e.preventDefault(); + } + document.removeEventListener("pointermove", this.onPointerMove) + document.addEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp) + document.addEventListener("pointerup", this.onPointerUp); + } + } } - - @computed - get layoutKeys(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); + onPointerMove = (e: PointerEvent): void => { + if (e.cancelBubble) { + return; + } + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) + if (this._mainCont.current != null && !this.topMost) { + const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + let dragData: { [id: string]: any } = {}; + dragData["documentView"] = this; + dragData["xOffset"] = e.x - left; + dragData["yOffset"] = e.y - top; + DragManager.StartDrag(this._mainCont.current, dragData, { + handlers: { + dragComplete: action(() => { }), + }, + hideSource: true + }) + } + } + e.stopPropagation(); + e.preventDefault(); } - - @computed - get layoutFields(): Key[] { - return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointermove", this.onPointerMove) + document.removeEventListener("pointerup", this.onPointerUp) + e.stopPropagation(); + if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { + SelectionManager.SelectDoc(this, e.ctrlKey); + } } - // - // returns the cumulative scaling between the document and the screen - // - @computed - public get ScalingToScreenSpace(): number { - if (this.props.ContainingCollectionView != undefined && - this.props.ContainingCollectionView.props.ContainingDocumentView != undefined) { - let ss = this.props.ContainingCollectionView.props.DocumentForCollection.GetNumber(KeyStore.Scale, 1); - return this.props.ContainingCollectionView.props.ContainingDocumentView.ScalingToScreenSpace * ss; + deleteClicked = (): void => { + if (this.props.RemoveDocument) { + this.props.RemoveDocument(this.props.Document); } - return 1; } - - public LeftCorner(): number { - if (this.props.ContainingCollectionView) { - if (this.props.ContainingCollectionView instanceof CollectionDockingView) { - // this is a hacky way to account for the titles/pane placement/etc of a CollectionDockingView - // this only works if the collectionDockingView is the root collection, too. - // need to find a better way. - var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!); - return rx + COLLECTION_BORDER_WIDTH; - } - return COLLECTION_BORDER_WIDTH; // assumes all collections have the same border + fieldsClicked = (e: React.MouseEvent): void => { + if (this.props.AddDocument) { + this.props.AddDocument(Documents.KVPDocument(this.props.Document)); } - return 0; + } + fullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.OpenFullScreen(this.props.Document); + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) } - public TopCorner(): number { - if (this.props.ContainingCollectionView) { - if (this.props.ContainingCollectionView instanceof CollectionDockingView) { - // this is a hacky way to account for the titles/pane placement/etc of a CollectionDockingView - // this only works if the collectionDockingView is the root collection, too. - // need to find a better way. - var { translateX: rx, translateY: ry } = Utils.GetScreenTransform(this.MainContent.current!); - return ry + COLLECTION_BORDER_WIDTH; - } - return COLLECTION_BORDER_WIDTH; // assumes all collections have the same border - } - return 0; + closeFullScreenClicked = (e: React.MouseEvent): void => { + CollectionDockingView.Instance.CloseFullScreen(); + ContextMenu.Instance.clearItems(); + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) } - // - // Converts a coordinate in the screen space of the app to a local point in the space of the DocumentView. - // This also returns the point in the coordinate space of this document's containing CollectionView - // - public TransformToLocalPoint(screenX: number, screenY: number) { - // if this collection view is nested within another collection view, then - // first transform the screen point into the parent collection's coordinate space. - let containingCollectionViewDoc = this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.ContainingDocumentView : undefined; - let { LocalX: parentX, LocalY: parentY } = !containingCollectionViewDoc ? { LocalX: screenX, LocalY: screenY } : - containingCollectionViewDoc.TransformToLocalPoint(screenX, screenY); - let ContainerX: number = parentX - COLLECTION_BORDER_WIDTH; - let ContainerY: number = parentY - COLLECTION_BORDER_WIDTH; - let Ss = this.props.Document.GetNumber(KeyStore.Scale, 1); - let Panxx = this.props.Document.GetNumber(KeyStore.PanX, 0); - let Panyy = this.props.Document.GetNumber(KeyStore.PanY, 0); - let LocalX = (ContainerX - (this.LeftCorner() + Panxx)) / Ss; - let LocalY = (ContainerY - (this.TopCorner() + Panyy)) / Ss; + @action + onContextMenu = (e: React.MouseEvent): void => { + e.stopPropagation(); + let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3; + if (moved || e.isDefaultPrevented()) { + e.preventDefault() + return; + } + e.preventDefault() + + ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }) + ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }) + ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }) + ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }) + //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + if (!this.topMost) { + // DocumentViews should stop propagation of this event + e.stopPropagation(); + } - return { LocalX, LocalY, ContainerX, ContainerY }; + ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }) + ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15) + SelectionManager.SelectDoc(this, e.ctrlKey); + } + @computed get mainContent() { + return <JsxParser + components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }} + bindings={this._documentBindings} + jsx={this.layout} + showWarnings={true} + onError={(test: any) => { console.log(test) }} + /> } - // - // Converts a point in the coordinate space of the document to coordinate in app screen coordinates - // - public TransformToScreenPoint(localX: number, localY: number, applyViewXf: boolean = false): { ScreenX: number, ScreenY: number } { - var parentScaling = applyViewXf ? this.parentScaling : 1; - let Panxx = applyViewXf ? this.props.Document.GetNumber(KeyStore.PanX, 0) : 0; - let Panyy = applyViewXf ? this.props.Document.GetNumber(KeyStore.PanY, 0) : 0; - var Zoom = applyViewXf ? this.props.Document.GetNumber(KeyStore.Scale, 1) : 1; + isSelected = () => { + return SelectionManager.IsSelected(this); + } - let parentX = this.LeftCorner() + (Panxx + (localX - COLLECTION_BORDER_WIDTH) * Zoom) * parentScaling; - let parentY = this.TopCorner() + (Panyy + (localY - COLLECTION_BORDER_WIDTH) * Zoom) * parentScaling; - // if this collection view is nested within another collection view, then - // first transform the local point into the parent collection's coordinate space. - let containingDocView = this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.ContainingDocumentView : undefined; - if (containingDocView) { - let { ScreenX, ScreenY } = containingDocView.TransformToScreenPoint(parentX + COLLECTION_BORDER_WIDTH * parentScaling, parentY + COLLECTION_BORDER_WIDTH * parentScaling, true); - parentX = ScreenX; - parentY = ScreenY; - } - return { ScreenX: parentX, ScreenY: parentY }; + select = (ctrlPressed: boolean) => { + SelectionManager.SelectDoc(this, ctrlPressed) } render() { - let bindings = { ...this.props } as any; + if (!this.props.Document) return <div></div> + let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField); + if (!lkeys || lkeys === "<Waiting>") { + return <p>Error loading layout keys</p>; + } + this._documentBindings = { + ...this.props, + isSelected: this.isSelected, + select: this.select, + focus: this.props.focus + }; for (const key of this.layoutKeys) { - bindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data + this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data } for (const key of this.layoutFields) { let field = this.props.Document.Get(key); - bindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; - } - if (bindings.DocumentView === undefined) { - bindings.DocumentView = this; // set the DocumentView to this if it hasn't already been set by a sub-class during its render method. + this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field; } - if (this.backgroundLayout) { - var backgroundview = <JsxParser - components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }} - bindings={bindings} - jsx={this.backgroundLayout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - />; - bindings["BackgroundView"] = backgroundview; - } - var nativewidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); - var nativeheight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); - var width = nativewidth > 0 ? nativewidth + "px" : "100%"; - var height = nativeheight > 0 ? nativeheight + "px" : "100%"; + this._documentBindings.bindings = this._documentBindings; + var scaling = this.props.ContentScaling(); + var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); + var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); return ( <div className="documentView-node" ref={this._mainCont} style={{ - width: width, - height: height, - transformOrigin: "top left", - transform: `scale(${this.props.ParentScaling},${this.props.ParentScaling})` - }}> - <JsxParser - components={{ FormattedTextBox: FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }} - bindings={bindings} - jsx={this.layout} - showWarnings={true} - onError={(test: any) => { console.log(test) }} - /> + width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", + height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", + transformOrigin: "left top", + transform: `scale(${scaling} , ${scaling})` + }} + onContextMenu={this.onContextMenu} + onPointerDown={this.onPointerDown} > + {this.mainContent} </div> ) } -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 12371eb2e..9e63006d1 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,16 +1,17 @@ import React = require("react") import { observer } from "mobx-react"; import { computed } from "mobx"; -import { Field, Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Field, FieldWaiting, FieldValue } from "../../../fields/Field"; import { Document } from "../../../fields/Document"; import { TextField } from "../../../fields/TextField"; import { NumberField } from "../../../fields/NumberField"; import { RichTextField } from "../../../fields/RichTextField"; import { ImageField } from "../../../fields/ImageField"; +import { WebField } from "../../../fields/WebField"; import { Key } from "../../../fields/Key"; import { FormattedTextBox } from "./FormattedTextBox"; import { ImageBox } from "./ImageBox"; -import { DocumentView } from "./DocumentView"; +import { WebBox } from "./WebBox"; // // these properties get assigned through the render() method of the DocumentView when it creates this node. @@ -20,12 +21,19 @@ import { DocumentView } from "./DocumentView"; export interface FieldViewProps { fieldKey: Key; doc: Document; - DocumentViewForField: Opt<DocumentView> + isSelected: () => boolean; + select: () => void; + isTopMost: boolean; + selectOnLoad: boolean; + bindings: any; } @observer export class FieldView extends React.Component<FieldViewProps> { - public static LayoutString(fieldType: string) { return `<${fieldType} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} />`; } + public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") { + return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`; + } + @computed get field(): FieldValue<Field> { const { doc, fieldKey } = this.props; @@ -45,11 +53,20 @@ export class FieldView extends React.Component<FieldViewProps> { else if (field instanceof ImageField) { return <ImageBox {...this.props} /> } + else if (field instanceof WebField) { + return <WebBox {...this.props} /> + } + // bcz: this belongs here, but it doesn't render well so taking it out for now + // else if (field instanceof HtmlField) { + // return <WebBox {...this.props} /> + // } else if (field instanceof NumberField) { return <p>{field.Data}</p> - } else if (field != FieldWaiting) { - return <p>{field.GetValue}</p> - } else + } + else if (field != FieldWaiting) { + return <p>{JSON.stringify(field.GetValue())}</p> + } + else return <p> {"Waiting for server..."} </p> } diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 5139d5d6b..ab5849f09 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,7 +1,7 @@ .ProseMirror { - margin-top: -1em; width: 100%; - height: 100%; + height: auto; + min-height: 100% } .ProseMirror:focus { @@ -9,6 +9,30 @@ } .formattedTextBox-cont { - background: beige; - padding: 1vw; -}
\ No newline at end of file + background: white; + padding: 1; + border-width: 1px; + border-radius: 2px; + border-color:black; + box-sizing: border-box; + background: white; + border-style:solid; + overflow-y: scroll; + overflow-x: hidden; + color: initial; + height: 100%; +} + +.menuicon { + display: inline-block; + border-right: 1px solid rgba(0, 0, 0, 0.2); + color: #888; + line-height: 1; + padding: 0 7px; + margin: 1px; + cursor: pointer; + text-align: center; + min-width: 1.4em; + } + .strong, .heading { font-weight: bold; } + .em { font-style: italic; }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index f6bd0c0f8..a6cee9957 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -2,17 +2,20 @@ import { action, IReactionDisposer, reaction } from "mobx"; import { baseKeymap } from "prosemirror-commands"; import { history, redo, undo } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; -import { schema } from "prosemirror-schema-basic"; -import { EditorState, Transaction } from "prosemirror-state"; +import { schema } from "../../util/RichTextSchema"; +import { EditorState, Transaction, } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field"; -import { SelectionManager } from "../../util/SelectionManager"; +import { Opt, FieldWaiting } from "../../../fields/Field"; import "./FormattedTextBox.scss"; import React = require("react") import { RichTextField } from "../../../fields/RichTextField"; import { FieldViewProps, FieldView } from "./FieldView"; -import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView"; -import { observer } from "mobx-react"; +import { Plugin } from 'prosemirror-state' +import { Decoration, DecorationSet } from 'prosemirror-view' +import { TooltipTextMenu } from "../../util/TooltipTextMenu" +import { ContextMenu } from "../../views/ContextMenu"; + + // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document @@ -33,7 +36,7 @@ import { observer } from "mobx-react"; //] export class FormattedTextBox extends React.Component<FieldViewProps> { - public static LayoutString() { return FieldView.LayoutString("FormattedTextBox"); } + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) } private _ref: React.RefObject<HTMLDivElement>; private _editorView: Opt<EditorView>; private _reactionDisposer: Opt<IReactionDisposer>; @@ -42,7 +45,6 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); - this.onChange = this.onChange.bind(this); } @@ -50,25 +52,24 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { if (this._editorView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const { doc, fieldKey } = this.props; - doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField); + this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField); } } componentDidMount() { let state: EditorState; - const { doc, fieldKey } = this.props; const config = { schema, plugins: [ history(), keymap({ "Mod-z": undo, "Mod-y": redo }), - keymap(baseKeymap) + keymap(baseKeymap), + this.tooltipMenuPlugin() ] }; - let field = doc.GetT(fieldKey, RichTextField); - if (field && field != FieldWaiting) { // bcz: don't think this works + let field = this.props.doc.GetT(this.props.fieldKey, RichTextField); + if (field && field != FieldWaiting) { state = EditorState.fromJSON(config, JSON.parse(field.Data)); } else { state = EditorState.create(config); @@ -88,6 +89,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))); } }) + if (this.props.selectOnLoad) { + this.props.select(); + this._editorView!.focus(); + } } componentWillUnmount() { @@ -105,22 +110,52 @@ export class FormattedTextBox extends React.Component<FieldViewProps> { @action onChange(e: React.ChangeEvent<HTMLInputElement>) { - const { fieldKey, doc } = this.props; - doc.SetData(fieldKey, e.target.value, RichTextField); + this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField); } onPointerDown = (e: React.PointerEvent): void => { - let me = this; - if (e.buttons === 1 && me.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(me.props.DocumentViewForField)) { + if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); } } + + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + textCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability }); + // ContextMenu.Instance.addItem({ + // description: "Submenu", + // items: [ + // { + // description: "item 1", event: + // }, + // { + // description: "item 2", event: + // } + // ] + // }) + // e.stopPropagation() + + } + + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + tooltipMenuPlugin() { + return new Plugin({ + view(_editorView) { + return new TooltipTextMenu(_editorView) + } + }) + } + render() { return (<div className="formattedTextBox-cont" - style={{ - color: "initial", - whiteSpace: "initial", - }} onPointerDown={this.onPointerDown} + onContextMenu={this.specificContextMenu} + onWheel={this.onPointerWheel} ref={this._ref} />) } }
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 36f5e0fe0..ea459b911 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -1,14 +1,17 @@ .imageBox-cont { padding: 0vw; - position: absolute; + position: relative; + text-align: center; width: 100%; + height: auto; max-width: 100%; max-height: 100% } + .imageBox-cont img { - max-width: 100%; - max-height: 100% + object-fit: contain; + height: 100%; } .imageBox-button { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2fa70734d..30910fb1f 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,22 +1,22 @@ +import { action, observable } from 'mobx'; +import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { SelectionManager } from "../../util/SelectionManager"; +import { FieldWaiting } from '../../../fields/Field'; +import { ImageField } from '../../../fields/ImageField'; +import { KeyStore } from '../../../fields/KeyStore'; +import { ContextMenu } from "../../views/ContextMenu"; +import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react") -import { ImageField } from '../../../fields/ImageField'; -import { FieldViewProps, FieldView } from './FieldView'; -import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; -import { FieldWaiting } from '../../../fields/Field'; -import { observer } from "mobx-react" -import { observable, action, spy } from 'mobx'; -import { KeyStore } from '../../../fields/Key'; @observer export class ImageBox extends React.Component<FieldViewProps> { - public static LayoutString() { return FieldView.LayoutString("ImageBox"); } + public static LayoutString() { return FieldView.LayoutString(ImageBox) } private _ref: React.RefObject<HTMLDivElement>; + private _imgRef: React.RefObject<HTMLImageElement>; private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @@ -27,12 +27,20 @@ export class ImageBox extends React.Component<FieldViewProps> { super(props); this._ref = React.createRef(); + this._imgRef = React.createRef(); this.state = { photoIndex: 0, isOpen: false, }; } + @action + onLoad = (target: any) => { + var h = this._imgRef.current!.naturalHeight; + var w = this._imgRef.current!.naturalWidth; + this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w) + } + componentDidMount() { } @@ -41,7 +49,7 @@ export class ImageBox extends React.Component<FieldViewProps> { onPointerDown = (e: React.PointerEvent): void => { if (Date.now() - this._lastTap < 300) { - if (e.buttons === 1 && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) { + if (e.buttons === 1 && this.props.isSelected()) { e.stopPropagation(); this._downX = e.clientX; this._downY = e.clientY; @@ -63,7 +71,7 @@ export class ImageBox extends React.Component<FieldViewProps> { lightbox = (path: string) => { const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"]; - if (this._isOpen && this.props.DocumentViewForField instanceof CollectionFreeFormDocumentView && SelectionManager.IsSelected(this.props.DocumentViewForField)) { + if (this._isOpen && this.props.isSelected()) { return (<Lightbox mainSrc={images[this._photoIndex]} nextSrc={images[(this._photoIndex + 1) % images.length]} @@ -81,15 +89,22 @@ export class ImageBox extends React.Component<FieldViewProps> { } } + //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE + imageCapability = (e: React.MouseEvent): void => { + } + + specificContextMenu = (e: React.MouseEvent): void => { + ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability }); + } + render() { let field = this.props.doc.Get(this.props.fieldKey); let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif"; let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1); - return ( - <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} > - <img src={path} width={nativeWidth} alt="Image not found" /> + <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}> + <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> {this.lightbox(path)} </div>) } diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss new file mode 100644 index 000000000..1295266e5 --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.scss @@ -0,0 +1,31 @@ +.keyValueBox-cont { + overflow-y:scroll; + height: 100%; + border: black; + border-width: 1px; + border-style: solid; + box-sizing: border-box; + display: inline-block; + .imageBox-cont img { + max-height:45px; + height: auto; + } +} +.keyValueBox-table { + position: relative; +} +.keyValueBox-header { + background:gray; +} +.keyValueBox-evenRow { + background: white; + .formattedTextBox-cont { + background: white; + } +} +.keyValueBox-oddRow { + background: lightGray; + .formattedTextBox-cont { + background: lightgray; + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx new file mode 100644 index 000000000..ac8c949a9 --- /dev/null +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -0,0 +1,66 @@ + +import { observer } from "mobx-react"; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Document } from '../../../fields/Document'; +import { FieldWaiting } from '../../../fields/Field'; +import { KeyStore } from '../../../fields/KeyStore'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./KeyValueBox.scss"; +import { KeyValuePair } from "./KeyValuePair"; +import React = require("react") + +@observer +export class KeyValueBox extends React.Component<FieldViewProps> { + + public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) } + + onPointerDown = (e: React.PointerEvent): void => { + if (e.buttons === 1 && this.props.isSelected()) { + e.stopPropagation(); + } + } + onPointerWheel = (e: React.WheelEvent): void => { + e.stopPropagation(); + } + + createTable = () => { + let doc = this.props.doc.GetT(KeyStore.Data, Document); + if (!doc || doc == FieldWaiting) { + return <tr><td>Loading...</td></tr> + } + let realDoc = doc; + + let ids: { [key: string]: string } = {}; + let protos = doc.GetAllPrototypes(); + for (const proto of protos) { + proto._proxies.forEach((val, key) => { + if (!(key in ids)) { + ids[key] = key; + } + }) + } + + let rows: JSX.Element[] = []; + let i = 0; + for (let key in ids) { + rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />) + } + return rows; + } + + + render() { + + return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> + <table className="keyValueBox-table"> + <tbody> + <tr className="keyValueBox-header"> + <th>Key</th> + <th>Fields</th> + </tr> + {this.createTable()} + </tbody> + </table> + </div>) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx new file mode 100644 index 000000000..a97e98313 --- /dev/null +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -0,0 +1,58 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import "./KeyValueBox.scss"; +import React = require("react") +import { FieldViewProps, FieldView } from './FieldView'; +import { Opt, Field } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { observable, action } from 'mobx'; +import { Document } from '../../../fields/Document'; +import { Key } from '../../../fields/Key'; +import { Server } from "../../Server" + +// Represents one row in a key value plane + +export interface KeyValuePairProps { + rowStyle: string; + fieldId: string; + doc: Document; +} +@observer +export class KeyValuePair extends React.Component<KeyValuePairProps> { + + @observable + private key: Opt<Key> + + constructor(props: KeyValuePairProps) { + super(props); + Server.GetField(this.props.fieldId, + action((field: Opt<Field>) => { + if (field) { + this.key = field as Key; + } + })); + + } + + + render() { + if (!this.key) { + return <tr><td>error</td><td></td></tr> + + } + let props: FieldViewProps = { + doc: this.props.doc, + fieldKey: this.key, + isSelected: () => false, + select: () => { }, + isTopMost: false, + bindings: {}, + selectOnLoad: false, + } + return ( + <tr className={this.props.rowStyle}> + <td>{this.key.Name}</td> + <td><FieldView {...props} /></td> + </tr> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss new file mode 100644 index 000000000..9f92410d4 --- /dev/null +++ b/src/client/views/nodes/PDFBox.scss @@ -0,0 +1,15 @@ +.react-pdf__Page { + transform-origin: left top; + position: absolute; +} +.react-pdf__Document { + position: absolute; +} +.pdfBox-buttonTray { + position:absolute; + z-index: 25; +} +.pdfBox-contentContainer { + position: absolute; + transform-origin: "left top"; +}
\ No newline at end of file diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx new file mode 100644 index 000000000..70a70c7c8 --- /dev/null +++ b/src/client/views/nodes/PDFBox.tsx @@ -0,0 +1,490 @@ +import * as htmlToImage from "html-to-image"; +import { action, computed, observable, reaction, IReactionDisposer } from 'mobx'; +import { observer } from "mobx-react"; +import 'react-image-lightbox/style.css'; +import Measure from "react-measure"; +//@ts-ignore +import { Document, Page } from "react-pdf"; +import 'react-pdf/dist/Page/AnnotationLayer.css'; +import { FieldWaiting, Opt } from '../../../fields/Field'; +import { ImageField } from '../../../fields/ImageField'; +import { KeyStore } from '../../../fields/KeyStore'; +import { PDFField } from '../../../fields/PDFField'; +import { Utils } from '../../../Utils'; +import { Annotation } from './Annotation'; +import { FieldView, FieldViewProps } from './FieldView'; +import "./ImageBox.scss"; +import "./PDFBox.scss"; +import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here +import React = require("react") + +/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx + * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, + * area selection (I call it stickies), embedded ink node for directly annotating using a pen or + * mouse, and pagination. + * + * + * HOW TO USE: + * AREA selection: + * 1) Click on Area button. + * 2) click on any part of the PDF, and drag to get desired sized area shape + * 3) You can write on the area (hence the reason why it's called sticky) + * 4) to make another area, you need to click on area button AGAIN. + * + * HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...) + * 1) just click and drag on a text + * 2) click highlight + * 3) for annotation, just pull your cursor over to that text + * 4) another method: click on highlight first and then drag on your desired text + * 5) To make another highlight, you need to reclick on the button + * + * Draw: + * 1) click draw and select color. then just draw like there's no tomorrow. + * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session. + * + * Pagination: + * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't. + * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved. + * + * + * written by: Andrew Kim + */ +@observer +export class PDFBox extends React.Component<FieldViewProps> { + public static LayoutString() { return FieldView.LayoutString(PDFBox); } + + private _mainDiv = React.createRef<HTMLDivElement>() + private _pdf = React.createRef<HTMLCanvasElement>(); + + //very useful for keeping track of X and y position throughout the PDF Canvas + private initX: number = 0; + private initY: number = 0; + private initPage: boolean = false; + + //checks if tool is on + private _toolOn: boolean = false; //checks if tool is on + private _pdfContext: any = null; //gets pdf context + private bool: Boolean = false; //general boolean debounce + private currSpan: any;//keeps track of current span (for highlighting) + + private _currTool: any; //keeps track of current tool button reference + private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool + private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference + + private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference + private _currColor: string = "black"; //current color that user selected (for ink/pen) + + private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference + private _highlightToolOn: boolean = false; + private _pdfCanvas: any; + private _reactionDisposer: Opt<IReactionDisposer>; + + @observable private _perPageInfo: Object[] = []; //stores pageInfo + @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno + + @observable private _currAnno: any = [] + @observable private _interactive: boolean = false; + @observable private _loaded: boolean = false; + + @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); } + + componentDidMount() { + this._reactionDisposer = reaction( + () => this.curPage, + () => { + if (this.curPage && this.initPage) { + this.saveThumbnail(); + this._interactive = true; + } else { + if (this.curPage) + this.initPage = true; + } + }, + { fireImmediately: true }); + + } + + componentWillUnmount() { + if (this._reactionDisposer) { + this._reactionDisposer(); + } + } + + /** + * selection tool used for area highlighting (stickies). Kinda temporary + */ + selectionTool = () => { + this._toolOn = true; + } + /** + * when user draws on the canvas. When mouse pointer is down + */ + drawDown = (e: PointerEvent) => { + this.initX = e.offsetX; + this.initY = e.offsetY; + this._pdfContext.beginPath(); + this._pdfContext.lineTo(this.initX, this.initY); + this._pdfContext.strokeStyle = this._currColor; + this._pdfCanvas.addEventListener("pointermove", this.drawMove); + this._pdfCanvas.addEventListener("pointerup", this.drawUp); + + } + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this._pdfContext.lineTo(x, y); + this._pdfContext.stroke(); + } + + drawUp = (e: PointerEvent) => { + this._pdfContext.closePath(); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + } + + + /** + * highlighting helper function + */ + makeEditableAndHighlight = (colour: string) => { + var range, sel = window.getSelection(); + if (sel.rangeCount && sel.getRangeAt) { + range = sel.getRangeAt(0); + } + document.designMode = "on"; + if (!document.execCommand("HiliteColor", false, colour)) { + document.execCommand("HiliteColor", false, colour); + } + + if (range) { + sel.removeAllRanges(); + sel.addRange(range); + + let obj: Object = { parentDivs: [], spans: [] }; + //@ts-ignore + if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case + obj = this.highlightNodes(range.commonAncestorContainer.childNodes) + } else { //single line highlighting case + let parentDiv = range.commonAncestorContainer.parentElement + if (parentDiv) { + if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten + obj = this.highlightNodes(parentDiv.childNodes) + } else { + parentDiv.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + obj.parentDivs.push(parentDiv) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + obj.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + } + } + } + this._pageInfo.divs.push(obj); + + } + document.designMode = "off"; + } + + highlightNodes = (nodes: NodeListOf<ChildNode>) => { + let temp = { parentDivs: [], spans: [] } + nodes.forEach((div) => { + div.childNodes.forEach((child) => { + if (child.nodeName == 'SPAN') { + //@ts-ignore + temp.parentDivs.push(div) + //@ts-ignore + child.id = "highlighted" + //@ts-ignore + temp.spans.push(child) + child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler + } + }) + + }) + return temp; + } + + /** + * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES + */ + @action + onEnter = (e: any) => { + let span: HTMLSpanElement = e.toElement; + let index: any; + this._pageInfo.divs.forEach((obj: any) => { + obj.spans.forEach((element: any) => { + if (element == span) { + if (!index) { + index = this._pageInfo.divs.indexOf(obj); + } + } + }) + }) + + if (this._pageInfo.anno.length >= index + 1) { + if (this._currAnno.length == 0) { + this._currAnno.push(this._pageInfo.anno[index]); + } + } else { + if (this._currAnno.length == 0) { //if there are no current annotation + let div = span.offsetParent; + //@ts-ignore + let divX = div.style.left + //@ts-ignore + let divY = div.style.top + //slicing "px" from the end + divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) + divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) + let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} /> + this._pageInfo.anno.push(annotation); + this._currAnno.push(annotation); + } + } + + } + + /** + * highlight function for highlighting actual text. This works fine. + */ + highlight = (color: string) => { + if (window.getSelection()) { + try { + if (!document.execCommand("hiliteColor", false, color)) { + this.makeEditableAndHighlight(color); + } + } catch (ex) { + this.makeEditableAndHighlight(color) + } + } + } + + /** + * controls the area highlighting (stickies) Kinda temporary + */ + onPointerDown = (e: React.PointerEvent) => { + if (this._toolOn) { + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + + } + } + + /** + * controls area highlighting and partially highlighting. Kinda temporary + */ + @action + onPointerUp = (e: React.PointerEvent) => { + if (this._highlightToolOn) { + this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. + this._highlightToolOn = false; + } + if (this._toolOn) { + let mouse = e.nativeEvent; + let finalX = mouse.offsetX; + let finalY = mouse.offsetY; + let width = Math.abs(finalX - this.initX); //width + let height = Math.abs(finalY - this.initY); //height + + //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky + if (finalX < this.initX) { + this.initX = finalX; + } + if (finalY < this.initY) { + this.initY = finalY; + } + + if (this._mainDiv.current) { + let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} /> + this._pageInfo.area.push(sticky); + } + this._toolOn = false; + } + this._interactive = true; + } + + /** + * starts drawing the line when user presses down. + */ + onDraw = () => { + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + + if (this._drawTool.current) { + this._currTool = this._drawTool.current; + if (this._drawToolOn) { + this._drawToolOn = false; + this._pdfCanvas.removeEventListener("pointerdown", this.drawDown); + this._pdfCanvas.removeEventListener("pointerup", this.drawUp); + this._pdfCanvas.removeEventListener("pointermove", this.drawMove); + this._drawTool.current.style.backgroundColor = "grey"; + } else { + this._drawToolOn = true; + this._pdfCanvas.addEventListener("pointerdown", this.drawDown); + this._drawTool.current.style.backgroundColor = "cyan"; + } + } + } + + + /** + * for changing color (for ink/pen) + */ + onColorChange = (e: React.PointerEvent) => { + if (e.currentTarget.innerHTML == "Red") { + this._currColor = "red"; + } else if (e.currentTarget.innerHTML == "Blue") { + this._currColor = "blue"; + } else if (e.currentTarget.innerHTML == "Green") { + this._currColor = "green"; + } else if (e.currentTarget.innerHTML == "Black") { + this._currColor = "black"; + } + + } + + + /** + * For highlighting (text drag highlighting) + */ + onHighlight = () => { + this._drawToolOn = false; + if (this._currTool != null) { + this._currTool.style.backgroundColor = "grey"; + } + if (this._highlightTool.current) { + this._currTool = this._drawTool.current; + if (this._highlightToolOn) { + this._highlightToolOn = false; + this._highlightTool.current.style.backgroundColor = "grey"; + } else { + this._highlightToolOn = true; + this._highlightTool.current.style.backgroundColor = "orange"; + } + } + } + + + @action + saveThumbnail = () => { + setTimeout(() => { + var me = this; + htmlToImage.toPng(this._mainDiv.current!, + { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 }) + .then(function (dataUrl: string) { + me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); + }, 1000); + } + + @action + onLoaded = (page: any) => { + if (this._mainDiv.current) { + this._mainDiv.current.childNodes.forEach((element) => { + if (element.nodeName == "DIV") { + element.childNodes[0].childNodes.forEach((e) => { + + if (e instanceof HTMLCanvasElement) { + this._pdfCanvas = e; + this._pdfContext = e.getContext("2d") + + } + + }) + } + }) + } + + // bcz: the number of pages should really be set when the document is imported. + this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages); + if (this._perPageInfo.length == 0) { //Makes sure it only runs once + this._perPageInfo = [...Array(page._transport.numPages)] + } + this._loaded = true; + } + + @action + setScaling = (r: any) => { + // bcz: the nativeHeight should really be set when the document is imported. + // also, the native dimensions could be different for different pages of the PDF + // so this design is flawed. + var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0); + if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) { + this.props.doc.SetNumber(KeyStore.NativeHeight, nativeWidth * r.entry.height / r.entry.width); + } + if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) { + this.saveThumbnail(); + } + } + + @computed + get pdfContent() { + let page = this.curPage; + if (page == 0) + page = 1; + const renderHeight = 2400; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; + return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> + <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}> + <Measure onResize={this.setScaling}> + {({ measureRef }) => + <div className="pdfBox-page" ref={measureRef}> + <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} /> + </div> + } + </Measure> + </Document> + </div >; + } + + @computed + get pdfRenderer() { + let proxy = this._loaded ? (null) : this.imageProxyRenderer; + let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); + if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) { + return proxy; + } + return [ + this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), + this._currAnno.map((element: any) => element), + <div key="pdfBox-contentShell"> + {this.pdfContent} + {proxy} + </div> + ]; + } + + @computed + get imageProxyRenderer() { + let field = this.props.doc.Get(KeyStore.Thumbnail); + if (field) { + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; + return <img src={path} width="100%" />; + } + return (null); + } + + render() { + return ( + <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} > + {this.pdfRenderer} + </div > + ); + } + +}
\ No newline at end of file diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx new file mode 100644 index 000000000..d57dd5c0b --- /dev/null +++ b/src/client/views/nodes/Sticky.tsx @@ -0,0 +1,83 @@ +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import React = require("react") +import { observer } from "mobx-react" +import 'react-pdf/dist/Page/AnnotationLayer.css' + +interface IProps { + Height: number; + Width: number; + X: number; + Y: number; +} + +/** + * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file. + * Improvements that could be made: maybe store line array and store that somewhere for future rerendering. + * + * Written By: Andrew Kim + */ +@observer +export class Sticky extends React.Component<IProps> { + + private initX: number = 0; + private initY: number = 0; + + private _ref = React.createRef<HTMLCanvasElement>(); + private ctx: any; //context that keeps track of sticky canvas + + /** + * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas + */ + drawDown = (e: React.PointerEvent) => { + if (this._ref.current) { + this.ctx = this._ref.current.getContext("2d"); + let mouse = e.nativeEvent; + this.initX = mouse.offsetX; + this.initY = mouse.offsetY; + this.ctx.beginPath(); + this.ctx.lineTo(this.initX, this.initY); + this.ctx.strokeStyle = "black"; + document.addEventListener("pointermove", this.drawMove); + document.addEventListener("pointerup", this.drawUp); + } + } + + //when user drags + drawMove = (e: PointerEvent): void => { + //x and y mouse movement + let x = this.initX += e.movementX, + y = this.initY += e.movementY; + //connects the point + this.ctx.lineTo(x, y); + this.ctx.stroke(); + + } + + /** + * when user lifts the mouse, the drawing ends + */ + drawUp = (e: PointerEvent) => { + this.ctx.closePath(); + console.log(this.ctx); + document.removeEventListener("pointermove", this.drawMove); + } + + render() { + return ( + <div onPointerDown={this.drawDown}> + <canvas ref={this._ref} height={this.props.Height} width={this.props.Width} + style={{ + position: "absolute", + top: "20px", + left: "0px", + zIndex: 1, + background: "yellow", + transform: `translate(${this.props.X}px, ${this.props.Y}px)`, + opacity: 0.4 + }} + /> + + </div> + ); + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss new file mode 100644 index 000000000..e72b3c4da --- /dev/null +++ b/src/client/views/nodes/WebBox.scss @@ -0,0 +1,14 @@ + +.webBox-cont { + padding: 0vw; + position: absolute; + width: 100%; + height: 100%; +} + +.webBox-button { + padding : 0vw; + border: none; + width : 100%; + height: 100%; +}
\ No newline at end of file diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx new file mode 100644 index 000000000..2ca8d49ce --- /dev/null +++ b/src/client/views/nodes/WebBox.tsx @@ -0,0 +1,38 @@ +import "./WebBox.scss"; +import React = require("react") +import { WebField } from '../../../fields/WebField'; +import { FieldViewProps, FieldView } from './FieldView'; +import { FieldWaiting } from '../../../fields/Field'; +import { observer } from "mobx-react" +import { computed } from 'mobx'; +import { KeyStore } from '../../../fields/KeyStore'; + +@observer +export class WebBox extends React.Component<FieldViewProps> { + + public static LayoutString() { return FieldView.LayoutString(WebBox); } + + constructor(props: FieldViewProps) { + super(props); + } + + @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); } + + render() { + let field = this.props.doc.Get(this.props.fieldKey); + let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" : + field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu"; + + let content = this.html ? + <span dangerouslySetInnerHTML={{ __html: this.html }}></span> : + <div style={{ width: "100%", height: "100%", position: "absolute" }}> + <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe> + {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />} + </div>; + + return ( + <div className="webBox-cont" > + {content} + </div>) + } +}
\ No newline at end of file |
