diff options
Diffstat (limited to 'src/client')
75 files changed, 3127 insertions, 1898 deletions
| diff --git a/src/client/Server.ts b/src/client/Server.ts index 857101a33..66e9878d9 100644 --- a/src/client/Server.ts +++ b/src/client/Server.ts @@ -1,10 +1,10 @@  import { Key } from "../fields/Key"; -import { ObservableMap, action, reaction } from "mobx"; +import { ObservableMap, action, reaction, runInAction } from "mobx";  import { Field, FieldWaiting, FIELD_WAITING, Opt, FieldId } from "../fields/Field";  import { Document } from "../fields/Document";  import { SocketStub, FieldMap } from "./SocketStub";  import * as OpenSocket from 'socket.io-client'; -import { Utils } from "./../Utils"; +import { Utils, emptyFunction } from "./../Utils";  import { MessageStore, Types } from "./../server/Message";  export class Server { @@ -12,7 +12,6 @@ export class Server {      static Socket: SocketIOClient.Socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`);      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.      public static GetField(fieldid: FieldId): Promise<Opt<Field>>; @@ -59,14 +58,14 @@ export class Server {      public static GetFields(fieldIds: FieldId[]): Promise<{ [id: string]: Field }>;      public static GetFields(fieldIds: FieldId[], callback: (fields: FieldMap) => any): void;      public static GetFields(fieldIds: FieldId[], callback?: (fields: FieldMap) => any): Promise<FieldMap> | void { -        let fn = (cb: (fields: FieldMap) => void) => { +        let fn = action((cb: (fields: FieldMap) => void) => {              let neededFieldIds: FieldId[] = [];              let waitingFieldIds: FieldId[] = []; -            let existingFields: { [id: string]: Field } = {}; +            let existingFields: FieldMap = {};              for (let id of fieldIds) {                  let field = this.ClientFieldsCached.get(id); -                if (!field) { +                if (field === undefined) {                      neededFieldIds.push(id);                      this.ClientFieldsCached.set(id, FieldWaiting);                  } else if (field === FieldWaiting) { @@ -79,7 +78,7 @@ export class Server {                  for (let id of neededFieldIds) {                      let field = fields[id];                      if (field) { -                        if (!(this.ClientFieldsCached.get(field.Id) instanceof Field)) { +                        if (this.ClientFieldsCached.get(field.Id) === FieldWaiting) {                              this.ClientFieldsCached.set(field.Id, field);                          } else {                              throw new Error("we shouldn't be trying to replace things that are already in the cache"); @@ -94,17 +93,17 @@ export class Server {                  }                  reaction(() => waitingFieldIds.map(id => this.ClientFieldsCached.get(id)),                      (cachedFields, reaction) => { -                        if (!cachedFields.some(field => !field)) { +                        if (!cachedFields.some(field => field === FieldWaiting)) { +                            const realFields = cachedFields as Opt<Field>[];                              reaction.dispose(); -                            for (let field of cachedFields) { -                                let realField = field as Field; -                                existingFields[realField.Id] = realField; -                            } +                            waitingFieldIds.forEach((id, index) => { +                                existingFields[id] = realFields[index]; +                            });                              cb({ ...fields, ...existingFields });                          }                      }, { fireImmediately: true });              })); -        }; +        });          if (callback) {              fn(callback);          } else { @@ -127,13 +126,6 @@ export class Server {          }      } -    public static AddDocument(document: Document) { -        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);      } @@ -161,18 +153,18 @@ export class Server {      }      @action -    static updateField(field: { _id: string, data: any, type: Types }) { -        if (Server.ClientFieldsCached.has(field._id)) { -            var f = Server.ClientFieldsCached.get(field._id); +    static updateField(field: { id: string, data: any, type: Types }) { +        if (Server.ClientFieldsCached.has(field.id)) { +            var f = Server.ClientFieldsCached.get(field.id);              if (f) { -                // console.log("Applying        : " + field._id); +                // console.log("Applying        : " + field.id);                  f.UpdateFromServer(field.data); -                f.init(() => { }); +                f.init(emptyFunction);              } else { -                // console.log("Not applying wa : " + field._id); +                // console.log("Not applying wa : " + field.id);              }          } else { -            // console.log("Not applying mi : " + field._id); +            // console.log("Not applying mi : " + field.id);          }      }  } diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts index 257973e3d..382a81f66 100644 --- a/src/client/SocketStub.ts +++ b/src/client/SocketStub.ts @@ -2,7 +2,7 @@ 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 { MessageStore, Transferable } from "../server/Message";  import { Utils } from "../Utils";  import { Server } from "./Server";  import { ServerUtils } from "../server/ServerUtil"; @@ -16,35 +16,12 @@ export interface FieldMap {  export class SocketStub {      static FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); -    public static SEND_ADD_DOCUMENT(document: Document) { - -        // Send a serialized version of the document to the server -        // ...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)); -        // 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): Promise<Opt<Field>>;      public static SEND_FIELD_REQUEST(fieldid: FieldId, callback: (field: Opt<Field>) => void): void;      public static SEND_FIELD_REQUEST(fieldid: FieldId, callback?: (field: Opt<Field>) => void): Promise<Opt<Field>> | void {          let fn = function (cb: (field: Opt<Field>) => void) { -            Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: any) => { +            Utils.EmitCallback(Server.Socket, MessageStore.GetField, fieldid, (field: Transferable) => {                  if (field) {                      ServerUtils.FromJson(field).init(cb);                  } else { @@ -60,33 +37,15 @@ export class SocketStub {      }      public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: FieldMap) => 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); +        Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: Transferable[]) => { +            let fieldMap: FieldMap = {}; +            fields.map(field => fieldMap[field.id] = ServerUtils.FromJson(field)); +            let proms = Object.values(fieldMap).map(val => +                new Promise(resolve => val!.init(resolve))); +            Promise.all(proms).then(() => 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 -        // associated info of the document id and key where it is used. - -        // ...SOCKET(ADD_DOCUMENT_FIELD, document id, key id, serialized field) - -        // 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.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) {          // Send a request to delete the field stored under the specified key from the document diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 72e6e57ab..b0bb74d89 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1,6 +1,6 @@  import { AudioField } from "../../fields/AudioField";  import { Document } from "../../fields/Document"; -import { Field } from "../../fields/Field"; +import { Field, Opt } from "../../fields/Field";  import { HtmlField } from "../../fields/HtmlField";  import { ImageField } from "../../fields/ImageField";  import { InkField, StrokeData } from "../../fields/InkField"; @@ -26,6 +26,15 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox";  import { PDFBox } from "../views/nodes/PDFBox";  import { VideoBox } from "../views/nodes/VideoBox";  import { WebBox } from "../views/nodes/WebBox"; +import { Gateway } from "../northstar/manager/Gateway"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; +import { action } from "mobx"; +import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel"; +import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel"; +import { AggregateFunction } from "../northstar/model/idea/idea"; +import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss"; +import { IconBox } from "../views/nodes/IconBox"; +import { IconField } from "../../fields/IconFIeld";  export interface DocumentOptions {      x?: number; @@ -57,6 +66,7 @@ export namespace Documents {      let videoProto: Document;      let audioProto: Document;      let pdfProto: Document; +    let iconProto: Document;      const textProtoId = "textProto";      const histoProtoId = "histoProto";      const pdfProtoId = "pdfProto"; @@ -66,6 +76,7 @@ export namespace Documents {      const kvpProtoId = "kvpProto";      const videoProtoId = "videoProto";      const audioProtoId = "audioProto"; +    const iconProtoId = "iconProto";      export function initProtos(): Promise<void> {          return Server.GetFields([textProtoId, histoProtoId, collProtoId, pdfProtoId, imageProtoId, videoProtoId, audioProtoId, webProtoId, kvpProtoId]).then(fields => { @@ -78,6 +89,7 @@ export namespace Documents {              videoProto = fields[videoProtoId] as Document || CreateVideoPrototype();              audioProto = fields[audioProtoId] as Document || CreateAudioPrototype();              pdfProto = fields[pdfProtoId] as Document || CreatePdfPrototype(); +            iconProto = fields[iconProtoId] as Document || CreateIconPrototype();          });      }      function assignOptions(doc: Document, options: DocumentOptions): Document { @@ -86,6 +98,8 @@ export namespace Documents {          if (options.title !== undefined) { doc.SetText(KeyStore.Title, options.title); }          if (options.page !== undefined) { doc.SetNumber(KeyStore.Page, options.page); }          if (options.scale !== undefined) { doc.SetNumber(KeyStore.Scale, options.scale); } +        if (options.width !== undefined) { doc.SetNumber(KeyStore.Width, options.width); } +        if (options.height !== undefined) { doc.SetNumber(KeyStore.Height, options.height); }          if (options.viewType !== undefined) { doc.SetNumber(KeyStore.ViewType, options.viewType); }          if (options.backgroundColor !== undefined) { doc.SetText(KeyStore.BackgroundColor, options.backgroundColor); }          if (options.ink !== undefined) { doc.Set(KeyStore.Ink, new InkField(options.ink)); } @@ -121,7 +135,7 @@ export namespace Documents {      function CreateImagePrototype(): Document {          let imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"), -            { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] }); +            { x: 0, y: 0, nativeWidth: 600, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });          imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString());          imageProto.SetNumber(KeyStore.CurPage, 0);          return imageProto; @@ -133,6 +147,11 @@ export namespace Documents {          histoProto.SetText(KeyStore.BackgroundLayout, HistogramBox.LayoutString());          return histoProto;      } +    function CreateIconPrototype(): Document { +        let iconProto = setupPrototypeOptions(iconProtoId, "ICON_PROTO", IconBox.LayoutString(), +            { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data] }); +        return iconProto; +    }      function CreateTextPrototype(): Document {          let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),              { x: 0, y: 0, width: 300, height: 150, layoutKeys: [KeyStore.Data] }); @@ -197,9 +216,37 @@ export namespace Documents {      export function TextDocument(options: DocumentOptions = {}) {          return assignToDelegate(SetInstanceOptions(textProto, options, ["", TextField]).MakeDelegate(), options);      } +    export function IconDocument(icon: string, options: DocumentOptions = {}) { +        return assignToDelegate(SetInstanceOptions(iconProto, { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE), layoutKeys: [KeyStore.Data], layout: IconBox.LayoutString(), ...options }, [icon, IconField]), options); +    }      export function PdfDocument(url: string, options: DocumentOptions = {}) {          return assignToDelegate(SetInstanceOptions(pdfProto, options, [new URL(url), PDFField]).MakeDelegate(), options);      } +    export async function DBDocument(url: string, options: DocumentOptions = {}) { +        let schemaName = options.title ? options.title : "-no schema-"; +        let ctlog = await Gateway.Instance.GetSchema(url, schemaName); +        if (ctlog && ctlog.schemas) { +            let schema = ctlog.schemas[0]; +            let schemaDoc = Documents.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); +            let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); +            CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { +                Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => { +                    if (field instanceof Document) { +                        schemaDocuments.push(field); +                    } else { +                        var atmod = new ColumnAttributeModel(attr); +                        let histoOp = new HistogramOperation(schema.displayName!, +                            new AttributeTransformationModel(atmod, AggregateFunction.None), +                            new AttributeTransformationModel(atmod, AggregateFunction.Count), +                            new AttributeTransformationModel(atmod, AggregateFunction.Count)); +                        schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias")); +                    } +                })); +            }); +            return schemaDoc; +        } +        return Documents.TreeDocument([], { width: 50, height: 100, title: schemaName }); +    }      export function WebDocument(url: string, options: DocumentOptions = {}) {          return assignToDelegate(SetInstanceOptions(webProto, options, [new URL(url), WebField]).MakeDelegate(), options);      } @@ -242,13 +289,15 @@ export namespace Documents {              <div style="position:relative; height:15%; text-align:center; ">`              + FormattedTextBox.LayoutString("CaptionKey") +              `</div>  -        </div>`; } +        </div>`; +    }      export function FixedCaption(fieldName: string = "Caption") {          return `<div style="position:absolute; height:30px; bottom:0; width:100%">              <div style="position:absolute; width:100%; height:100%; text-align:center;bottom:0;">`              + FormattedTextBox.LayoutString(fieldName + "Key") +              `</div>  -        </div>`; } +        </div>`; +    }      function OuterCaption() {          return (` diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts index 6abde4677..c699691a4 100644 --- a/src/client/northstar/dash-fields/HistogramField.ts +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -6,6 +6,7 @@ import { BasicField } from "../../../fields/BasicField";  import { Field, FieldId } from "../../../fields/Field";  import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";  import { Types } from "../../../server/Message"; +import { OmitKeys } from "../../../Utils";  export class HistogramField extends BasicField<HistogramOperation> { @@ -13,17 +14,8 @@ export class HistogramField extends BasicField<HistogramOperation> {          super(data ? data : HistogramOperation.Empty, save, id);      } -    omitKeys(obj: any, keys: any) { -        var dup: any = {}; -        for (var key in obj) { -            if (keys.indexOf(key) === -1) { -                dup[key] = obj[key]; -            } -        } -        return dup; -    }      toString(): string { -        return JSON.stringify(this.omitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand'])); +        return JSON.stringify(OmitKeys(this.Data, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']));      }      Copy(): Field { @@ -35,12 +27,11 @@ export class HistogramField extends BasicField<HistogramOperation> {      } -    ToJson(): { type: Types, data: string, _id: string } { +    ToJson() {          return {              type: Types.HistogramOp, -              data: this.toString(), -            _id: this.Id +            id: this.Id          };      } diff --git a/src/client/northstar/dash-nodes/HistogramBox.scss b/src/client/northstar/dash-nodes/HistogramBox.scss index e899cf15e..06d781263 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.scss +++ b/src/client/northstar/dash-nodes/HistogramBox.scss @@ -1,12 +1,12 @@  .histogrambox-container {      padding: 0vw;      position: absolute; -    top: 0; -    left:0; +    top: -50%; +    left:-50%;      text-align: center;      width: 100%;      height: 100%; -    background: black; +    background: black;     }    .histogrambox-xaxislabel {      position:absolute; diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index 7df59ef07..e2ecc8c83 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -8,7 +8,7 @@ import { KeyStore } from "../../../fields/KeyStore";  import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";  import { ChartType, VisualBinRange } from '../../northstar/model/binRanges/VisualBinRange';  import { VisualBinRangeHelper } from "../../northstar/model/binRanges/VisualBinRangeHelper"; -import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult, Result } from "../../northstar/model/idea/idea"; +import { AggregateBinRange, AggregateFunction, BinRange, Catalog, DoubleValueAggregateResult, HistogramResult } from "../../northstar/model/idea/idea";  import { ModelHelpers } from "../../northstar/model/ModelHelpers";  import { HistogramOperation } from "../../northstar/operations/HistogramOperation";  import { SizeConverter } from "../../northstar/utils/SizeConverter"; @@ -47,10 +47,6 @@ export class HistogramBox extends React.Component<FieldViewProps> {              this.BinRanges[1] instanceof AggregateBinRange ? ChartType.VerticalBar : ChartType.HeatMap;      } -    constructor(props: FieldViewProps) { -        super(props); -    } -      @action      dropX = (e: Event, de: DragManager.DropEvent) => {          if (de.data instanceof DragManager.DocumentDragData) { @@ -122,7 +118,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {              this.props.Document.GetTAsync(this.props.fieldKey, HistogramField).then((histoOp: Opt<HistogramField>) => runInAction(() => {                  this.HistoOp = histoOp ? histoOp.Data : HistogramOperation.Empty;                  if (this.HistoOp !== HistogramOperation.Empty) { -                    reaction(() => this.props.Document.GetList(KeyStore.LinkedFromDocs, []), (docs: Document[]) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); +                    reaction(() => this.props.Document.GetList(KeyStore.LinkedFromDocs, [] as Document[]), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true });                      reaction(() => this.props.Document.GetList(KeyStore.BrushingDocs, []).length,                          () => {                              let brushingDocs = this.props.Document.GetList(KeyStore.BrushingDocs, [] as Document[]); @@ -150,7 +146,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {          return (              <Measure onResize={(r: any) => runInAction(() => { this.PanelWidth = r.entry.width; this.PanelHeight = r.entry.height; })}>                  {({ measureRef }) => -                    <div className="histogrambox-container" ref={measureRef} style={{ transform: `translate(-50%, -50%)` }}> +                    <div className="histogrambox-container" ref={measureRef}>                          <div className="histogrambox-yaxislabel" onPointerDown={this.yLabelPointerDown} ref={this._dropYRef} >                              <span className="histogrambox-yaxislabel-text">                                  {labelY} diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx index 4c5bdb14b..721bf6a89 100644 --- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx +++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx @@ -1,7 +1,7 @@  import React = require("react");  import { computed, observable, reaction, runInAction, trace, action } from "mobx";  import { observer } from "mobx-react"; -import { Utils as DashUtils } from '../../../Utils'; +import { Utils as DashUtils, emptyFunction } from '../../../Utils';  import { FilterModel } from "../../northstar/core/filter/FilterModel";  import { ModelHelpers } from "../../northstar/model/ModelHelpers";  import { ArrayUtil } from "../../northstar/utils/ArrayUtil"; @@ -49,7 +49,7 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP      private getSelectionToggle(binPrimitives: HistogramBinPrimitive[], allBrushIndex: number, filterModel: FilterModel) {          let rawAllBrushPrim = ArrayUtil.FirstOrDefault(binPrimitives, bp => bp.BrushIndex === allBrushIndex);          if (!rawAllBrushPrim) { -            return () => { }; +            return emptyFunction;          }          let allBrushPrim = rawAllBrushPrim;          return () => runInAction(() => { @@ -97,7 +97,7 @@ export class HistogramBoxPrimitives extends React.Component<HistogramPrimitivesP          let trans1Ypercent = `${yFrom / this.renderDimension * 100}%`;          return <line className="histogramboxprimitives-line" key={DashUtils.GenerateGuid()} x1={trans1Xpercent} x2={`${trans2Xpercent}`} y1={trans1Ypercent} y2={`${trans2Ypercent}`} />;      } -    drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = () => { }) { +    drawRect(r: PIXIRectangle, barAxis: number, color: number | undefined, classExt: string, tapHandler: () => void = emptyFunction) {          if (r.height < 0) {              r.y += r.height;              r.height = -r.height; diff --git a/src/client/northstar/manager/Gateway.ts b/src/client/northstar/manager/Gateway.ts index 8f3b6b11c..d26f2724f 100644 --- a/src/client/northstar/manager/Gateway.ts +++ b/src/client/northstar/manager/Gateway.ts @@ -23,9 +23,9 @@ export class Gateway {          }      } -    public async GetSchema(dbName: string): Promise<Catalog> { +    public async GetSchema(pathname: string, schemaname: string): Promise<Catalog> {          try { -            const json = await this.MakeGetRequest("schema", undefined, dbName); +            const json = await this.MakeGetRequest("schema", undefined, { path: pathname, schema: schemaname });              const cat = Catalog.fromJS(json);              return cat;          } @@ -36,7 +36,7 @@ export class Gateway {      public async ClearCatalog(): Promise<void> {          try { -            const json = await this.MakePostJsonRequest("Datamart/ClearAllAugmentations", {}); +            await this.MakePostJsonRequest("Datamart/ClearAllAugmentations", {});          }          catch (error) {              throw new Error("can not reach northstar's backend"); @@ -144,13 +144,13 @@ export class Gateway {              });      } -    public async MakeGetRequest(endpoint: string, signal?: AbortSignal, data?: any): Promise<any> { -        let url = !data ? Gateway.ConstructUrl(endpoint) : +    public async MakeGetRequest(endpoint: string, signal?: AbortSignal, params?: any): Promise<any> { +        let url = !params ? Gateway.ConstructUrl(endpoint) :              (() => {                  let newUrl = new URL(Gateway.ConstructUrl(endpoint)); -                newUrl.searchParams.append("data", data); +                Object.getOwnPropertyNames(params).map(prop => +                    newUrl.searchParams.append(prop, params[prop]));                  return Gateway.ConstructUrl(endpoint) + newUrl.search; -                return newUrl as any;              })();          const response = await fetch(url, @@ -180,18 +180,18 @@ export class Gateway {      public static ConstructUrl(appendix: string): string { -        let base = Settings.Instance.ServerUrl; +        let base = NorthstarSettings.Instance.ServerUrl;          if (base.slice(-1) === "/") {              base = base.slice(0, -1);          } -        let url = base + "/" + Settings.Instance.ServerApiPath + "/" + appendix; +        let url = base + "/" + NorthstarSettings.Instance.ServerApiPath + "/" + appendix;          return url;      }  }  declare var ENV: any; -export class Settings { +export class NorthstarSettings {      private _environment: any;      @observable @@ -248,10 +248,10 @@ export class Settings {          return window.location.origin + "/";      } -    private static _instance: Settings; +    private static _instance: NorthstarSettings;      @action -    public Update(environment: any): void { +    public UpdateEnvironment(environment: any): void {          /*let serverParam = new URL(document.URL).searchParams.get("serverUrl");          if (serverParam) {              if (serverParam === "debug") { @@ -278,9 +278,9 @@ export class Settings {          this.DegreeOfParallelism = environment.DEGREE_OF_PARALLISM;      } -    public static get Instance(): Settings { +    public static get Instance(): NorthstarSettings {          if (!this._instance) { -            this._instance = new Settings(); +            this._instance = new NorthstarSettings();          }          return this._instance;      } diff --git a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts index 9671e55f8..a92412686 100644 --- a/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts +++ b/src/client/northstar/model/binRanges/VisualBinRangeHelper.ts @@ -4,7 +4,7 @@ import { NominalVisualBinRange } from "./NominalVisualBinRange";  import { QuantitativeVisualBinRange } from "./QuantitativeVisualBinRange";  import { AlphabeticVisualBinRange } from "./AlphabeticVisualBinRange";  import { DateTimeVisualBinRange } from "./DateTimeVisualBinRange"; -import { Settings } from "../../manager/Gateway"; +import { NorthstarSettings } from "../../manager/Gateway";  import { ModelHelpers } from "../ModelHelpers";  import { AttributeTransformationModel } from "../../core/attribute/AttributeTransformationModel"; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f38b8ca75..56669fb79 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,11 +1,9 @@ -import React = require('react'); -import { observer } from 'mobx-react'; -import { observable, action, computed } from 'mobx'; +import { computed, observable } from 'mobx';  import { Document } from "../../fields/Document"; -import { DocumentView } from '../views/nodes/DocumentView'; -import { KeyStore } from '../../fields/KeyStore';  import { FieldWaiting } from '../../fields/Field'; +import { KeyStore } from '../../fields/KeyStore';  import { ListField } from '../../fields/ListField'; +import { DocumentView } from '../views/nodes/DocumentView';  export class DocumentManager { @@ -27,11 +25,6 @@ export class DocumentManager {          // this.DocumentViews = new Array<DocumentView>();      } -    public getAllDocumentViews(collection: Document) { -        return this.DocumentViews.filter(dv => -            dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document === collection); -    } -      public getDocumentView(toFind: Document): DocumentView | null {          let toReturn: DocumentView | null; @@ -40,19 +33,24 @@ export class DocumentManager {          //gets document view that is in a freeform canvas collection          DocumentManager.Instance.DocumentViews.map(view => {              let doc = view.props.Document; -            // if (view.props.ContainingCollectionView instanceof CollectionFreeFormView) {              if (doc === toFind) {                  toReturn = view;                  return;              } -            let docSrc = doc.GetT(KeyStore.Prototype, Document); -            if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { -                toReturn = view; -            }          }); +        if (!toReturn) { +            DocumentManager.Instance.DocumentViews.map(view => { +                let doc = view.props.Document; + +                let docSrc = doc.GetT(KeyStore.Prototype, Document); +                if (docSrc && docSrc !== FieldWaiting && Object.is(docSrc, toFind)) { +                    toReturn = view; +                } +            }); +        } -        return (toReturn); +        return toReturn;      }      public getDocumentViews(toFind: Document): DocumentView[] { @@ -73,7 +71,7 @@ export class DocumentManager {              }          }); -        return (toReturn); +        return toReturn;      }      @computed @@ -85,9 +83,8 @@ export class DocumentManager {                      if (link instanceof Document) {                          let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);                          if (linkToDoc && linkToDoc !== FieldWaiting) { -                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => { -                                pairs.push({ a: dv, b: docView1, l: link }); -                            }); +                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => +                                pairs.push({ a: dv, b: docView1, l: link }));                          }                      }                      return pairs; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 4849ae9f7..46658867b 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -1,12 +1,14 @@  import { action } from "mobx";  import { Document } from "../../fields/Document"; +import { FieldWaiting } from "../../fields/Field"; +import { KeyStore } from "../../fields/KeyStore"; +import { emptyFunction } from "../../Utils";  import { CollectionDockingView } from "../views/collections/CollectionDockingView"; -import { CollectionView } from "../views/collections/CollectionView";  import { DocumentDecorations } from "../views/DocumentDecorations"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { returnFalse } from "../../Utils"; +import * as globalCssVariables from "../views/globalCssVariables.scss"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; -export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) { +export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: () => Document, moveFunc?: DragManager.MoveFunction, copyOnDrop: boolean = false) {      let onRowMove = action((e: PointerEvent): void => {          e.stopPropagation();          e.preventDefault(); @@ -38,6 +40,31 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:      return onItemDown;  } +export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Document) { +    let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document); +    let draggedDocs = (srcTarg && srcTarg !== FieldWaiting) ? +        srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => +            (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; +    let draggedFromDocs = (srcTarg && srcTarg !== FieldWaiting) ? +        srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => +            (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; +    draggedDocs.push(...draggedFromDocs); +    if (draggedDocs.length) { +        let moddrag = [] as Document[]; +        for (const draggedDoc of draggedDocs) { +            let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document); +            if (doc) moddrag.push(doc); +        } +        let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); +        DragManager.StartDocumentDrag([dragEle], dragData, x, y, { +            handlers: { +                dragComplete: action(emptyFunction), +            }, +            hideSource: false +        }); +    } +} +  export namespace DragManager {      export function Root() {          const root = document.getElementById("root"); @@ -112,11 +139,13 @@ export namespace DragManager {          constructor(dragDoc: Document[]) {              this.draggedDocuments = dragDoc;              this.droppedDocuments = dragDoc; +            this.xOffset = 0; +            this.yOffset = 0;          }          draggedDocuments: Document[];          droppedDocuments: Document[]; -        xOffset?: number; -        yOffset?: number; +        xOffset: number; +        yOffset: number;          aliasOnDrop?: boolean;          copyOnDrop?: boolean;          moveDocument?: MoveFunction; @@ -129,11 +158,11 @@ export namespace DragManager {      }      export class LinkDragData { -        constructor(linkSourceDoc: DocumentView) { -            this.linkSourceDocumentView = linkSourceDoc; +        constructor(linkSourceDoc: Document) { +            this.linkSourceDocument = linkSourceDoc;          }          droppedDocuments: Document[] = []; -        linkSourceDocumentView: DocumentView; +        linkSourceDocument: Document;          [id: string]: any;      } @@ -147,6 +176,7 @@ export namespace DragManager {              dragDiv.className = "dragManager-dragDiv";              DragManager.Root().appendChild(dragDiv);          } +        MainOverlayTextBox.Instance.SetTextDoc();          let scaleXs: number[] = [];          let scaleYs: number[] = []; @@ -175,7 +205,7 @@ export namespace DragManager {              dragElement.style.bottom = "";              dragElement.style.left = "0";              dragElement.style.transformOrigin = "0 0"; -            dragElement.style.zIndex = "1000"; +            dragElement.style.zIndex = globalCssVariables.contextMenuZindex;// "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`; @@ -220,11 +250,11 @@ export namespace DragManager {                  dragData.aliasOnDrop = e.ctrlKey || e.altKey;              }              if (e.shiftKey) { -                abortDrag(); +                AbortDrag();                  CollectionDockingView.Instance.StartOtherDrag(docs, {                      pageX: e.pageX,                      pageY: e.pageY, -                    preventDefault: () => { }, +                    preventDefault: emptyFunction,                      button: 0                  });              } @@ -239,20 +269,22 @@ export namespace DragManager {              );          }; -        const abortDrag = () => { +        AbortDrag = () => {              document.removeEventListener("pointermove", moveHandler, true);              document.removeEventListener("pointerup", upHandler); -            dragElements.map(dragElement => dragDiv.removeChild(dragElement)); +            dragElements.map(dragElement => { if (dragElement.parentNode == dragDiv) dragDiv.removeChild(dragElement); });              eles.map(ele => (ele.hidden = false));          };          const upHandler = (e: PointerEvent) => { -            abortDrag(); +            AbortDrag();              FinishDrag(eles, e, dragData, options, finishDrag);          };          document.addEventListener("pointermove", moveHandler, true);          document.addEventListener("pointerup", upHandler);      } +    export let AbortDrag: () => void = emptyFunction; +      function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {          let removed = dragEles.map(dragEle => {              let parent = dragEle.parentElement; diff --git a/src/client/util/ProsemirrorKeymap.ts b/src/client/util/ProsemirrorKeymap.ts new file mode 100644 index 000000000..00d086b97 --- /dev/null +++ b/src/client/util/ProsemirrorKeymap.ts @@ -0,0 +1,100 @@ +import { Schema } from "prosemirror-model"; +import { +    wrapIn, setBlockType, chainCommands, toggleMark, exitCode, +    joinUp, joinDown, lift, selectParentNode +} from "prosemirror-commands"; +import { wrapInList, splitListItem, liftListItem, sinkListItem } from "prosemirror-schema-list"; +import { undo, redo } from "prosemirror-history"; +import { undoInputRule } from "prosemirror-inputrules"; +import { Transaction, EditorState } from "prosemirror-state"; + +const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false; + +export type KeyMap = { [key: string]: any }; + +export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap { +    let keys: { [key: string]: any } = {}, type; + +    function bind(key: string, cmd: any) { +        if (mapKeys) { +            let mapped = mapKeys[key]; +            if (mapped === false) return; +            if (mapped) key = mapped; +        } +        keys[key] = cmd; +    } + +    bind("Mod-z", undo); +    bind("Shift-Mod-z", redo); +    bind("Backspace", undoInputRule); + +    if (!mac) { +        bind("Mod-y", redo); +    } + +    bind("Alt-ArrowUp", joinUp); +    bind("Alt-ArrowDown", joinDown); +    bind("Mod-BracketLeft", lift); +    bind("Escape", selectParentNode); + +    if (type = schema.marks.strong) { +        bind("Mod-b", toggleMark(type)); +        bind("Mod-B", toggleMark(type)); +    } +    if (type = schema.marks.em) { +        bind("Mod-i", toggleMark(type)); +        bind("Mod-I", toggleMark(type)); +    } +    if (type = schema.marks.code) { +        bind("Mod-`", toggleMark(type)); +    } + +    if (type = schema.nodes.bullet_list) { +        bind("Ctrl-b", wrapInList(type)); +    } +    if (type = schema.nodes.ordered_list) { +        bind("Ctrl-n", wrapInList(type)); +    } +    if (type = schema.nodes.blockquote) { +        bind("Ctrl->", wrapIn(type)); +    } +    if (type = schema.nodes.hard_break) { +        let br = type, cmd = chainCommands(exitCode, (state, dispatch) => { +            if (dispatch) { +                dispatch(state.tr.replaceSelectionWith(br.create()).scrollIntoView()); +                return true; +            } +            return false; +        }); +        bind("Mod-Enter", cmd); +        bind("Shift-Enter", cmd); +        if (mac) { +            bind("Ctrl-Enter", cmd); +        } +    } +    if (type = schema.nodes.list_item) { +        bind("Enter", splitListItem(type)); +        bind("Shift-Tab", liftListItem(type)); +        bind("Tab", sinkListItem(type)); +    } +    if (type = schema.nodes.paragraph) { +        bind("Shift-Ctrl-0", setBlockType(type)); +    } +    if (type = schema.nodes.code_block) { +        bind("Shift-Ctrl-\\", setBlockType(type)); +    } +    if (type = schema.nodes.heading) { +        for (let i = 1; i <= 6; i++) { +            bind("Shift-Ctrl-" + i, setBlockType(type, { level: i })); +        } +    } +    if (type = schema.nodes.horizontal_rule) { +        let hr = type; +        bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => { +            dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()); +            return true; +        }); +    } + +    return keys; +}
\ No newline at end of file diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index 92944bec0..9ef71e305 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -7,135 +7,134 @@ import { EditorState, Transaction, NodeSelection, } from "prosemirror-state";  import { EditorView, } from "prosemirror-view";  const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], -  preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; - +    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' -  }, -  //this doesn't currently work for some reason -  bullet_list: { -    ...bulletList, -    content: 'list_item+', -    group: 'block', -    // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], -    // toDOM() { return ulDOM } -  }, -  //bullet_list: { -  //  content: 'list_item+', -  // group: 'block', -  //active: blockActive(schema.nodes.bullet_list), -  //enable: wrapInList(schema.nodes.bullet_list), -  //run: wrapInList(schema.nodes.bullet_list), -  //select: state => true, -  // }, -  list_item: { -    ...listItem, -    content: 'paragraph block*' -  } +    // :: 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' +    }, +    //this doesn't currently work for some reason +    bullet_list: { +        ...bulletList, +        content: 'list_item+', +        group: 'block', +        // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], +        // toDOM() { return ulDOM } +    }, +    //bullet_list: { +    //  content: 'list_item+', +    // group: 'block', +    //active: blockActive(schema.nodes.bullet_list), +    //enable: wrapInList(schema.nodes.bullet_list), +    //run: wrapInList(schema.nodes.bullet_list), +    //select: state => true, +    // }, +    list_item: { +        ...listItem, +        content: 'paragraph block*' +    }  };  const emDOM: DOMOutputSpecArray = ["em", 0]; @@ -145,84 +144,186 @@ 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; } -  } +    // :: 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; } +    }, + + +    /* FONTS */ +    timesNewRoman: { +        parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: "Times New Roman", Times, serif;' +        }] +    }, + +    arial: { +        parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: Arial, Helvetica, sans-serif;' +        }] +    }, + +    georgia: { +        parseDOM: [{ style: 'font-family: Georgia, serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: Georgia, serif;' +        }] +    }, + +    comicSans: { +        parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: "Comic Sans MS", cursive, sans-serif;' +        }] +    }, + +    tahoma: { +        parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: Tahoma, Geneva, sans-serif;' +        }] +    }, + +    impact: { +        parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: Impact, Charcoal, sans-serif;' +        }] +    }, + +    crimson: { +        parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }], +        toDOM: () => ['span', { +            style: 'font-family: "Crimson Text", sans-serif;' +        }] +    }, + +    /** FONT SIZES */ + +    p10: { +        parseDOM: [{ style: 'font-size: 10px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 10px;' +        }] +    }, + +    p12: { +        parseDOM: [{ style: 'font-size: 12px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 12px;' +        }] +    }, + +    p16: { +        parseDOM: [{ style: 'font-size: 16px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 16px;' +        }] +    }, + +    p24: { +        parseDOM: [{ style: 'font-size: 24px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 24px;' +        }] +    }, + +    p32: { +        parseDOM: [{ style: 'font-size: 32px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 32px;' +        }] +    }, + +    p48: { +        parseDOM: [{ style: 'font-size: 48px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 48px;' +        }] +    }, + +    p72: { +        parseDOM: [{ style: 'font-size: 72px;' }], +        toDOM: () => ['span', { +            style: 'font-size: 72px;' +        }] +    },  };  // :: Schema diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index 9015f21cf..c67cc067a 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -54,15 +54,20 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an      let paramNames = ["KeyStore", "Documents", ...fieldTypes.map(fn => fn.name)];      let params: any[] = [KeyStore, Documents, ...fieldTypes];      let compiledFunction = new Function(...paramNames, `return ${script}`); +    let { capturedVariables = {} } = options;      let run = (args: { [name: string]: any } = {}): ScriptResult => {          let argsArray: any[] = [];          for (let name of customParams) {              if (name === "this") {                  continue;              } -            argsArray.push(args[name]); +            if (name in args) { +                argsArray.push(args[name]); +            } else { +                argsArray.push(capturedVariables[name]); +            }          } -        let thisParam = args.this; +        let thisParam = args.this || capturedVariables.this;          try {              const result = compiledFunction.apply(thisParam, params).apply(thisParam, argsArray);              return { success: true, result }; @@ -130,22 +135,30 @@ export interface ScriptOptions {      requiredType?: string;      addReturn?: boolean;      params?: { [name: string]: string }; +    capturedVariables?: { [name: string]: Field };  } -export function CompileScript(script: string, { requiredType = "", addReturn = false, params = {} }: ScriptOptions = {}): CompileResult { +export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult { +    const { requiredType = "", addReturn = false, params = {}, capturedVariables = {} } = options;      let host = new ScriptingCompilerHost; -    let paramArray: string[] = []; -    if ("this" in params) { -        paramArray.push("this"); +    let paramNames: string[] = []; +    if ("this" in params || "this" in capturedVariables) { +        paramNames.push("this");      }      for (const key in params) {          if (key === "this") continue; -        paramArray.push(key); +        paramNames.push(key);      } -    let paramString = paramArray.map(key => { +    let paramList = paramNames.map(key => {          const val = params[key];          return `${key}: ${val}`; -    }).join(", "); +    }); +    for (const key in capturedVariables) { +        if (key === "this") continue; +        paramNames.push(key); +        paramList.push(`${key}: ${capturedVariables[key].constructor.name}`); +    } +    let paramString = paramList.join(", ");      let funcScript = `(function(${paramString})${requiredType ? `: ${requiredType}` : ''} {          ${addReturn ? `return ${script};` : script}      })`; @@ -157,7 +170,7 @@ export function CompileScript(script: string, { requiredType = "", addReturn = f      let diagnostics = ts.getPreEmitDiagnostics(program).concat(testResult.diagnostics); -    return Run(outputText, paramArray, diagnostics, script, { requiredType, addReturn, params }); +    return Run(outputText, paramNames, diagnostics, script, options);  }  export function OrLiteralType(returnType: string): string { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 5ddaafc72..92d78696e 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,6 +2,8 @@ import { observable, action } from "mobx";  import { DocumentView } from "../views/nodes/DocumentView";  import { Document } from "../../fields/Document";  import { Main } from "../views/Main"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; +import { DragManager } from "./DragManager";  export namespace SelectionManager {      class Manager { @@ -17,14 +19,25 @@ export namespace SelectionManager {              if (manager.SelectedDocuments.indexOf(doc) === -1) {                  manager.SelectedDocuments.push(doc); -                doc.props.onActiveChanged(true); +                doc.props.whenActiveChanged(true);              }          }          @action          DeselectAll(): void { -            manager.SelectedDocuments.map(dv => dv.props.onActiveChanged(false)); +            manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));              manager.SelectedDocuments = []; +            MainOverlayTextBox.Instance.SetTextDoc(); +        } +        @action +        ReselectAll() { +            let sdocs = manager.SelectedDocuments.map(d => d); +            manager.SelectedDocuments = []; +            return sdocs; +        } +        @action +        ReselectAll2(sdocs: DocumentView[]) { +            sdocs.map(s => SelectionManager.SelectDoc(s, true));          }      } @@ -48,9 +61,12 @@ export namespace SelectionManager {          manager.DeselectAll();          if (found) manager.SelectDoc(found, false); -        Main.Instance.SetTextDoc(undefined, undefined);      } +    export function ReselectAll() { +        let sdocs = manager.ReselectAll(); +        manager.ReselectAll2(sdocs); +    }      export function SelectedDocuments(): Array<DocumentView> {          return manager.SelectedDocuments;      } diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx new file mode 100644 index 000000000..55e0eb909 --- /dev/null +++ b/src/client/util/TooltipLinkingMenu.tsx @@ -0,0 +1,86 @@ +import { action, IReactionDisposer, reaction } from "mobx"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands"; +import { history, redo, undo } from "prosemirror-history"; +import { keymap } from "prosemirror-keymap"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state"; +import { EditorView } from "prosemirror-view"; +import { schema } from "./RichTextSchema"; +import { Schema, NodeType, MarkType } from "prosemirror-model"; +import React = require("react"); +import "./TooltipTextMenu.scss"; +const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { +    faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; + +const SVG = "http://www.w3.org/2000/svg"; + +//appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc. +export class TooltipLinkingMenu { + +    private tooltip: HTMLElement; +    private view: EditorView; +    private editorProps: FieldViewProps; + +    constructor(view: EditorView, editorProps: FieldViewProps) { +        this.view = view; +        this.editorProps = editorProps; +        this.tooltip = document.createElement("div"); +        this.tooltip.className = "tooltipMenu linking"; + +        //add the div which is the tooltip +        view.dom.parentNode!.parentNode!.appendChild(this.tooltip); + +        let target = "https://www.google.com"; + +        let link = document.createElement("a"); +        link.href = target; +        link.textContent = target; +        link.target = "_blank"; +        link.style.color = "white"; +        this.tooltip.appendChild(link); + +        this.update(view, undefined); +    } + +    //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; +        } + +        console.log("STORED:"); +        console.log(state.doc.content.firstChild!.content); + +        // Otherwise, reposition it and update its content +        this.tooltip.style.display = ""; +        let { from, to } = state.selection; +        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) * this.editorProps.ScreenToLocalTransform().Scale + "px"; +        let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale; +        let mid = Math.min(start.left, end.left) + width; + +        this.tooltip.style.width = "auto"; +        this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; +    } + +    destroy() { this.tooltip.remove(); } +} diff --git a/src/client/util/TooltipTextMenu.scss b/src/client/util/TooltipTextMenu.scss index ea580d104..70d9ad772 100644 --- a/src/client/util/TooltipTextMenu.scss +++ b/src/client/util/TooltipTextMenu.scss @@ -1,8 +1,252 @@ -@import "../views/global_variables"; +@import "../views/globalCssVariables"; + +.ProseMirror-textblock-dropdown { +    min-width: 3em; +  } +   +  .ProseMirror-menu { +    margin: 0 -4px; +    line-height: 1; +  } +   +  .ProseMirror-tooltip .ProseMirror-menu { +    width: -webkit-fit-content; +    width: fit-content; +    white-space: pre; +  } +   +  .ProseMirror-menuitem { +    margin-right: 3px; +    display: inline-block; +  } +   +  .ProseMirror-menuseparator { +   // border-right: 1px solid #ddd; +    margin-right: 3px; +  } +   +  .ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu { +    font-size: 90%; +    white-space: nowrap; +  } +   +  .ProseMirror-menu-dropdown { +    vertical-align: 1px; +    cursor: pointer; +    position: relative; +    padding-right: 15px; +    margin: 3px; +    background: #333333; +    border-radius: 3px; +    text-align: center; +  } +   +  .ProseMirror-menu-dropdown-wrap { +    padding: 1px 0 1px 4px; +    display: inline-block; +    position: relative; +  } +   +  .ProseMirror-menu-dropdown:after { +    content: ""; +    border-left: 4px solid transparent; +    border-right: 4px solid transparent; +    border-top: 4px solid currentColor; +    opacity: .6; +    position: absolute; +    right: 4px; +    top: calc(50% - 2px); +  } +   +  .ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu { +    position: absolute; +    background: $dark-color; +    color:white; +    border: 1px solid rgb(223, 223, 223); +    padding: 2px; +  } +   +  .ProseMirror-menu-dropdown-menu { +    z-index: 15; +    min-width: 6em; +  } +   +  .linking { +      text-align: center; +  } + +  .ProseMirror-menu-dropdown-item { +    cursor: pointer; +    padding: 2px 8px 2px 4px; +    width: auto; +  } +   +  .ProseMirror-menu-dropdown-item:hover { +    background: #2e2b2b; +  } +   +  .ProseMirror-menu-submenu-wrap { +    position: relative; +    margin-right: -4px; +  } +   +  .ProseMirror-menu-submenu-label:after { +    content: ""; +    border-top: 4px solid transparent; +    border-bottom: 4px solid transparent; +    border-left: 4px solid currentColor; +    opacity: .6; +    position: absolute; +    right: 4px; +    top: calc(50% - 4px); +  } +   +  .ProseMirror-menu-submenu { +    display: none; +    min-width: 4em; +    left: 100%; +    top: -3px; +  } +   +  .ProseMirror-menu-active { +    background: #eee; +    border-radius: 4px; +  } +   +  .ProseMirror-menu-active { +    background: #eee; +    border-radius: 4px; +  } +   +  .ProseMirror-menu-disabled { +    opacity: .3; +  } +   +  .ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu { +    display: block; +  } +   +  .ProseMirror-menubar { +    border-top-left-radius: inherit; +    border-top-right-radius: inherit; +    position: relative; +    min-height: 1em; +    color: white; +    padding: 1px 6px; +    top: 0; left: 0; right: 0; +    border-bottom: 1px solid silver; +    background:$dark-color; +    z-index: 10; +    -moz-box-sizing: border-box; +    box-sizing: border-box; +    overflow: visible; +  } +   +  .ProseMirror-icon { +    display: inline-block; +    line-height: .8; +    vertical-align: -2px; /* Compensate for padding */ +    padding: 2px 8px; +    cursor: pointer; +  } +   +  .ProseMirror-menu-disabled.ProseMirror-icon { +    cursor: default; +  } +   +  .ProseMirror-icon svg { +    fill: currentColor; +    height: 1em; +  } +   +  .ProseMirror-icon span { +    vertical-align: text-top; +  }   +.ProseMirror-example-setup-style hr { +    padding: 2px 10px; +    border: none; +    margin: 1em 0; +  } +   +  .ProseMirror-example-setup-style hr:after { +    content: ""; +    display: block; +    height: 1px; +    background-color: silver; +    line-height: 2px; +  } +   +  .ProseMirror ul, .ProseMirror ol { +    padding-left: 30px; +  } +   +  .ProseMirror blockquote { +    padding-left: 1em; +    border-left: 3px solid #eee; +    margin-left: 0; margin-right: 0; +  } +   +  .ProseMirror-example-setup-style img { +    cursor: default; +  } +   +  .ProseMirror-prompt { +    background: white; +    padding: 5px 10px 5px 15px; +    border: 1px solid silver; +    position: fixed; +    border-radius: 3px; +    z-index: 11; +    box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2); +  } +   +  .ProseMirror-prompt h5 { +    margin: 0; +    font-weight: normal; +    font-size: 100%; +    color: #444; +  } +   +  .ProseMirror-prompt input[type="text"], +  .ProseMirror-prompt textarea { +    background: #eee; +    border: none; +    outline: none; +  } +   +  .ProseMirror-prompt input[type="text"] { +    padding: 0 4px; +  } +   +  .ProseMirror-prompt-close { +    position: absolute; +    left: 2px; top: 1px; +    color: #666; +    border: none; background: transparent; padding: 0; +  } +   +  .ProseMirror-prompt-close:after { +    content: "✕"; +    font-size: 12px; +  } +   +  .ProseMirror-invalid { +    background: #ffc; +    border: 1px solid #cc7; +    border-radius: 4px; +    padding: 5px 10px; +    position: absolute; +    min-width: 10em; +  } +   +  .ProseMirror-prompt-buttons { +    margin-top: 5px; +    display: none; +  }  .tooltipMenu {      position: absolute; -    z-index: 20; +    z-index: 200;      background: $dark-color;      border: 1px solid silver;      border-radius: 4px; @@ -10,6 +254,7 @@      margin-bottom: 7px;      -webkit-transform: translateX(-50%);      transform: translateX(-50%); +    pointer-events: all;  }  .tooltipMenu:before { @@ -39,7 +284,7 @@      display: inline-block;      border-right: 1px solid rgba(0, 0, 0, 0.2);      //color: rgb(19, 18, 18); -    color: $light-color; +    color: white;      line-height: 1;      padding: 0px 2px;      margin: 1px; @@ -52,4 +297,8 @@    .underline {text-decoration: underline}    .superscript {vertical-align:super}    .subscript { vertical-align:sub } -  .strikethrough {text-decoration-line:line-through}
\ No newline at end of file +  .strikethrough {text-decoration-line:line-through} +  .font-size-indicator { +      font-size: 12px; +      padding-right: 0px; +  } diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index bd5753093..4f0eb7d63 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -1,34 +1,55 @@  import { action, IReactionDisposer, reaction } from "mobx"; -import { baseKeymap } from "prosemirror-commands"; +import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css +import { baseKeymap, lift } from "prosemirror-commands";  import { history, redo, undo } from "prosemirror-history";  import { keymap } from "prosemirror-keymap"; -import { EditorState, Transaction, NodeSelection } from "prosemirror-state"; +import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";  import { EditorView } from "prosemirror-view";  import { schema } from "./RichTextSchema"; -import { Schema, NodeType } from "prosemirror-model"; +import { Schema, NodeType, MarkType } from "prosemirror-model";  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'; +import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list'; +import { +    faListUl, +} from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { FieldViewProps } from "../views/nodes/FieldView"; +import { throwStatement } from "babel-types"; +const SVG = "http://www.w3.org/2000/svg";  //appears above a selection of text in a RichTextBox to give user options such as Bold, Italics, etc.  export class TooltipTextMenu {      private tooltip: HTMLElement; +    private num_icons = 0; +    private view: EditorView; +    private fontStyles: MarkType[]; +    private fontSizes: MarkType[]; +    private editorProps: FieldViewProps; +    private state: EditorState; +    private fontSizeToNum: Map<MarkType, number>; +    private fontStylesToName: Map<MarkType, string>; +    private fontSizeIndicator: HTMLSpanElement = document.createElement("span"); +    //dropdown doms +    private fontSizeDom: Node; +    private fontStyleDom: Node; -    constructor(view: EditorView) { +    constructor(view: EditorView, editorProps: FieldViewProps) { +        this.view = view; +        this.state = view.state; +        this.editorProps = editorProps;          this.tooltip = document.createElement("div");          this.tooltip.className = "tooltipMenu";          //add the div which is the tooltip -        view.dom.parentNode!.appendChild(this.tooltip); +        view.dom.parentNode!.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") }, @@ -37,36 +58,155 @@ export class TooltipTextMenu {              { 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") }, -            //this doesn't work currently - look into notion of active block              { command: wrapInList(schema.nodes.bullet_list), dom: this.icon(":", "bullets") }, +            { command: lift, dom: this.icon("<", "lift") },          ]; -        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.target as Node)) { -                    let active = command(view.state, view.dispatch, view); -                    //uncomment this if we want the bullet button to disappear if current selection is bulleted -                    // dom.style.display = active ? "" : "none" -                } +        //add menu items +        items.forEach(({ dom, command }) => { +            this.tooltip.appendChild(dom); + +            //pointer down handler to activate button effects +            dom.addEventListener("pointerdown", e => { +                e.preventDefault(); +                view.focus(); +                command(view.state, view.dispatch, view);              }); +          }); +        //list of font styles +        this.fontStylesToName = new Map(); +        this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman"); +        this.fontStylesToName.set(schema.marks.arial, "Arial"); +        this.fontStylesToName.set(schema.marks.georgia, "Georgia"); +        this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS"); +        this.fontStylesToName.set(schema.marks.tahoma, "Tahoma"); +        this.fontStylesToName.set(schema.marks.impact, "Impact"); +        this.fontStylesToName.set(schema.marks.crimson, "Crimson Text"); +        this.fontStyles = Array.from(this.fontStylesToName.keys()); + +        //font sizes +        this.fontSizeToNum = new Map(); +        this.fontSizeToNum.set(schema.marks.p10, 10); +        this.fontSizeToNum.set(schema.marks.p12, 12); +        this.fontSizeToNum.set(schema.marks.p16, 16); +        this.fontSizeToNum.set(schema.marks.p24, 24); +        this.fontSizeToNum.set(schema.marks.p32, 32); +        this.fontSizeToNum.set(schema.marks.p48, 48); +        this.fontSizeToNum.set(schema.marks.p72, 72); +        this.fontSizes = Array.from(this.fontSizeToNum.keys()); + +        //this.addFontDropdowns(); +          this.update(view, undefined);      } +    //label of dropdown will change to given label +    updateFontSizeDropdown(label: string) { +        //filtering function - might be unecessary +        let cut = (arr: MenuItem[]) => arr.filter(x => x); + +        //font SIZES +        let fontSizeBtns: MenuItem[] = []; +        this.fontSizeToNum.forEach((number, mark) => { +            fontSizeBtns.push(this.dropdownBtn(String(number), "width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes)); +        }); + +        if (this.fontSizeDom) { this.tooltip.removeChild(this.fontSizeDom); } +        this.fontSizeDom = (new Dropdown(cut(fontSizeBtns), { +            label: label, +            css: "color:white; min-width: 60px; padding-left: 5px; margin-right: 0;" +        }) as MenuItem).render(this.view).dom; +        this.tooltip.appendChild(this.fontSizeDom); +    } + +    //label of dropdown will change to given label +    updateFontStyleDropdown(label: string) { +        //filtering function - might be unecessary +        let cut = (arr: MenuItem[]) => arr.filter(x => x); + +        //font STYLES +        let fontBtns: MenuItem[] = []; +        this.fontStylesToName.forEach((name, mark) => { +            fontBtns.push(this.dropdownBtn(name, "font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles)); +        }); + +        if (this.fontStyleDom) { this.tooltip.removeChild(this.fontStyleDom); } +        this.fontStyleDom = (new Dropdown(cut(fontBtns), { +            label: label, +            css: "color:white; width: 125px; margin-left: -3px; padding-left: 2px;" +        }) as MenuItem).render(this.view).dom; + +        this.tooltip.appendChild(this.fontStyleDom); +    } + +    //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text +    changeToMarkInGroup(markType: MarkType, view: EditorView, fontMarks: MarkType[]) { +        let { empty, $cursor, ranges } = view.state.selection as TextSelection; +        let state = view.state; +        let dispatch = view.dispatch; + +        //remove all other active font marks +        fontMarks.forEach((type) => { +            if (dispatch) { +                if ($cursor) { +                    if (type.isInSet(state.storedMarks || $cursor.marks())) { +                        dispatch(state.tr.removeStoredMark(type)); +                    } +                } else { +                    let has = false, tr = state.tr; +                    for (let i = 0; !has && i < ranges.length; i++) { +                        let { $from, $to } = ranges[i]; +                        has = state.doc.rangeHasMark($from.pos, $to.pos, type); +                    } +                    for (let i of ranges) { +                        let { $from, $to } = i; +                        if (has) { +                            toggleMark(type)(view.state, view.dispatch, view); +                        } +                    } +                } +            } +        }); //actually apply font +        return toggleMark(markType)(view.state, view.dispatch, view); +    } + +    //makes a button for the drop down +    //css is the style you want applied to the button +    dropdownBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) { +        return new MenuItem({ +            title: "", +            label: label, +            execEvent: "", +            class: "menuicon", +            css: css, +            enable(state) { return true; }, +            run() { +                changeToMarkInGroup(markType, view, groupMarks); +            } +        }); +    }      // Helper function to create menu icons      icon(text: string, name: string) {          let span = document.createElement("span"); -        span.className = "menuicon " + name; +        span.className = name + " menuicon";          span.title = name;          span.textContent = text; +        span.style.color = "white";          return span;      } +    //method for checking whether node can be inserted +    canInsert(state: EditorState, nodeType: NodeType<Schema<string, string>>) { +        let $from = state.selection.$from; +        for (let d = $from.depth; d >= 0; d--) { +            let index = $from.index(d); +            if ($from.node(d).canReplaceWith(index, index, nodeType)) return true; +        } +        return false; +    } + +      //adapted this method - use it to check if block has a tag (ie bulleting)      blockActive(type: NodeType<Schema<string, string>>, state: EditorState) {          let attrs = {}; @@ -85,15 +225,6 @@ export class TooltipTextMenu {          }      } -    //this doesn't currently work but could be used to use icons for buttons -    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 { @@ -124,14 +255,58 @@ export class TooltipTextMenu {          // 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; +        this.tooltip.style.left = (left - box.left) * this.editorProps.ScreenToLocalTransform().Scale + "px"; +        let width = Math.abs(start.left - end.left) / 2 * this.editorProps.ScreenToLocalTransform().Scale;          let mid = Math.min(start.left, end.left) + width; -        //THIS WIDTH IS 15 * NUMBER OF ICONS + 15 -        this.tooltip.style.width = 122 + "px"; -        this.tooltip.style.bottom = (box.bottom - start.top) + "px"; +        this.tooltip.style.width = 225 + "px"; +        this.tooltip.style.bottom = (box.bottom - start.top) * this.editorProps.ScreenToLocalTransform().Scale + "px"; + +        //UPDATE FONT STYLE DROPDOWN +        let activeStyles = this.activeMarksOnSelection(this.fontStyles); +        if (activeStyles.length === 1) { +            // if we want to update something somewhere with active font name +            let fontName = this.fontStylesToName.get(activeStyles[0]); +            if (fontName) { this.updateFontStyleDropdown(fontName); } +        } else if (activeStyles.length === 0) { +            //crimson on default +            this.updateFontStyleDropdown("Crimson Text"); +        } else { +            this.updateFontStyleDropdown("Various"); +        } + +        //UPDATE FONT SIZE DROPDOWN +        let activeSizes = this.activeMarksOnSelection(this.fontSizes); +        if (activeSizes.length === 1) { //if there's only one active font size +            let size = this.fontSizeToNum.get(activeSizes[0]); +            if (size) { this.updateFontSizeDropdown(String(size) + " pt"); } +        } else if (activeSizes.length === 0) { +            //should be 14 on default   +            this.updateFontSizeDropdown("14 pt"); +        } else { //multiple font sizes selected +            this.updateFontSizeDropdown("Various"); +        } +    } + +    //finds all active marks on selection +    activeMarksOnSelection(markGroup: MarkType[]) { +        //current selection +        let { empty, $cursor, ranges } = this.view.state.selection as TextSelection; +        let state = this.view.state; +        let dispatch = this.view.dispatch; + +        let activeMarks = markGroup.filter(mark => { +            if (dispatch) { +                let has = false, tr = state.tr; +                for (let i = 0; !has && i < ranges.length; i++) { +                    let { $from, $to } = ranges[i]; +                    return state.doc.rangeHasMark($from.pos, $to.pos, mark); +                } +            } +            return false; +        }); +        return activeMarks;      }      destroy() { this.tooltip.remove(); } -}
\ No newline at end of file +} diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index f6830d9cd..fe884ca85 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,8 +1,8 @@ -@import "global_variables"; +@import "globalCssVariables";  .contextMenu-cont {    position: absolute;    display: flex; -  z-index: 1000; +  z-index: $contextMenu-zindex;    box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;    flex-direction: column;  } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index c4e4aed8e..f78bf9ff8 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,13 +1,16 @@ -@import "global_variables"; +@import "globalCssVariables"; -#documentDecorations-container { +.documentDecorations { +    position: absolute; +} +.documentDecorations-container { +    z-index: $docDecorations-zindex;      position: absolute;      top: 0;      left:0;      display: grid; -    z-index: 1000;      grid-template-rows: 20px 8px 1fr 8px; -    grid-template-columns: 8px 8px 1fr 8px 8px; +    grid-template-columns: 8px 16px 1fr 8px 8px;      pointer-events: none;      #documentDecorations-centerCont { @@ -75,6 +78,7 @@      grid-column-end: 6;      pointer-events: all;      text-align: center; +    cursor: pointer;  }  .documentDecorations-minimizeButton {      background:$alt-accent; @@ -83,6 +87,12 @@      grid-column-end: 3;      pointer-events: all;      text-align: center; +    cursor: pointer; +    position: absolute; +    left: 0px; +    top: 0px; +    width: $MINIMIZED_ICON_SIZE; +    height: $MINIMIZED_ICON_SIZE;  }  .documentDecorations-background {      background: lightblue; @@ -90,37 +100,6 @@      opacity: 0.1;  } -//     position: absolute; -//     display: grid; -//     z-index: 1000; -//     grid-template-rows: 20px 1fr 20px 0px; -//     grid-template-columns: 20px 1fr 20px; -//     pointer-events: none; -//     #documentDecorations-centerCont { -//         background: none; -//     } -//     .documentDecorations-resizer { -//         pointer-events: auto; -//         background: lightblue; -//         opacity: 0.4; -//     } -//     #documentDecorations-topLeftResizer, -//     #documentDecorations-bottomRightResizer { -//         cursor: nwse-resize; -//     } -//     #documentDecorations-topRightResizer, -//     #documentDecorations-bottomLeftResizer { -//         cursor: nesw-resize; -//     } -//     #documentDecorations-topResizer, -//     #documentDecorations-bottomResizer { -//         cursor: ns-resize; -//     } -//     #documentDecorations-leftResizer, -//     #documentDecorations-rightResizer { -//         cursor: ew-resize; -//     } -// }  .linkFlyout {      grid-column: 1/4;      margin-left: 25px; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 28af46358..32cf985ce 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,20 +1,23 @@ -import { action, computed, observable, trace, runInAction } from "mobx"; +import { action, computed, observable, runInAction } from "mobx";  import { observer } from "mobx-react";  import { Key } from "../../fields/Key"; -//import ContentEditable from 'react-contenteditable'  import { KeyStore } from "../../fields/KeyStore";  import { ListField } from "../../fields/ListField";  import { NumberField } from "../../fields/NumberField"; -import { Document } from "../../fields/Document";  import { TextField } from "../../fields/TextField"; -import { DragManager } from "../util/DragManager"; +import { Document } from "../../fields/Document"; +import { emptyFunction } from "../../Utils"; +import { DragLinksAsDocuments, DragManager } from "../util/DragManager";  import { SelectionManager } from "../util/SelectionManager"; -import { CollectionView } from "./collections/CollectionView"; +import { undoBatch } from "../util/UndoManager";  import './DocumentDecorations.scss'; +import { MainOverlayTextBox } from "./MainOverlayTextBox";  import { DocumentView } from "./nodes/DocumentView";  import { LinkMenu } from "./nodes/LinkMenu";  import React = require("react"); -import { FieldWaiting } from "../../fields/Field"; +import { CompileScript } from "../util/Scripting"; +import { IconBox } from "./nodes/IconBox"; +import { FieldValue, Field } from "../../fields/Field";  const higflyout = require("@hig/flyout");  export const { anchorPoints } = higflyout;  export const Flyout = higflyout.default; @@ -31,12 +34,17 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>      private _titleHeight = 20;      private _linkButton = React.createRef<HTMLDivElement>();      private _linkerButton = React.createRef<HTMLDivElement>(); +    private _downX = 0; +    private _downY = 0; +    @observable private _minimizedX = 0; +    @observable private _minimizedY = 0;      //@observable private _title: string = this._documents[0].props.Document.Title;      @observable private _title: string = this._documents.length > 0 ? this._documents[0].props.Document.Title : "";      @observable private _fieldKey: Key = KeyStore.Title;      @observable private _hidden = false;      @observable private _opacity = 1;      @observable private _dragging = false; +    @observable private _iconifying = false;      constructor(props: Readonly<{}>) { @@ -70,7 +78,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  // TODO: Change field with switch statement              }              else { -                this._title = "changed"; +                if (this._documents.length > 0) { +                    let field = this._documents[0].props.Document.Get(this._fieldKey); +                    if (field instanceof TextField) { +                        this._documents.forEach(d => +                            d.props.Document.Set(this._fieldKey, new TextField(this._title))); +                    } +                    else if (field instanceof NumberField) { +                        this._documents.forEach(d => +                            d.props.Document.Set(this._fieldKey, new NumberField(+this._title))); +                    } +                    this._title = "changed"; +                }              }              e.target.blur();          } @@ -106,7 +125,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          document.addEventListener("pointerup", this.onBackgroundUp);          this._lastDrag = [e.clientX, e.clientY];          e.stopPropagation(); -        e.preventDefault(); +        if (e.currentTarget.localName !== "input") { +            e.preventDefault(); +        }      }      @action @@ -122,7 +143,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          this._dragging = true;          document.removeEventListener("pointermove", this.onBackgroundMove);          document.removeEventListener("pointerup", this.onBackgroundUp); -        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentRef.current!), dragData, e.x, e.y, { +        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, {              handlers: {                  dragComplete: action(() => this._dragging = false),              }, @@ -152,6 +173,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          if (e.button === 0) {          }      } +    @undoBatch      @action      onCloseUp = (e: PointerEvent): void => {          e.stopPropagation(); @@ -162,28 +184,76 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>              document.removeEventListener("pointerup", this.onCloseUp);          }      } +    @action      onMinimizeDown = (e: React.PointerEvent): void => {          e.stopPropagation();          if (e.button === 0) { +            this._downX = e.pageX; +            this._downY = e.pageY; +            let selDoc = SelectionManager.SelectedDocuments()[0]; +            let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); +            this._minimizedX = selDocPos[0] + 12; +            this._minimizedY = selDocPos[1] + 12;              document.removeEventListener("pointermove", this.onMinimizeMove);              document.addEventListener("pointermove", this.onMinimizeMove);              document.removeEventListener("pointerup", this.onMinimizeUp);              document.addEventListener("pointerup", this.onMinimizeUp);          }      } + +    @action      onMinimizeMove = (e: PointerEvent): void => {          e.stopPropagation(); -        if (e.button === 0) { +        let moved = Math.abs(e.pageX - this._downX) > 4 || Math.abs(e.pageY - this._downY) > 4; +        if (moved) { +            let selDoc = SelectionManager.SelectedDocuments()[0]; +            let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); +            let snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20; +            this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX; +            this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY; +            let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); +            Promise.all(selectedDocs.map(async selDoc => await selDoc.getIconDoc())).then(minDocSet => +                this.moveIconDocs(SelectionManager.SelectedDocuments()) +            ); +            this._iconifying = snapped;          }      } +    @action      onMinimizeUp = (e: PointerEvent): void => {          e.stopPropagation();          if (e.button === 0) { -            SelectionManager.SelectedDocuments().map(dv => dv.minimize());              document.removeEventListener("pointermove", this.onMinimizeMove);              document.removeEventListener("pointerup", this.onMinimizeUp); +            let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); +            Promise.all(selectedDocs.map(async selDoc => await selDoc.getIconDoc())).then(minDocSet => { +                let minDocs = minDocSet.filter(minDoc => minDoc instanceof Document).map(minDoc => minDoc as Document); +                minDocs.map(minDoc => { +                    minDoc.SetNumber(KeyStore.X, minDocs[0].GetNumber(KeyStore.X, 0)); +                    minDoc.SetNumber(KeyStore.Y, minDocs[0].GetNumber(KeyStore.Y, 0)); +                    minDoc.SetData(KeyStore.LinkTags, minDocs, ListField); +                    if (this._iconifying && selectedDocs[0].props.removeDocument) { +                        selectedDocs[0].props.removeDocument(minDoc); +                        (minDoc.Get(KeyStore.MaximizedDoc, false) as Document)!.Set(KeyStore.MinimizedDoc, undefined); +                    } +                }); +                runInAction(() => this._minimizedX = this._minimizedY = 0); +                if (!this._iconifying) selectedDocs[0].toggleIcon(); +                this._iconifying = false; +            });          }      } +    moveIconDocs(selViews: DocumentView[], minDocSet?: FieldValue<Field>[]) { +        selViews.map(selDoc => { +            let minDoc = selDoc.props.Document.Get(KeyStore.MinimizedDoc); +            if (minDoc instanceof Document) { +                let zoom = selDoc.props.Document.GetNumber(KeyStore.Zoom, 1); +                let where = (selDoc.props.ScreenToLocalTransform()).scale(selDoc.props.ContentScaling()).scale(1 / zoom). +                    transformPoint(this._minimizedX - 12, this._minimizedY - 12); +                minDoc.SetNumber(KeyStore.X, where[0] + selDoc.props.Document.GetNumber(KeyStore.X, 0)); +                minDoc.SetNumber(KeyStore.Y, where[1] + selDoc.props.Document.GetNumber(KeyStore.Y, 0)); +            } +        }); +    }      onPointerDown = (e: React.PointerEvent): void => {          e.stopPropagation(); @@ -214,10 +284,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>          if (this._linkerButton.current !== null) {              document.removeEventListener("pointermove", this.onLinkerButtonMoved);              document.removeEventListener("pointerup", this.onLinkerButtonUp); -            let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]); +            let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0].props.Document);              DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {                  handlers: { -                    dragComplete: action(() => { }), +                    dragComplete: action(emptyFunction),                  },                  hideSource: false              }); @@ -240,38 +310,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>      }      onLinkButtonMoved = async (e: PointerEvent) => { -        if (this._linkButton.current !== null) { +        if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {              document.removeEventListener("pointermove", this.onLinkButtonMoved);              document.removeEventListener("pointerup", this.onLinkButtonUp); -            let sourceDoc = SelectionManager.SelectedDocuments()[0].props.Document; -            let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document); -            let draggedDocs = (srcTarg && srcTarg !== FieldWaiting) ? -                srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => -                    (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; -            let draggedFromDocs = (srcTarg && srcTarg !== FieldWaiting) ? -                srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => -                    (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; -            draggedDocs.push(...draggedFromDocs); -            if (draggedDocs.length) { -                let moddrag = [] as Document[]; -                for (const draggedDoc of draggedDocs) { -                    let doc = await draggedDoc.GetTAsync(KeyStore.AnnotationOn, Document); -                    if (doc) moddrag.push(doc); -                } -                let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); -                DragManager.StartDocumentDrag([this._linkButton.current], dragData, e.x, e.y, { -                    handlers: { -                        dragComplete: action(() => { }), -                    }, -                    hideSource: false -                }); -            } +            DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);          }          e.stopPropagation();      } -      onPointerMove = (e: PointerEvent): void => {          e.stopPropagation();          e.preventDefault(); @@ -320,8 +367,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  break;          } +        MainOverlayTextBox.Instance.SetTextDoc();          SelectionManager.SelectedDocuments().forEach(element => { -            const rect = element.screenRect(); +            const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); +              if (rect.width !== 0) {                  let doc = element.props.Document;                  let width = doc.GetNumber(KeyStore.Width, 0); @@ -380,11 +429,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>      // }      render() {          var bounds = this.Bounds; -        if (bounds.x === Number.MAX_VALUE) { +        let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined; +        if (bounds.x === Number.MAX_VALUE || !seldoc) {              return (null);          } -        // console.log(this._documents.length) -        // let test = this._documents[0].props.Document.Title; +        let minimizeIcon = ( +            <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}> +                {SelectionManager.SelectedDocuments().length == 1 ? IconBox.DocumentIcon(SelectionManager.SelectedDocuments()[0].props.Document.GetText(KeyStore.Layout, "...")) : "..."} +            </div>); +          if (this.Hidden) {              return (null);          } @@ -416,15 +469,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>                  zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,              }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >              </div> -            <div id="documentDecorations-container" style={{ +            <div className="documentDecorations-container" style={{                  width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",                  height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px",                  left: bounds.x - this._resizeBorderWidth / 2,                  top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,                  opacity: this._opacity              }}> -                <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>...</div> -                <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} /> +                {minimizeIcon} + +                <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onBackgroundDown} onKeyPress={this.enterPressed} />                  <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>                  <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>                  <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div> diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index be3c5069a..ea401eaf9 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -2,5 +2,4 @@      overflow-wrap: break-word;      word-wrap: break-word;      hyphens: auto; -    max-width: 300px;  }
\ No newline at end of file diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss index 42ae38c73..2c550051c 100644 --- a/src/client/views/InkingCanvas.scss +++ b/src/client/views/InkingCanvas.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables";  .inkingCanvas {      opacity:0.99; diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss index 0d8fd8784..ba4ec41af 100644 --- a/src/client/views/InkingControl.scss +++ b/src/client/views/InkingControl.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables";  .inking-control {      position: absolute;      left: 70px; diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index fe7f007b0..4373534b2 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -1,4 +1,4 @@ -@import "global_variables"; +@import "globalCssVariables";  @import "nodeModuleOverrides";  html,  body { @@ -168,23 +168,6 @@ button:hover {      left:0;      overflow: scroll;  } -.mainDiv-textInput { -    background:pink; -        width: 200px; -        height: 200px; -        position:absolute; -        overflow: visible; -        top: 0; -        left: 0; -        .formattedTextBox-cont { -            background:pink; -            width: 100%; -            height: 100%; -            position:absolute; -            top: 0; -            left: 0; -    } -}  #mainContent-div {      width:100%;      height:100%; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index d8e97fe72..175ef3c6d 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,7 +1,7 @@  import { IconName, library } from '@fortawesome/fontawesome-svg-core';  import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, configure, observable, runInAction, trace } from 'mobx'; +import { action, computed, configure, observable, runInAction } from 'mobx';  import { observer } from 'mobx-react';  import "normalize.css";  import * as React from 'react'; @@ -9,26 +9,25 @@ import * as ReactDOM from 'react-dom';  import Measure from 'react-measure';  import * as request from 'request';  import { Document } from '../../fields/Document'; -import { Field, FieldWaiting, Opt } from '../../fields/Field'; +import { Field, FieldWaiting, Opt, FIELD_WAITING } from '../../fields/Field';  import { KeyStore } from '../../fields/KeyStore';  import { ListField } from '../../fields/ListField';  import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu';  import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';  import { MessageStore } from '../../server/Message'; -import { Utils, returnTrue, emptyFunction } from '../../Utils'; -import * as rp from 'request-promise';  import { RouteStore } from '../../server/RouteStore';  import { ServerUtils } from '../../server/ServerUtil'; +import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne } from '../../Utils';  import { Documents } from '../documents/Documents';  import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel';  import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; -import { Gateway, Settings } from '../northstar/manager/Gateway'; +import { Gateway, NorthstarSettings } from '../northstar/manager/Gateway';  import { AggregateFunction, Catalog } from '../northstar/model/idea/idea';  import '../northstar/model/ModelExtensions';  import { HistogramOperation } from '../northstar/operations/HistogramOperation';  import '../northstar/utils/Extensions';  import { Server } from '../Server'; -import { setupDrag } from '../util/DragManager'; +import { SetupDrag, DragManager } from '../util/DragManager';  import { Transform } from '../util/Transform';  import { UndoManager } from '../util/UndoManager';  import { CollectionDockingView } from './collections/CollectionDockingView'; @@ -36,34 +35,26 @@ import { ContextMenu } from './ContextMenu';  import { DocumentDecorations } from './DocumentDecorations';  import { InkingControl } from './InkingControl';  import "./Main.scss"; +import { MainOverlayTextBox } from './MainOverlayTextBox';  import { DocumentView } from './nodes/DocumentView'; -import { FormattedTextBox } from './nodes/FormattedTextBox'; +import { PreviewCursor } from './PreviewCursor'; +import { SelectionManager } from '../util/SelectionManager'; +  @observer  export class Main extends React.Component { -    // dummy initializations keep the compiler happy -    @observable private mainfreeform?: Document; +    public static Instance: Main; +    @observable private _workspacesShown: boolean = false;      @observable public pwidth: number = 0;      @observable public pheight: number = 0; -    private _northstarSchemas: Document[] = []; -    @computed private get mainContainer(): Document | undefined { -        let doc = this.userDocument.GetT(KeyStore.ActiveWorkspace, Document); -        return doc === FieldWaiting ? undefined : doc; +    @computed private get mainContainer(): Document | undefined | FIELD_WAITING { +        return CurrentUserUtils.UserDocument.GetT(KeyStore.ActiveWorkspace, Document);      } - -    private set mainContainer(doc: Document | undefined) { -        if (doc) { -            this.userDocument.Set(KeyStore.ActiveWorkspace, doc); -        } -    } - -    private get userDocument(): Document { -        return CurrentUserUtils.UserDocument; +    private set mainContainer(doc: Document | undefined | FIELD_WAITING) { +        doc && CurrentUserUtils.UserDocument.Set(KeyStore.ActiveWorkspace, doc);      } -    public static Instance: Main; -      constructor(props: Readonly<{}>) {          super(props);          Main.Instance = this; @@ -94,9 +85,17 @@ export class Main extends React.Component {          this.initEventListeners();          this.initAuthenticationRouters(); -        this.initializeNorthstar(); +        try { +            this.initializeNorthstar(); +        } catch (e) { + +        }      } +    componentDidMount() { window.onpopstate = this.onHistory; } + +    componentWillUnmount() { window.onpopstate = null; } +      onHistory = () => {          if (window.location.pathname !== RouteStore.home) {              let pathname = window.location.pathname.split("/"); @@ -109,18 +108,16 @@ export class Main extends React.Component {          }      } -    componentDidMount() { -        window.onpopstate = this.onHistory; -    } - -    componentWillUnmount() { -        window.onpopstate = null; -    } -      initEventListeners = () => {          // window.addEventListener("pointermove", (e) => this.reportLocation(e))          window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler          window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler +        window.addEventListener("keydown", (e) => { +            if (e.key == "Escape") { +                DragManager.AbortDrag(); +                SelectionManager.DeselectAll() +            } +        }, false); // drag event handler          // click interactions for the context menu          document.addEventListener("pointerdown", action(function (e: PointerEvent) {              if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { @@ -132,7 +129,7 @@ export class Main extends React.Component {      initAuthenticationRouters = () => {          // Load the user's active workspace, or create a new one if initial session after signup          if (!CurrentUserUtils.MainDocId) { -            this.userDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => { +            CurrentUserUtils.UserDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => {                  if (doc) {                      CurrentUserUtils.MainDocId = doc.Id;                      this.openWorkspace(doc); @@ -141,19 +138,15 @@ export class Main extends React.Component {                  }              });          } else { -            Server.GetField(CurrentUserUtils.MainDocId).then(field => { -                if (field instanceof Document) { -                    this.openWorkspace(field); -                } else { -                    this.createNewWorkspace(CurrentUserUtils.MainDocId); -                } -            }); +            Server.GetField(CurrentUserUtils.MainDocId).then(field => +                field instanceof Document ? this.openWorkspace(field) : +                    this.createNewWorkspace(CurrentUserUtils.MainDocId));          }      }      @action      createNewWorkspace = (id?: string): void => { -        this.userDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => { +        CurrentUserUtils.UserDocument.GetTAsync<ListField<Document>>(KeyStore.Workspaces, ListField).then(action((list: Opt<ListField<Document>>) => {              if (list) {                  let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });                  var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; @@ -174,82 +167,48 @@ export class Main extends React.Component {      openWorkspace = (doc: Document, fromHistory = false): void => {          this.mainContainer = doc;          fromHistory || window.history.pushState(null, doc.Title, "/doc/" + doc.Id); -        this.userDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col => { +        CurrentUserUtils.UserDocument.GetTAsync(KeyStore.OptionalRightCollection, Document).then(col =>              // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized) -            setTimeout(() => { -                if (col) { -                    col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => { -                        if (f && f.Data.length > 0) { -                            CollectionDockingView.Instance.AddRightSplit(col); -                        } -                    }); -                } -            }, 100); -        }); -    } - -    @observable -    workspacesShown: boolean = false; - -    areWorkspacesShown = () => this.workspacesShown; -    @action -    toggleWorkspaces = () => { -        this.workspacesShown = !this.workspacesShown; -    } - -    pwidthFunc = () => this.pwidth; -    pheightFunc = () => this.pheight; -    focusDocument = (doc: Document) => { }; -    noScaling = () => 1; - -    @observable _textDoc?: Document = undefined; -    _textRect: any; -    @action -    SetTextDoc(textDoc?: Document, div?: HTMLDivElement) { -        this._textDoc = undefined; -        this._textDoc = textDoc; -        if (div) { -            this._textRect = div.getBoundingClientRect(); -        } -    } - -    @computed -    get activeTextBox() { -        if (this._textDoc) { -            let x: number = this._textRect.x; -            let y: number = this._textRect.y; -            let w: number = this._textRect.width; -            let h: number = this._textRect.height; -            return <div className="mainDiv-textInput" style={{ transform: `translate(${x}px, ${y}px)`, width: `${w}px`, height: `${h}px` }} > -                <FormattedTextBox fieldKey={KeyStore.Archives} Document={this._textDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true} onActiveChanged={emptyFunction} active={returnTrue} ScreenToLocalTransform={Transform.Identity} focus={(doc) => { }} /> -            </ div>; -        } -        else return (null); +            setTimeout(() => +                col && col.GetTAsync<ListField<Document>>(KeyStore.Data, ListField, (f: Opt<ListField<Document>>) => +                    f && f.Data.length > 0 && CollectionDockingView.Instance.AddRightSplit(col)) +                , 100) +        );      }      @computed      get mainContent() { -        return !this.mainContainer ? (null) : -            <DocumentView Document={this.mainContainer} -                addDocument={undefined} -                removeDocument={undefined} -                ScreenToLocalTransform={Transform.Identity} -                ContentScaling={this.noScaling} -                PanelWidth={this.pwidthFunc} -                PanelHeight={this.pheightFunc} -                isTopMost={true} -                selectOnLoad={false} -                focus={this.focusDocument} -                parentActive={returnTrue} -                onActiveChanged={emptyFunction} -                ContainingCollectionView={undefined} />; +        let pwidthFunc = () => this.pwidth; +        let pheightFunc = () => this.pheight; +        let noScaling = () => 1; +        let mainCont = this.mainContainer; +        return <Measure onResize={action((r: any) => { this.pwidth = r.entry.width; this.pheight = r.entry.height; })}> +            {({ measureRef }) => +                <div ref={measureRef} id="mainContent-div"> +                    {!mainCont ? (null) : +                        <DocumentView Document={mainCont} +                            addDocument={undefined} +                            removeDocument={undefined} +                            ScreenToLocalTransform={Transform.Identity} +                            ContentScaling={noScaling} +                            PanelWidth={pwidthFunc} +                            PanelHeight={pheightFunc} +                            isTopMost={true} +                            selectOnLoad={false} +                            focus={emptyDocFunction} +                            parentActive={returnTrue} +                            whenActiveChanged={emptyFunction} +                            ContainingCollectionView={undefined} />} +                </div> +            } +        </Measure>;      }      /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */      @computed      get nodesMenu() {          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 pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";          let weburl = "https://cs.brown.edu/courses/cs166/";          let audiourl = "http://techslides.com/demos/samples/sample.mp3";          let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; @@ -258,9 +217,9 @@ export class Main extends React.Component {          let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));          let addSchemaNode = action(() => Documents.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" }));          let addTreeNode = action(() => Documents.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", copyDraggedItems: true })); -        let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" })); -        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 addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, title: "video node" })); +        let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" })); +        let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));          let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));          let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })); @@ -284,7 +243,7 @@ export class Main extends React.Component {                  <ul id="add-options-list">                      {btns.map(btn =>                          <li key={btn[1]} ><div ref={btn[0]}> -                            <button className="round-button add-button" title={btn[2]} onPointerDown={setupDrag(btn[0], btn[3])}> +                            <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>                                  <FontAwesomeIcon icon={btn[1]} size="sm" />                              </button>                          </div></li>)} @@ -298,6 +257,7 @@ export class Main extends React.Component {      get miscButtons() {          let workspacesRef = React.createRef<HTMLDivElement>();          let logoutRef = React.createRef<HTMLDivElement>(); +        let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown);          let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}));          return [ @@ -308,55 +268,53 @@ export class Main extends React.Component {                  <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>              </div >,              <div className="main-buttonDiv" key="workspaces" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}> -                <button onClick={this.toggleWorkspaces}>Workspaces</button></div>, +                <button onClick={toggleWorkspaces}>Workspaces</button></div>,              <div className="main-buttonDiv" key="logout" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}> -                <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div> +                <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>          ];      } +<<<<<<< HEAD +======= +    @computed +    get workspaceMenu() { +        let areWorkspacesShown = () => this._workspacesShown; +        let toggleWorkspaces = () => runInAction(() => this._workspacesShown = !this._workspacesShown); +        let workspaces = CurrentUserUtils.UserDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField); +        return (!workspaces || workspaces === FieldWaiting || this.mainContainer === FieldWaiting) ? (null) : +            <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} +                new={this.createNewWorkspace} allWorkspaces={workspaces.Data} +                isShown={areWorkspacesShown} toggle={toggleWorkspaces} />; +    } +>>>>>>> e47656cdc18aa1fd801a3853fa0f819140a68646      render() { -        let workspaceMenu: any = null; -        let workspaces = this.userDocument.GetT<ListField<Document>>(KeyStore.Workspaces, ListField); -        if (workspaces && workspaces !== FieldWaiting) { -            workspaceMenu = <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={workspaces.Data} -                isShown={this.areWorkspacesShown} toggle={this.toggleWorkspaces} />; -        }          return ( -            <> -                <div id="main-div"> -                    <DocumentDecorations /> -                    <Measure onResize={(r: any) => runInAction(() => { -                        this.pwidth = r.entry.width; -                        this.pheight = r.entry.height; -                    })}> -                        {({ measureRef }) => -                            <div ref={measureRef} id="mainContent-div"> -                                {this.mainContent} -                            </div> -                        } -                    </Measure> -                    <ContextMenu /> -                    {this.nodesMenu} -                    {this.miscButtons} -                    {workspaceMenu} -                    <InkingControl /> -                </div> -                {this.activeTextBox} -            </> +            <div id="main-div"> +                <DocumentDecorations /> +                {this.mainContent} +                <PreviewCursor /> +                <ContextMenu /> +                {this.nodesMenu} +                {this.miscButtons} +                {this.workspaceMenu} +                <InkingControl /> +                <MainOverlayTextBox /> +            </div>          );      }      // --------------- Northstar hooks ------------- / +    private _northstarSchemas: Document[] = []; -    @action AddToNorthstarCatalog(ctlog: Catalog) { -        CurrentUserUtils.NorthstarDBCatalog = CurrentUserUtils.NorthstarDBCatalog ? CurrentUserUtils.NorthstarDBCatalog : ctlog; +    @action SetNorthstarCatalog(ctlog: Catalog) { +        CurrentUserUtils.NorthstarDBCatalog = ctlog;          if (ctlog && ctlog.schemas) { -            this._northstarSchemas.push(...ctlog.schemas.map(schema => { -                let schemaDoc = Documents.TreeDocument([], { width: 50, height: 100, title: schema.displayName! }); -                let schemaDocuments = schemaDoc.GetList(KeyStore.Data, [] as Document[]); -                CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { -                    Server.GetField(attr.displayName! + ".alias", action((field: Opt<Field>) => { +            ctlog.schemas.map(schema => { +                let schemaDocuments: Document[] = []; +                let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema); +                Promise.all(attributesToBecomeDocs.reduce((promises, attr) => { +                    promises.push(Server.GetField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => {                          if (field instanceof Document) {                              schemaDocuments.push(field);                          } else { @@ -367,13 +325,15 @@ export class Main extends React.Component {                                  new AttributeTransformationModel(atmod, AggregateFunction.Count));                              schemaDocuments.push(Documents.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! }, undefined, attr.displayName! + ".alias"));                          } -                    })); -                }); -                return schemaDoc; -            })); +                    }))); +                    return promises; +                }, [] as Promise<void>[])).finally(() => +                    this._northstarSchemas.push(Documents.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }))); +            });          }      }      async initializeNorthstar(): Promise<void> { +<<<<<<< HEAD          let envPath = "/assets/env.json";          const response = await fetch(envPath, {              redirect: "follow", @@ -390,6 +350,11 @@ export class Main extends React.Component {              }          }); +======= +        const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); +        NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); +        Gateway.Instance.ClearCatalog().then(async () => this.SetNorthstarCatalog(await Gateway.Instance.GetCatalog())); +>>>>>>> e47656cdc18aa1fd801a3853fa0f819140a68646      }  } diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss new file mode 100644 index 000000000..f6a746e63 --- /dev/null +++ b/src/client/views/MainOverlayTextBox.scss @@ -0,0 +1,20 @@ +@import "globalCssVariables"; +.mainOverlayTextBox-textInput { +    background-color: rgba(248, 6, 6, 0.001); +    width: 200px; +    height: 200px; +    position:absolute; +    overflow: visible; +    top: 0; +    left: 0; +    pointer-events: none;    +    z-index: $mainTextInput-zindex; +    .formattedTextBox-cont { +        background-color: rgba(248, 6, 6, 0.001); +        width: 100%; +        height: 100%; +        position:absolute; +        top: 0; +        left: 0; +    } +}
\ No newline at end of file diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx new file mode 100644 index 000000000..a6c7eabf7 --- /dev/null +++ b/src/client/views/MainOverlayTextBox.tsx @@ -0,0 +1,113 @@ +import { action, observable, trace } from 'mobx'; +import { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import { Document } from '../../fields/Document'; +import { Key } from '../../fields/Key'; +import { KeyStore } from '../../fields/KeyStore'; +import { emptyDocFunction, emptyFunction, returnTrue } from '../../Utils'; +import '../northstar/model/ModelExtensions'; +import '../northstar/utils/Extensions'; +import { DragManager } from '../util/DragManager'; +import { Transform } from '../util/Transform'; +import "./MainOverlayTextBox.scss"; +import { FormattedTextBox } from './nodes/FormattedTextBox'; + +interface MainOverlayTextBoxProps { +} + +@observer +export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> { +    public static Instance: MainOverlayTextBox; +    @observable public TextDoc?: Document = undefined; +    public TextScroll: number = 0; +    private _textRect: any; +    private _textXf: Transform = Transform.Identity(); +    private _textFieldKey: Key = KeyStore.Data; +    private _textColor: string | null = null; +    private _textTargetDiv: HTMLDivElement | undefined; +    private _textProxyDiv: React.RefObject<HTMLDivElement>; + +    constructor(props: MainOverlayTextBoxProps) { +        super(props); +        this._textProxyDiv = React.createRef(); +        MainOverlayTextBox.Instance = this; +    } + +    @action +    SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: Transform) { +        if (this._textTargetDiv) { +            this._textTargetDiv.style.color = this._textColor; +        } + +        this.TextDoc = textDoc; +        this._textFieldKey = textFieldKey!; +        this._textXf = tx ? tx : Transform.Identity(); +        this._textTargetDiv = div; +        if (div) { +            this._textColor = div.style.color; +            div.style.color = "transparent"; +            this._textRect = div.getBoundingClientRect(); +            this.TextScroll = div.scrollTop; +        } +    } + +    @action +    textScroll = (e: React.UIEvent) => { +        if (this._textProxyDiv.current && this._textTargetDiv) { +            this.TextScroll = (e as any)._targetInst.stateNode.scrollTop;//  this._textProxyDiv.current.children[0].scrollTop; +            this._textTargetDiv.scrollTop = this.TextScroll; +        } +    } + +    textBoxDown = (e: React.PointerEvent) => { +        if (e.button !== 0 || e.metaKey || e.altKey) { +            document.addEventListener("pointermove", this.textBoxMove); +            document.addEventListener('pointerup', this.textBoxUp); +        } +    } +    textBoxMove = (e: PointerEvent) => { +        if (e.movementX > 1 || e.movementY > 1) { +            document.removeEventListener("pointermove", this.textBoxMove); +            document.removeEventListener('pointerup', this.textBoxUp); +            let dragData = new DragManager.DocumentDragData([this.TextDoc!]); +            const [left, top] = this._textXf +                .inverse() +                .transformPoint(0, 0); +            dragData.xOffset = e.clientX - left; +            dragData.yOffset = e.clientY - top; +            DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, { +                handlers: { +                    dragComplete: action(emptyFunction), +                }, +                hideSource: false +            }); +        } +    } +    textBoxUp = (e: PointerEvent) => { +        document.removeEventListener("pointermove", this.textBoxMove); +        document.removeEventListener('pointerup', this.textBoxUp); +    } + +    textXf = () => this._textXf; + +    render() { +        if (this.TextDoc) { +            let x: number = this._textRect.x; +            let y: number = this._textRect.y; +            let w: number = this._textRect.width; +            let h: number = this._textRect.height; +            let t = this._textXf.transformPoint(0, 0); +            let s = this._textXf.transformPoint(1, 0); +            s[0] = Math.sqrt((s[0] - t[0]) * (s[0] - t[0]) + (s[1] - t[1]) * (s[1] - t[1])); +            return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${x}px, ${y}px) scale(${1 / s[0]},${1 / s[0]})`, width: "auto", height: "auto" }} > +                <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} style={{ transform: `scale(${1}, ${1})`, width: `${w * s[0]}px`, height: `${h * s[0]}px` }}> +                    <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} +                        selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} +                        ScreenToLocalTransform={this.textXf} focus={emptyDocFunction} /> +                </div> +            </ div>; +        } +        else return (null); +    } +}
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss new file mode 100644 index 000000000..20f9b9a49 --- /dev/null +++ b/src/client/views/PreviewCursor.scss @@ -0,0 +1,9 @@ + +.previewCursor { +    color: black; +    position: absolute; +    transform-origin: left top; +    top: 0; +    left:0; +    pointer-events: none; +}
\ No newline at end of file diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx new file mode 100644 index 000000000..ff8434681 --- /dev/null +++ b/src/client/views/PreviewCursor.tsx @@ -0,0 +1,37 @@ +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import "./PreviewCursor.scss"; + +@observer +export class PreviewCursor extends React.Component<{}> { +    private _prompt = React.createRef<HTMLDivElement>(); +    //when focus is lost, this will remove the preview cursor +    @action onBlur = (): void => { +        PreviewCursor.Visible = false; +        PreviewCursor.hide(); +    } + +    @observable static clickPoint = [0, 0]; +    @observable public static Visible = false; +    @observable public static hide = () => { }; +    @action +    public static Show(hide: any, x: number, y: number) { +        this.clickPoint = [x, y]; +        this.hide = hide; +        setTimeout(action(() => this.Visible = true), (1)); +    } +    render() { +        if (!PreviewCursor.clickPoint) { +            return (null); +        } +        if (PreviewCursor.Visible && this._prompt.current) { +            this._prompt.current.focus(); +        } +        return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._prompt} +            style={{ transform: `translate(${PreviewCursor.clickPoint[0]}px, ${PreviewCursor.clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}> +            I +        </div >; +    } +}
\ No newline at end of file diff --git a/src/client/views/_global_variables.scss b/src/client/views/_global_variables.scss deleted file mode 100644 index 44a819b79..000000000 --- a/src/client/views/_global_variables.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700"); -// colors -$light-color: #fcfbf7; -$light-color-secondary: rgb(241, 239, 235); -$main-accent: #61aaa3; -// $alt-accent: #cdd5ec; -// $alt-accent: #cdeceb; -$alt-accent: #59dff7; -$lighter-alt-accent: rgb(207, 220, 240); -$intermediate-color: #9c9396; -$dark-color: #121721; -// fonts -$sans-serif: "Noto Sans", sans-serif; -// $sans-serif: "Roboto Slab", sans-serif; -$serif: "Crimson Text", serif; -// misc values -$border-radius: 0.3em; diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 4380c8194..0c1cd7b8f 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,4 +1,4 @@ -import { action } from 'mobx'; +import { action, computed } from 'mobx';  import { observer } from 'mobx-react';  import * as React from 'react';  import { Document } from '../../../fields/Document'; @@ -22,7 +22,7 @@ export interface CollectionRenderProps {      removeDocument: (document: Document) => boolean;      moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;      active: () => boolean; -    onActiveChanged: (isActive: boolean) => void; +    whenActiveChanged: (isActive: boolean) => void;  }  export interface CollectionViewProps extends FieldViewProps { @@ -32,19 +32,18 @@ export interface CollectionViewProps extends FieldViewProps {      contentRef?: React.Ref<HTMLDivElement>;  } -export const COLLECTION_BORDER_WIDTH = 1;  @observer  export class CollectionBaseView extends React.Component<CollectionViewProps> { -    get collectionViewType(): CollectionViewType { +    get collectionViewType(): CollectionViewType | undefined {          let Document = this.props.Document;          let viewField = Document.GetT(KeyStore.ViewType, NumberField);          if (viewField === FieldWaiting) { -            return CollectionViewType.Invalid; +            return undefined;          } else if (viewField) {              return viewField.Data;          } else { -            return CollectionViewType.Freeform; +            return CollectionViewType.Invalid;          }      } @@ -56,19 +55,22 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {      //TODO should this be observable?      private _isChildActive = false; -    onActiveChanged = (isActive: boolean) => { +    whenActiveChanged = (isActive: boolean) => {          this._isChildActive = isActive; -        this.props.onActiveChanged(isActive); +        this.props.whenActiveChanged(isActive);      }      createsCycle(documentToAdd: Document, containerDocument: Document): boolean { -        let data = documentToAdd.GetList<Document>(KeyStore.Data, []); -        for (const doc of data) { +        if (!(documentToAdd instanceof Document)) { +            return false; +        } +        let data = documentToAdd.GetList(KeyStore.Data, [] as Document[]); +        for (const doc of data.filter(d => d instanceof Document)) {              if (this.createsCycle(doc, containerDocument)) {                  return true;              }          } -        let annots = documentToAdd.GetList<Document>(KeyStore.Annotations, []); +        let annots = documentToAdd.GetList(KeyStore.Annotations, [] as Document[]);          for (const annot of annots) {              if (this.createsCycle(annot, containerDocument)) {                  return true; @@ -81,6 +83,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {          }          return false;      } +    @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?      @action.bound      addDocument(doc: Document, allowDuplicates: boolean = false): boolean { @@ -97,6 +100,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {                  if (!value.some(v => v.Id === doc.Id) || allowDuplicates) {                      value.push(doc);                  } +                return true;              }              else {                  return false; @@ -107,15 +111,18 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {                  const field = new ListField([doc]);                  // const script = CompileScript(`                  //     if(added) { -                //         console.log("added " + field.Title); +                //         console.log("added " + field.Title + " " + doc.Title);                  //     } else { -                //         console.log("removed " + field.Title); +                //         console.log("removed " + field.Title + " " + doc.Title);                  //     }                  // `, {                  //         addReturn: false,                  //         params: {                  //             field: Document.name,                  //             added: "boolean" +                //         }, +                //         capturedVariables: { +                //             doc: this.props.Document                  //         }                  //     });                  // if (script.compiled) { @@ -127,6 +134,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {                  return false;              }          } +        if (true || this.isAnnotationOverlay) { +            doc.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1)); +        }          return true;      } @@ -175,11 +185,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {              removeDocument: this.removeDocument,              moveDocument: this.moveDocument,              active: this.active, -            onActiveChanged: this.onActiveChanged, +            whenActiveChanged: this.whenActiveChanged,          }; +        const viewtype = this.collectionViewType;          return (              <div className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> -                {this.props.children(this.collectionViewType, props)} +                {viewtype !== undefined ? this.props.children(viewtype, props) : (null)}              </div>          );      } diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 583d50c5b..0e7e0afa7 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,8 +1,29 @@ +@import "../../views/globalCssVariables.scss"; +  .collectiondockingview-content {      height: 100%;  } +.lm_active .messageCounter{ +    color:white; +    background: #999999; +} +.messageCounter { +    width:18px; +    height:20px; +    text-align: center; +    border-radius: 20px; +    margin-left: 5px; +    transform: translate(0px, -8px); +    display: inline-block; +    background: transparent; +    border: 1px #999999 solid; +}  .collectiondockingview-container { +    width: 100%; +    height: 100%; +    border-style: solid; +    border-width: $COLLECTION_BORDER_WIDTH;      position: absolute;      top: 0;      left: 0; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index ea6d3a247..e4c647635 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -7,18 +7,19 @@ import * as ReactDOM from 'react-dom';  import { Document } from "../../../fields/Document";  import { KeyStore } from "../../../fields/KeyStore";  import Measure from "react-measure"; -import { FieldId, Opt, Field } from "../../../fields/Field"; -import { Utils, returnTrue, emptyFunction } from "../../../Utils"; +import { FieldId, Opt, Field, FieldWaiting } from "../../../fields/Field"; +import { Utils, returnTrue, emptyFunction, emptyDocFunction, returnOne } from "../../../Utils";  import { Server } from "../../Server";  import { undoBatch } from "../../util/UndoManager";  import { DocumentView } from "../nodes/DocumentView";  import "./CollectionDockingView.scss"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionBaseView";  import React = require("react");  import { SubCollectionViewProps } from "./CollectionSubView";  import { ServerUtils } from "../../../server/ServerUtil"; -import { DragManager } from "../../util/DragManager"; +import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";  import { TextField } from "../../../fields/TextField"; +import { ListField } from "../../../fields/ListField"; +import { Transform } from '../../util/Transform'  @observer  export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -50,7 +51,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp      public StartOtherDrag(dragDocs: Document[], e: any) {          dragDocs.map(dragDoc =>              this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener. -                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })); +                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));      }      @action @@ -172,7 +173,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp          } catch (e) {          } -        this._goldenLayout.destroy(); +        if (this._goldenLayout) this._goldenLayout.destroy();          this._goldenLayout = null;          window.removeEventListener('resize', this.onResize);      } @@ -194,23 +195,35 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp      @action      onPointerDown = (e: React.PointerEvent): void => {          var className = (e.target as any).className; -        if ((className === "lm_title" || className === "lm_tab lm_active") && (e.ctrlKey || e.altKey)) { +        if (className === "messageCounter") {              e.stopPropagation();              e.preventDefault(); +            let x = e.clientX; +            let y = e.clientY;              let docid = (e.target as any).DashDocId;              let tab = (e.target as any).parentElement as HTMLElement; -            Server.GetField(docid, action((f: Opt<Field>) => { -                if (f instanceof Document) { -                    DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), e.pageX, e.pageY, -                        { -                            handlers: { -                                dragComplete: action(() => { }), -                            }, -                            hideSource: false -                        }); -                } -            })); -        } +            Server.GetField(docid, action(async (sourceDoc: Opt<Field>) => +                (sourceDoc instanceof Document) && DragLinksAsDocuments(tab, x, y, sourceDoc))); +        } else +            if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) { +                e.stopPropagation(); +                e.preventDefault(); +                let x = e.clientX; +                let y = e.clientY; +                let docid = (e.target as any).DashDocId; +                let tab = (e.target as any).parentElement as HTMLElement; +                Server.GetField(docid, action((f: Opt<Field>) => { +                    if (f instanceof Document) { +                        DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, +                            { +                                handlers: { +                                    dragComplete: action(emptyFunction), +                                }, +                                hideSource: false +                            }); +                    } +                })); +            }          if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {              this._flush = true;          } @@ -229,24 +242,44 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp          this.stateChanged();      } +    htmlToElement(html: string) { +        var template = document.createElement('template'); +        html = html.trim(); // Never return a text node of whitespace as the result +        template.innerHTML = html; +        return template.content.firstChild; +    } +      tabCreated = (tab: any) => {          if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { -            if (tab.titleElement[0].textContent.indexOf("-waiting") !== -1) { -                Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => { -                    if (f !== undefined && f instanceof Document) { -                        f.GetTAsync(KeyStore.Title, TextField, (tfield) => { -                            if (tfield !== undefined) { -                                tab.titleElement[0].textContent = f.Title; -                            } -                        }); -                    } -                })); -                tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; -            } -            tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; +            Server.GetField(tab.contentItem.config.props.documentId, action((f: Opt<Field>) => { +                if (f !== undefined && f instanceof Document) { +                    f.GetTAsync(KeyStore.Title, TextField, (tfield) => { +                        if (tfield !== undefined) { +                            tab.titleElement[0].textContent = f.Title; +                        } +                    }); +                    f.GetTAsync(KeyStore.LinkedFromDocs, ListField).then(lf => +                        f.GetTAsync(KeyStore.LinkedToDocs, ListField).then(lt => { +                            let count = (lf ? lf.Data.length : 0) + (lt ? lt.Data.length : 0); +                            let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`); +                            tab.element.append(counter); +                            counter.DashDocId = tab.contentItem.config.props.documentId; +                            tab.reactionDisposer = reaction(() => [f.GetT(KeyStore.LinkedFromDocs, ListField), f.GetT(KeyStore.LinkedToDocs, ListField)], +                                (lists) => { +                                    let count = (lists.length > 0 && lists[0] && lists[0]!.Data ? lists[0]!.Data.length : 0) + +                                        (lists.length > 1 && lists[1] && lists[1]!.Data ? lists[1]!.Data.length : 0); +                                    counter.innerHTML = count; +                                }); +                        })); +                    tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; +                } +            }));          }          tab.closeElement.off('click') //unbind the current click handler              .click(function () { +                if (tab.reactionDisposer) { +                    tab.reactionDisposer(); +                }                  tab.contentItem.remove();              });      } @@ -271,13 +304,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp      render() {          return (              <div className="collectiondockingview-container" id="menuContainer" -                onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} -                style={{ -                    width: "100%", -                    height: "100%", -                    borderStyle: "solid", -                    borderWidth: `${COLLECTION_BORDER_WIDTH}px`, -                }} /> +                onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />          );      }  } @@ -289,7 +316,7 @@ interface DockedFrameProps {  @observer  export class DockedFrameRenderer extends React.Component<DockedFrameProps> { -    private _mainCont = React.createRef<HTMLDivElement>(); +    _mainCont = React.createRef<HTMLDivElement>();      @observable private _panelWidth = 0;      @observable private _panelHeight = 0;      @observable private _document: Opt<Document>; @@ -299,38 +326,49 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {          Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document));      } -    private _nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); -    private _nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); -    private _contentScaling = () => this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); +    nativeWidth = () => this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); +    nativeHeight = () => this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); +    contentScaling = () => { +        let wscale = this._panelWidth / (this.nativeWidth() ? this.nativeWidth() : this._panelWidth); +        if (wscale * this.nativeHeight() > this._panelHeight) +            return this._panelHeight / (this.nativeHeight() ? this.nativeHeight() : this._panelHeight); +        return wscale; +    }      ScreenToLocalTransform = () => { -        let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!); -        return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling()); +        if (this._mainCont.current && this._mainCont.current.children) { +            let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!.children[0].firstChild as HTMLElement); +            scale = Utils.GetScreenTransform(this._mainCont.current!).scale; +            return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling()); +        } +        return Transform.Identity();      } +    get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } -    render() { -        if (!this._document) { -            return (null); -        } -        var content = -            <div className="collectionDockingView-content" ref={this._mainCont}> -                <DocumentView key={this._document.Id} Document={this._document} +    get content() { +        return ( +            <div className="collectionDockingView-content" ref={this._mainCont} +                style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> +                <DocumentView key={this._document!.Id} Document={this._document!}                      addDocument={undefined}                      removeDocument={undefined} -                    ContentScaling={this._contentScaling} -                    PanelWidth={this._nativeWidth} -                    PanelHeight={this._nativeHeight} +                    ContentScaling={this.contentScaling} +                    PanelWidth={this.nativeWidth} +                    PanelHeight={this.nativeHeight}                      ScreenToLocalTransform={this.ScreenToLocalTransform}                      isTopMost={true}                      selectOnLoad={false}                      parentActive={returnTrue} -                    onActiveChanged={emptyFunction} -                    focus={(doc: Document) => { }} +                    whenActiveChanged={emptyFunction} +                    focus={emptyDocFunction}                      ContainingCollectionView={undefined} /> -            </div>; +            </div>); +    } -        return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> -            {({ measureRef }) => <div ref={measureRef}>  {content} </div>} -        </Measure>; +    render() { +        return !this._document ? (null) : +            <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}> +                {({ measureRef }) => <div ref={measureRef}>  {this.content} </div>} +            </Measure>;      }  }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index 97bac745c..229bc4059 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable } from "mobx"; +import { action } from "mobx";  import { observer } from "mobx-react";  import { KeyStore } from "../../../fields/KeyStore";  import { ContextMenu } from "../ContextMenu"; @@ -7,6 +7,7 @@ import React = require("react");  import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";  import { FieldView, FieldViewProps } from "../nodes/FieldView";  import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView"; +import { emptyFunction } from "../../../Utils";  @observer @@ -33,7 +34,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {      onContextMenu = (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: () => { } }); +            ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction });          }      } @@ -41,7 +42,7 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {          let props = { ...this.props, ...renderProps };          return (              <> -                <CollectionFreeFormView {...props} /> +                <CollectionFreeFormView {...props} CollectionView={this} />                  {this.props.isSelected() ? this.uIButtons : (null)}              </>          ); diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index c3a2e88ac..cfdb3ab22 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,76 +1,41 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -//options menu styling -#schemaOptionsMenuBtn { -    position: absolute; -    height: 20px; -    width: 20px; -    border-radius: 50%; -    z-index: 21;  -    right: 4px; -    top: 4px;  -    pointer-events: auto; -    background-color:black; -    display:inline-block; -    padding: 0px; -    font-size: 100%; -} -#schema-options-header { -    text-align: center; -    padding: 0px; -    margin: 0px; -} -.schema-options-subHeader { -    color: $intermediate-color; -    margin-bottom: 5px; -} -#schemaOptionsMenuBtn:hover { -    transform: scale(1.15); -} - -#preview-schema-checkbox-div { -    margin-left: 20px; -    font-size: 12px; -} -  #options-flyout-div { -    text-align: left; -    padding:0px; -    z-index: 100; -    font-family: $sans-serif; -    padding-left: 5px; -  } - -  #schema-col-checklist { -    overflow: scroll; -    text-align: left; -    //background-color: $light-color-secondary; -    line-height: 25px; -    max-height: 175px; -    font-family: $sans-serif; -    font-size: 12px; -  } -    .collectionSchemaView-container { -    border: 1px solid $intermediate-color; +    border-width: $COLLECTION_BORDER_WIDTH; +    border-color : $intermediate-color; +    border-style: solid;      border-radius: $border-radius;      box-sizing: border-box;      position: absolute;      width: 100%;      height: 100%; -     -    .collectionSchemaView-content { -        position: absolute; -        height: 100%; -        width: 100%; -        overflow: auto; + +    .collectionSchemaView-cellContents { +        height: $MAX_ROW_HEIGHT;      } +          .collectionSchemaView-previewRegion {          position: relative;          background: $light-color;          float: left;          height: 100%; +        .collectionSchemaView-previewDoc { +            height: 100%; +            width: 100%;  +            position: absolute; +        } +        .collectionSchemaView-input { +            position: absolute; +            max-width: 150px; +            width: 100%; +            bottom: 0px; +        } +        .documentView-node:first-child { +            position: relative; +            background: $light-color; +        }      }      .collectionSchemaView-previewHandle {          position: absolute; @@ -144,7 +109,7 @@          }          .rt-tr-group {              direction: ltr; -            max-height: 44px; +            max-height: $MAX_ROW_HEIGHT;          }          .rt-td {              border-width: 1px; @@ -176,7 +141,7 @@      }      .ReactTable .rt-th,      .ReactTable .rt-td { -        max-height: 44; +        max-height: $MAX_ROW_HEIGHT;          padding: 3px 7px;          font-size: 13px;          text-align: center; @@ -186,13 +151,71 @@          border-bottom-style: solid;          border-bottom-width: 1;      } +    .documentView-node-topmost { +        text-align:left; +        transform-origin: center top; +        display: inline-block; +    }      .documentView-node:first-child {          background: $light-color; -        .imageBox-cont img { -            object-fit: contain; -        }      }  } +//options menu styling +#schemaOptionsMenuBtn { +    position: absolute; +    height: 20px; +    width: 20px; +    border-radius: 50%; +    z-index: 21;  +    right: 4px; +    top: 4px;  +    pointer-events: auto; +    background-color:black; +    display:inline-block; +    padding: 0px; +    font-size: 100%; +} + +ul { +    list-style-type: disc; +} + +#schema-options-header { +    text-align: center; +    padding: 0px; +    margin: 0px; +} +.schema-options-subHeader { +    color: $intermediate-color; +    margin-bottom: 5px; +} +#schemaOptionsMenuBtn:hover { +    transform: scale(1.15); +} + +#preview-schema-checkbox-div { +    margin-left: 20px; +    font-size: 12px; +} + +  #options-flyout-div { +    text-align: left; +    padding:0px; +    z-index: 100; +    font-family: $sans-serif; +    padding-left: 5px; +  } + +  #schema-col-checklist { +    overflow: scroll; +    text-align: left; +    //background-color: $light-color-secondary; +    line-height: 25px; +    max-height: 175px; +    font-family: $sans-serif; +    font-size: 12px; +  } +    .Resizer {      box-sizing: border-box; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 587f60b3d..90077b053 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -2,31 +2,29 @@ import React = require("react");  import { library } from '@fortawesome/fontawesome-svg-core';  import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable, trace, untracked } from "mobx"; +import { action, computed, observable, untracked } from "mobx";  import { observer } from "mobx-react"; -import Measure from "react-measure";  import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; +import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss'  import "react-table/react-table.css";  import { Document } from "../../../fields/Document"; -import { Field, Opt, FieldWaiting } from "../../../fields/Field"; +import { Field, Opt } from "../../../fields/Field";  import { Key } from "../../../fields/Key";  import { KeyStore } from "../../../fields/KeyStore";  import { ListField } from "../../../fields/ListField"; +import { emptyDocFunction, emptyFunction, returnFalse } from "../../../Utils";  import { Server } from "../../Server"; -import { setupDrag } from "../../util/DragManager"; +import { SetupDrag } from "../../util/DragManager";  import { CompileScript, ToField } from "../../util/Scripting";  import { Transform } from "../../util/Transform"; +import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";  import { anchorPoints, Flyout } from "../DocumentDecorations";  import '../DocumentDecorations.scss';  import { EditableView } from "../EditableView";  import { DocumentView } from "../nodes/DocumentView";  import { FieldView, FieldViewProps } from "../nodes/FieldView";  import "./CollectionSchemaView.scss"; -import { CollectionView } from "./CollectionView";  import { CollectionSubView } from "./CollectionSubView"; -import { TextField } from "../../../fields/TextField"; -import { COLLECTION_BORDER_WIDTH } from "./CollectionBaseView"; -import { emptyFunction, returnFalse } from "../../../Utils";  // bcz: need to add drag and drop of rows and columns.  This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @@ -36,61 +34,55 @@ import { emptyFunction, returnFalse } from "../../../Utils";  class KeyToggle extends React.Component<{ keyId: string, checked: boolean, toggle: (key: Key) => void }> {      @observable key: Key | undefined; -    componentWillReceiveProps() { -        Server.GetField(this.props.keyId, action((field: Opt<Field>) => { -            if (field instanceof Key) { -                this.key = field; -            } -        })); +    constructor(props: any) { +        super(props); +        Server.GetField(this.props.keyId, action((field: Opt<Field>) => field instanceof Key && (this.key = field)));      }      render() { -        if (this.key) { -            return (<div key={this.key.Id}> +        return !this.key ? (null) : +            (<div key={this.key.Id}>                  <input type="checkbox" checked={this.props.checked} onChange={() => this.key && this.props.toggle(this.key)} />                  {this.key.Name}              </div>); -        } -        return (null);      }  }  @observer  export class CollectionSchemaView extends CollectionSubView { -    private _mainCont = React.createRef<HTMLDivElement>(); +    private _mainCont?: HTMLDivElement;      private _startSplitPercent = 0;      private DIVIDER_WIDTH = 4;      @observable _columns: Array<Key> = [KeyStore.Title, KeyStore.Data, KeyStore.Author]; -    @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 _columnsPercentage = 0;      @observable _keys: Key[] = []; +    @observable _newKeyName: string = "";      @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0); } - +    @computed get columns() { return this.props.Document.GetList(KeyStore.ColumnsKey, [] as Key[]); } +    @computed get borderWidth() { return COLLECTION_BORDER_WIDTH; }      renderCell = (rowProps: CellInfo) => {          let props: FieldViewProps = {              Document: rowProps.value[0],              fieldKey: rowProps.value[1], -            isSelected: () => false, -            select: () => { }, +            ContainingCollectionView: this.props.CollectionView, +            isSelected: returnFalse, +            select: emptyFunction,              isTopMost: false,              selectOnLoad: false,              ScreenToLocalTransform: Transform.Identity, -            focus: emptyFunction, +            focus: emptyDocFunction,              active: returnFalse, -            onActiveChanged: emptyFunction, +            whenActiveChanged: emptyFunction,          };          let contents = (              <FieldView {...props} />          );          let reference = React.createRef<HTMLDivElement>(); -        let onItemDown = setupDrag(reference, () => props.Document, this.props.moveDocument); +        let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument);          let applyToDoc = (doc: Document, run: (args?: { [name: string]: any }) => any) => {              const res = run({ this: doc });              if (!res.success) return false; @@ -108,11 +100,11 @@ export class CollectionSchemaView extends CollectionSubView {              return false;          };          return ( -            <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} style={{ height: "56px" }} key={props.Document.Id} ref={reference}> +            <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document.Id} ref={reference}>                  <EditableView                      display={"inline"}                      contents={contents} -                    height={56} +                    height={Number(MAX_ROW_HEIGHT)}                      GetValue={() => {                          let field = props.Document.Get(props.fieldKey);                          if (field && field instanceof Field) { @@ -166,9 +158,9 @@ export class CollectionSchemaView extends CollectionSubView {          };      } -    @computed -    get columns() { -        return this.props.Document.GetList<Key>(KeyStore.ColumnsKey, []); +    private createTarget = (ele: HTMLDivElement) => { +        this._mainCont = ele; +        super.CreateDropTarget(ele);      }      @action @@ -188,31 +180,12 @@ export class CollectionSchemaView extends CollectionSubView {      //toggles preview side-panel of schema      @action      toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => { -        this._startSplitPercent = this.splitPercentage; -        if (this._startSplitPercent === this.splitPercentage) { -            this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0); -        } -    } - -    @computed -    get findAllDocumentKeys(): { [id: string]: boolean } { -        const docs = this.props.Document.GetList<Document>(this.props.fieldKey, []); -        let keys: { [id: string]: boolean } = {}; -        if (this._optionsActivated > -1) { -            // bcz: ugh.  this is untracked since otherwise a large collection of documents will blast the server for all their fields. -            //  then as each document's fields come back, we update the documents _proxies.  Each time we do this, the whole schema will be -            //  invalidated and re-rendered.   This workaround will inquire all of the document fields before the options button is clicked. -            //  then by the time the options button is clicked, all of the fields should be in place.  If a new field is added while this menu -            //  is displayed (unlikely) it won't show up until something else changes. -            untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); -        } -        this.columns.forEach(key => keys[key.Id] = true); -        return keys; +        this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, this.splitPercentage === 0 ? 33 : 0);      }      @action      onDividerMove = (e: PointerEvent): void => { -        let nativeWidth = this._mainCont.current!.getBoundingClientRect(); +        let nativeWidth = this._mainCont!.getBoundingClientRect();          this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100)));      }      @action @@ -231,155 +204,149 @@ export class CollectionSchemaView extends CollectionSubView {          document.addEventListener('pointerup', this.onDividerUp);      } -    @observable _tableWidth = 0; -    @action -    setTableDimensions = (r: any) => { -        this._tableWidth = r.entry.width; -    } -    @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); +    onPointerDown = (e: React.PointerEvent): void => { +        if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) { +            if (this.props.isSelected()) +                e.stopPropagation(); +            else e.preventDefault(); +        }      } -    getContentScaling = (): number => this._contentScaling; -    getPanelWidth = (): number => this._panelWidth; -    getPanelHeight = (): number => this._panelHeight; -    getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); -    getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX - this._tableWidth, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling); - -    focusDocument = (doc: Document) => { }; - -    onPointerDown = (e: React.PointerEvent): void => { -        if (this.props.isSelected()) { +    onWheel = (e: React.WheelEvent): void => { +        if (this.props.active()) {              e.stopPropagation();          }      }      @action      addColumn = () => { -        this.columns.push(new Key(this.newKeyName)); -        this.newKeyName = ""; +        this.columns.push(new Key(this._newKeyName)); +        this._newKeyName = "";      } -    @observable -    newKeyName: string = ""; -      @action      newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { -        this.newKeyName = e.currentTarget.value; -    } -    onWheel = (e: React.WheelEvent): void => { -        if (this.props.active()) { -            e.stopPropagation(); -        } -    } - -    @observable _optionsActivated: number = 0; -    @action -    OptionsMenuDown = (e: React.PointerEvent) => { -        this._optionsActivated++; +        this._newKeyName = e.currentTarget.value;      } -    @observable previewScript: string = "this"; +    @observable previewScript: string = "";      @action      onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {          this.previewScript = e.currentTarget.value;      } -    render() { -        library.add(faCog); -        library.add(faPlus); -        const columns = this.columns; -        const children = this.props.Document.GetList<Document>(this.props.fieldKey, []); +    get previewDocument(): Document | undefined { +        const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]);          const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined; -        //all the keys/columns that will be displayed in the schema -        const allKeys = this.findAllDocumentKeys; -        let doc: any = selected ? selected.Get(new Key(this.previewScript)) : undefined; +        return selected ? (this.previewScript ? selected.Get(new Key(this.previewScript)) as Document : selected) : undefined; +    } +    get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); } +    get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; } +    get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; } + +    private previewDocNativeWidth = () => this.previewDocument!.GetNumber(KeyStore.NativeWidth, this.previewRegionWidth); +    private previewDocNativeHeight = () => this.previewDocument!.GetNumber(KeyStore.NativeHeight, this.previewRegionHeight); +    private previewContentScaling = () => { +        let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth); +        if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) +            return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight); +        return wscale; +    } +    private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling(); +    private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling(); +    get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; } +    getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate( +        - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset, +        - this.borderWidth).scale(1 / this.previewContentScaling()); +    @computed +    get previewPanel() {          // let doc = CompileScript(this.previewScript, { this: selected }, true)(); -        let content = this._selectedIndex === -1 || !selected ? (null) : ( -            <Measure onResize={this.setScaling}> -                {({ measureRef }) => -                    <div className="collectionSchemaView-content" ref={measureRef}> -                        {doc instanceof Document ? -                            <DocumentView Document={doc} -                                addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} -                                isTopMost={false} -                                selectOnLoad={false} -                                ScreenToLocalTransform={this.getPreviewTransform} -                                ContentScaling={this.getContentScaling} -                                PanelWidth={this.getPanelWidth} -                                PanelHeight={this.getPanelHeight} -                                ContainingCollectionView={undefined} -                                focus={this.focusDocument} -                                parentActive={this.props.active} -                                onActiveChanged={this.props.onActiveChanged} /> : null} -                        <input value={this.previewScript} onChange={this.onPreviewScriptChange} -                            style={{ position: 'absolute', bottom: '0px' }} /> -                    </div> -                } -            </Measure> -        ); -        let dividerDragger = this.splitPercentage === 0 ? (null) : -            <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; - -        //options button and menu -        let optionsMenu = !this.props.active() ? (null) : (<Flyout -            anchorPoint={anchorPoints.LEFT_TOP} -            content={<div> -                <div id="schema-options-header"><h5><b>Options</b></h5></div> -                <div id="options-flyout-div"> -                    <h6 className="schema-options-subHeader">Preview Window</h6> -                    <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} />  Show Preview </div> -                    <h6 className="schema-options-subHeader" >Displayed Columns</h6> -                    <ul id="schema-col-checklist" > -                        {Array.from(Object.keys(allKeys)).map(item => -                            (<KeyToggle checked={allKeys[item]} key={item} keyId={item} toggle={this.toggleKey} />))} -                    </ul> -                    <input value={this.newKeyName} onChange={this.newKeyChange} /> -                    <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button> +        return !this.previewDocument ? (null) : ( +            <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> +                <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> +                    <DocumentView Document={this.previewDocument} isTopMost={false} selectOnLoad={false} +                        addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} +                        ScreenToLocalTransform={this.getPreviewTransform} +                        ContentScaling={this.previewContentScaling} +                        PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight} +                        ContainingCollectionView={this.props.CollectionView} +                        focus={emptyDocFunction} +                        parentActive={this.props.active} +                        whenActiveChanged={this.props.whenActiveChanged} +                    />                  </div> +                <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} +                    style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} />              </div> -            }> -            <button id="schemaOptionsMenuBtn" onPointerDown={this.OptionsMenuDown}><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> -        </Flyout>); +        ); +    } -        return ( -            <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} 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={this.setTableDimensions}> -                        {({ measureRef }) => -                            <div className="collectionSchemaView-tableContainer" ref={measureRef} style={{ width: `calc(100% - ${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, +    get documentKeysCheckList() { +        const docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); +        let keys: { [id: string]: boolean } = {}; +        // bcz: ugh.  this is untracked since otherwise a large collection of documents will blast the server for all their fields. +        //  then as each document's fields come back, we update the documents _proxies.  Each time we do this, the whole schema will be +        //  invalidated and re-rendered.   This workaround will inquire all of the document fields before the options button is clicked. +        //  then by the time the options button is clicked, all of the fields should be in place.  If a new field is added while this menu +        //  is displayed (unlikely) it won't show up until something else changes. +        untracked(() => docs.map(doc => doc.GetAllPrototypes().map(proto => proto._proxies.forEach((val: any, key: string) => keys[key] = false)))); + +        this.columns.forEach(key => keys[key.Id] = true); +        return Array.from(Object.keys(keys)).map(item => +            (<KeyToggle checked={keys[item]} key={item} keyId={item} toggle={this.toggleKey} />)); +    } -                                    }} -                                    getTrProps={this.getTrProps} -                                /> -                            </div>} -                    </Measure> -                    {dividerDragger} -                    <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 0)}% - ${this.DIVIDER_WIDTH}px)` }}> -                        {content} +    get tableOptionsPanel() { +        return !this.props.active() ? (null) : +            (<Flyout +                anchorPoint={anchorPoints.LEFT_TOP} +                content={<div> +                    <div id="schema-options-header"><h5><b>Options</b></h5></div> +                    <div id="options-flyout-div"> +                        <h6 className="schema-options-subHeader">Preview Window</h6> +                        <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} />  Show Preview </div> +                        <h6 className="schema-options-subHeader" >Displayed Columns</h6> +                        <ul id="schema-col-checklist" > +                            {this.documentKeysCheckList} +                        </ul> +                        <input value={this._newKeyName} onChange={this.newKeyChange} /> +                        <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>                      </div> -                    {optionsMenu}                  </div> -            </div > +                }> +                <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button> +            </Flyout>); +    } + +    @computed +    get dividerDragger() { +        return this.splitPercentage === 0 ? (null) : +            <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; +    } + +    render() { +        library.add(faCog); +        library.add(faPlus); +        const children = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); +        return ( +            <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} +                onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> +                <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}> +                    <ReactTable data={children} page={0} pageSize={children.length} showPagination={false} +                        columns={this.columns.map(col => ({ +                            Header: col.Name, +                            accessor: (doc: Document) => [doc, col], +                            id: col.Id +                        }))} +                        column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} +                        getTrProps={this.getTrProps} +                    /> +                </div> +                {this.dividerDragger} +                {this.previewPanel} +                {this.tableOptionsPanel} +            </div>          );      }  }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5cdea0568..5c3b2e586 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -15,14 +15,20 @@ import { ServerUtils } from "../../../server/ServerUtil";  import { Server } from "../../Server";  import { FieldViewProps } from "../nodes/FieldView";  import * as rp from 'request-promise'; +import { CollectionView } from "./CollectionView"; +import { CollectionPDFView } from "./CollectionPDFView"; +import { CollectionVideoView } from "./CollectionVideoView";  export interface CollectionViewProps extends FieldViewProps {      addDocument: (document: Document, allowDuplicates?: boolean) => boolean;      removeDocument: (document: Document) => boolean;      moveDocument: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean; +    PanelWidth: () => number; +    PanelHeight: () => number;  }  export interface SubCollectionViewProps extends CollectionViewProps { +    CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;  }  export type CursorEntry = TupleField<[string, string], [number, number]>; @@ -37,6 +43,9 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {              this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });          }      } +    protected CreateDropTarget(ele: HTMLDivElement) { +        this.createDropTarget(ele); +    }      @action      protected setCursorPosition(position: [number, number]) { @@ -74,12 +83,21 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {              }              let added = false;              if (de.data.aliasOnDrop || de.data.copyOnDrop) { -                added = de.data.droppedDocuments.reduce((added: boolean, d) => added || this.props.addDocument(d), false); +                added = de.data.droppedDocuments.reduce((added: boolean, d) => { +                    let moved = this.props.addDocument(d); +                    return moved || added; +                }, false);              } else if (de.data.moveDocument) {                  const move = de.data.moveDocument; -                added = de.data.droppedDocuments.reduce((added: boolean, d) => added || move(d, this.props.Document, this.props.addDocument), false); +                added = de.data.droppedDocuments.reduce((added: boolean, d) => { +                    let moved = move(d, this.props.Document, this.props.addDocument); +                    return moved || added; +                }, false);              } else { -                added = de.data.droppedDocuments.reduce((added: boolean, d) => added || this.props.addDocument(d), false); +                added = de.data.droppedDocuments.reduce((added: boolean, d) => { +                    let moved = this.props.addDocument(d); +                    return moved || added; +                }, false);              }              e.stopPropagation();              return added; @@ -87,8 +105,8 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {          return false;      } -    protected getDocumentFromType(type: string, path: string, options: DocumentOptions): Opt<Document> { -        let ctor: ((path: string, options: DocumentOptions) => Document) | undefined; +    protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Document>> { +        let ctor: ((path: string, options: DocumentOptions) => (Document | Promise<Document | undefined>)) | undefined = undefined;          if (type.indexOf("image") !== -1) {              ctor = Documents.ImageDocument;          } @@ -102,6 +120,10 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {              ctor = Documents.PdfDocument;              options.nativeWidth = 1200;          } +        if (type.indexOf("excel") !== -1) { +            ctor = Documents.DBDocument; +            options.copyDraggedItems = true; +        }          if (type.indexOf("html") !== -1) {              if (path.includes('localhost')) {                  let s = path.split('/'); @@ -152,21 +174,16 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {              let item = e.dataTransfer.items[i];              if (item.kind === "string" && item.type.indexOf("uri") !== -1) {                  let str: string; -                let prom = new Promise<string>(res => -                    e.dataTransfer.items[i].getAsString(res)).then(action((s: string) => { -                        str = s; -                        return rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + s)); -                    })).then(res => { -                        let type = res.headers["content-type"]; +                let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) +                    .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) +                    .then(result => { +                        let type = result.headers["content-type"];                          if (type) { -                            let doc = this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }); -                            if (doc) { -                                this.props.addDocument(doc, false); -                            } +                            this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 }) +                                .then(doc => doc && this.props.addDocument(doc, false));                          }                      });                  promises.push(prom); -                // this.props.addDocument(Documents.WebDocument(s, { ...options, width: 300, height: 300 }), false)              }              let type = item.type;              if (item.kind === "file") { @@ -176,17 +193,17 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {                  if (file) {                      formData.append('file', file);                  } +                let dropFileName = file ? file.name : "-empty-";                  let prom = fetch(upload, {                      method: 'POST',                      body: formData                  }).then(async (res: Response) => { -                    const json = await res.json(); -                    json.map((file: any) => { +                    (await res.json()).map(action((file: any) => {                          let path = window.location.origin + file; -                        runInAction(() => { -                            let doc = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300 }); +                        let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); +                        docPromise.then(action((doc?: Document) => {                              let docs = this.props.Document.GetT(KeyStore.Data, ListField);                              if (docs !== FieldWaiting) {                                  if (!docs) { @@ -197,15 +214,15 @@ export class CollectionSubView extends React.Component<SubCollectionViewProps> {                                      docs.Data.push(doc);                                  }                              } -                        }); -                    }); +                        })); +                    }));                  });                  promises.push(prom);              }          }          if (promises.length) { -            Promise.all(promises).catch(() => { }).then(() => batch.end()); +            Promise.all(promises).finally(() => batch.end());          } else {              batch.end();          } diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index f2affbf55..8ecc5b67b 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,7 +1,9 @@ -@import "../global_variables"; +@import "../globalCssVariables";  .collectionTreeView-dropTarget { -    border: 0px solid transparent; +    border-width: $COLLECTION_BORDER_WIDTH; +    border-color: transparent; +    border-style: solid;      border-radius: $border-radius;      box-sizing: border-box;      height: 100%; @@ -48,15 +50,15 @@      .docContainer:hover {          .delete-button {              display: inline; -            width: auto; +            // width: auto;          }      }      .delete-button {          color: $intermediate-color; -        float: right; +        // float: right;          margin-left: 15px; -        margin-top: 3px; +        // margin-top: 3px;          display: inline;      }  }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 659cff9fe..e0387f4b4 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,20 +1,17 @@  import { IconProp, library } from '@fortawesome/fontawesome-svg-core';  import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';  import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable, trace } from "mobx"; +import { action, observable } from "mobx";  import { observer } from "mobx-react";  import { Document } from "../../../fields/Document";  import { FieldWaiting } from "../../../fields/Field";  import { KeyStore } from "../../../fields/KeyStore";  import { ListField } from "../../../fields/ListField"; -import { setupDrag, DragManager } from "../../util/DragManager"; +import { DragManager, SetupDrag } from "../../util/DragManager";  import { EditableView } from "../EditableView"; -import "./CollectionTreeView.scss"; -import { CollectionView } from "./CollectionView";  import { CollectionSubView } from "./CollectionSubView"; +import "./CollectionTreeView.scss";  import React = require("react"); -import { COLLECTION_BORDER_WIDTH } from './CollectionBaseView'; -import { props } from 'bluebird';  export interface TreeViewProps { @@ -77,7 +74,7 @@ class TreeView extends React.Component<TreeViewProps> {       */      renderTitle() {          let reference = React.createRef<HTMLDivElement>(); -        let onItemDown = setupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag); +        let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.copyOnDrag);          let editableView = (titleString: string) =>              (<EditableView                  display={"inline"} @@ -139,7 +136,7 @@ export class CollectionTreeView extends CollectionSubView {              );          return ( -            <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> +            <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>                  <div className="coll-title">                      <EditableView                          contents={this.props.Document.Title} diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index b02983a2e..29fb342c6 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -7,6 +7,7 @@ import React = require("react");  import "./CollectionVideoView.scss";  import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";  import { FieldView, FieldViewProps } from "../nodes/FieldView"; +import { emptyFunction } from "../../../Utils";  @observer @@ -100,7 +101,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {      onContextMenu = (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: "VideoOptions", event: () => { } }); +            ContextMenu.Instance.addItem({ description: "VideoOptions", event: emptyFunction });          }      } @@ -108,7 +109,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> {          let props = { ...this.props, ...renderProps };          return (              <> -                <CollectionFreeFormView {...props} /> +                <CollectionFreeFormView {...props} CollectionView={this} />                  {this.props.isSelected() ? this.uIButtons : (null)}              </>          ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8abd0a02d..675e720e2 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -10,6 +10,7 @@ import { CurrentUserUtils } from '../../../server/authentication/models/current_  import { KeyStore } from '../../../fields/KeyStore';  import { observer } from 'mobx-react';  import { undoBatch } from '../../util/UndoManager'; +import { trace } from 'mobx';  @observer  export class CollectionView extends React.Component<FieldViewProps> { @@ -18,18 +19,20 @@ export class CollectionView extends React.Component<FieldViewProps> {      private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {          let props = { ...this.props, ...renderProps };          switch (type) { -            case CollectionViewType.Schema: return (<CollectionSchemaView {...props} />); -            case CollectionViewType.Docking: return (<CollectionDockingView {...props} />); -            case CollectionViewType.Tree: return (<CollectionTreeView {...props} />); +            case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />); +            case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />); +            case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);              case CollectionViewType.Freeform:              default: -                return (<CollectionFreeFormView {...props} />); +                return (<CollectionFreeFormView {...props} CollectionView={this} />);          }          return (null);      } +    get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? +      onContextMenu = (e: React.MouseEvent): void => { -        if (!e.isPropagationStopped() && this.props.Document.Id !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 +        if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document.Id !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7              ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform)) });              ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema)) });              ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree)) }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 081b3eb6c..20c5a84bf 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -23,10 +23,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo          let l = this.props.LinkDocs;          let a = this.props.A;          let b = this.props.B; -        let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Width, 0) / 2); -        let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Height, 0) / 2); -        let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Width, 0) / 2); -        let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Height, 0) / 2); +        let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Width() / 2); +        let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.IsMinimized, false) ? 5 : a.Height() / 2); +        let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Width() / 2); +        let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.IsMinimized, false) ? 5 : b.Height() / 2);          return (              <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown}                  style={{ strokeWidth: `${l.length * 5}` }} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index cf058090d..ebdb0c75c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -1,7 +1,6 @@ -import { computed, reaction } from "mobx"; +import { computed, IReactionDisposer, reaction } from "mobx";  import { observer } from "mobx-react";  import { Document } from "../../../../fields/Document"; -import { FieldWaiting } from "../../../../fields/Field";  import { KeyStore } from "../../../../fields/KeyStore";  import { ListField } from "../../../../fields/ListField";  import { Utils } from "../../../../Utils"; @@ -15,18 +14,15 @@ import React = require("react");  @observer  export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> { -    HackToAvoidReactionFiringUnnecessarily?: Document = undefined; +    _brushReactionDisposer?: IReactionDisposer;      componentDidMount() { -        this.HackToAvoidReactionFiringUnnecessarily = this.props.Document; -        reaction(() => -            DocumentManager.Instance.getAllDocumentViews(this.HackToAvoidReactionFiringUnnecessarily!). -                map(dv => dv.props.Document.GetNumber(KeyStore.X, 0)), +        this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)),              () => { -                let views = DocumentManager.Instance.getAllDocumentViews(this.props.Document); +                let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc.GetText(KeyStore.BackgroundLayout, "").indexOf("istogram") !== -1);                  for (let i = 0; i < views.length; i++) {                      for (let j = 0; j < views.length; j++) { -                        let srcDoc = views[j].props.Document; -                        let dstDoc = views[i].props.Document; +                        let srcDoc = views[j]; +                        let dstDoc = views[i];                          let x1 = srcDoc.GetNumber(KeyStore.X, 0);                          let x1w = srcDoc.GetNumber(KeyStore.Width, -1);                          let x2 = dstDoc.GetNumber(KeyStore.X, 0); @@ -53,7 +49,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP                              linkDoc.SetText(KeyStore.LinkDescription, "Brush between " + srcTarg.Title + " and " + dstTarg.Title);                              linkDoc.SetData(KeyStore.BrushingDocs, [dstTarg, srcTarg], ListField); -                            brushAction = brushAction = (field: ListField<Document>) => { +                            brushAction = (field: ListField<Document>) => {                                  if (findBrush(field) === -1) {                                      console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title);                                      (findBrush(field) === -1) && field.Data.push(linkDoc); @@ -67,6 +63,11 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP                  }              });      } +    componentWillUnmount() { +        if (this._brushReactionDisposer) { +            this._brushReactionDisposer(); +        } +    }      documentAnchors(view: DocumentView) {          let equalViews = [view];          let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document); @@ -83,19 +84,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP              let targetViews = this.documentAnchors(connection.b);              let possiblePairs: { a: Document, b: Document, }[] = [];              srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document }))); -            possiblePairs.map(possiblePair => { -                if (!drawnPairs.reduce((found, drawnPair) => { +            possiblePairs.map(possiblePair => +                drawnPairs.reduce((found, drawnPair) => {                      let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b); -                    if (match) { -                        if (!drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) { -                            drawnPair.l.push(connection.l); -                        } +                    if (match && !drawnPair.l.reduce((found, link) => found || link.Id === connection.l.Id, false)) { +                        drawnPair.l.push(connection.l);                      }                      return match || found; -                }, false)) { -                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }); -                } -            }); +                }, false) +                || +                drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] }) +            );              return drawnPairs;          }, [] as { a: Document, b: Document, l: Document[] }[]);          return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss new file mode 100644 index 000000000..c5b8fc5e8 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss @@ -0,0 +1,24 @@ +@import "globalCssVariables"; + +.collectionFreeFormRemoteCursors-cont { +     +    position:absolute; +    z-index: $remoteCursors-zindex; +    transform-origin: 'center center'; +} +.collectionFreeFormRemoteCursors-canvas { +     +    position:absolute; +    width: 20px; +    height: 20px; +    opacity: 0.5; +    border-radius: 50%; +    border: 2px solid black; +} +.collectionFreeFormRemoteCursors-symbol { +    font-size: 14; +    color: black; +    // fontStyle: "italic", +    margin-left: -12; +    margin-top: 4; +}
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx index fc832264d..cf0a6de00 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx @@ -12,7 +12,7 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV      protected getCursors(): CursorEntry[] {          let doc = this.props.Document;          let id = CurrentUserUtils.id; -        let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []); +        let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]);          let notMe = cursors.filter(entry => entry.Data[0][0] !== id);          return id ? notMe : [];      } @@ -59,37 +59,17 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV                  let point = entry.Data[1];                  this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22");                  return ( -                    <div -                        key={id} -                        style={{ -                            position: "absolute", -                            transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`, -                            zIndex: 10000, -                            transformOrigin: 'center center', -                        }} +                    <div key={id} className="collectionFreeFormRemoteCursors-cont" +                        style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }}                      > -                        <canvas +                        <canvas className="collectionFreeFormRemoteCursors-canvas"                              ref={(el) => { if (el) this.crosshairs = el; }}                              width={20}                              height={20} -                            style={{ -                                position: 'absolute', -                                width: "20px", -                                height: "20px", -                                opacity: 0.5, -                                borderRadius: "50%", -                                border: "2px solid black" -                            }}                          /> -                        <p -                            style={{ -                                fontSize: 14, -                                color: "black", -                                // fontStyle: "italic", -                                marginLeft: -12, -                                marginTop: 4 -                            }} -                        >{email[0].toUpperCase()}</p> +                        <p className="collectionFreeFormRemoteCursors-symbol"> +                            {email[0].toUpperCase()} +                        </p>                      </div>                  );              } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 81f2146e4..57706b51e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,13 +1,6 @@ -@import "../../global_variables"; -.collectionfreeformview-measure { -    position: absolute; -    top: 0; -    left: 0; -    width: 100%; -    height: 100%; -  } +@import "../../globalCssVariables";  .collectionfreeformview { -  position: absolute; +  position: inherit;    top: 0;    left: 0;    width: 100%; @@ -16,7 +9,7 @@  }  .collectionfreeformview-container {    .collectionfreeformview > .jsx-parser { -    position: absolute; +    position: inherit;      height: 100%;      width: 100%;    } @@ -28,8 +21,10 @@      // background-size: 30px 30px;    // } +  border-width: $COLLECTION_BORDER_WIDTH;    box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw; -  border: 0px solid $light-color-secondary; +  border-color:  $light-color-secondary; +  border-style: solid;     border-radius: $border-radius;    box-sizing: border-box;    position: absolute; @@ -41,18 +36,21 @@  }  .collectionfreeformview-overlay {    .collectionfreeformview > .jsx-parser { -    position: absolute; +    position: inherit;      height: 100%;    }    .formattedTextBox-cont {      background: $light-color-secondary; +    overflow: visible;    }    opacity: 0.99; -  border: 0px solid transparent; +  border-width: 0;  +  border-color: transparent; +  border-style: solid;     border-radius: $border-radius;    box-sizing: border-box; -  position:absolute; +  position: absolute;    overflow: hidden;    top: 0;    left: 0; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index fb3304e1f..ed33a6eb9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,23 +1,27 @@ -import { action, computed, observable, trace, ObservableSet, runInAction } from "mobx"; +import { action, computed, observable, trace } from "mobx";  import { observer } from "mobx-react";  import { Document } from "../../../../fields/Document"; -import { FieldWaiting } from "../../../../fields/Field";  import { KeyStore } from "../../../../fields/KeyStore"; -import { TextField } from "../../../../fields/TextField"; +import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; +import { DocumentManager } from "../../../util/DocumentManager";  import { DragManager } from "../../../util/DragManager"; +import { SelectionManager } from "../../../util/SelectionManager";  import { Transform } from "../../../util/Transform";  import { undoBatch } from "../../../util/UndoManager"; +import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";  import { InkingCanvas } from "../../InkingCanvas"; +import { MainOverlayTextBox } from "../../MainOverlayTextBox";  import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";  import { DocumentContentsView } from "../../nodes/DocumentContentsView";  import { DocumentViewProps } from "../../nodes/DocumentView"; -import { COLLECTION_BORDER_WIDTH } from "../CollectionBaseView";  import { CollectionSubView } from "../CollectionSubView";  import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView"; +import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";  import "./CollectionFreeFormView.scss";  import { MarqueeView } from "./MarqueeView";  import React = require("react");  import v5 = require("uuid/v5"); +<<<<<<< HEAD  import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";  import { PreviewCursor } from "./PreviewCursor";  import { Timeline } from "../../nodes/Timeline"; @@ -26,34 +30,44 @@ import { SelectionManager } from "../../../util/SelectionManager";  import { NumberField } from "../../../../fields/NumberField";  import { Main } from "../../Main";  import Measure from "react-measure"; +======= +import { BooleanField } from "../../../../fields/BooleanField"; +>>>>>>> e47656cdc18aa1fd801a3853fa0f819140a68646  @observer  export class CollectionFreeFormView extends CollectionSubView { -    public _canvasRef = React.createRef<HTMLDivElement>();      private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) +    private _lastX: number = 0; +    private _lastY: number = 0; +    private get _pwidth() { return this.props.PanelWidth(); } +    private get _pheight() { return this.props.PanelHeight(); } -    public addLiveTextBox = (newBox: Document) => { -        // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself and receive text input -        this._selectOnLoaded = newBox.Id; +    @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } +    @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } +    private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } +    private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's? +    private childViews = () => this.views; +    private panX = () => this.props.Document.GetNumber(KeyStore.PanX, 0); +    private panY = () => this.props.Document.GetNumber(KeyStore.PanY, 0); +    private zoomScaling = () => this.props.Document.GetNumber(KeyStore.Scale, 1); +    private centeringShiftX = () => !this.nativeWidth ? this._pwidth / 2 : 0;  // shift so pan position is at center of window for non-overlay collections +    private centeringShiftY = () => !this.nativeHeight ? this._pheight / 2 : 0;// shift so pan position is at center of window for non-overlay collections +    private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform()); +    private getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); +    private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); +    private addLiveTextBox = (newBox: Document) => { +        this._selectOnLoaded = newBox.Id;// track the new text box so we can give it a prop that tells it to focus itself when it's displayed          this.addDocument(newBox, false);      } - -    public addDocument = (newBox: Document, allowDuplicates: boolean) => { -        let added = this.props.addDocument(newBox, false); -        this.bringToFront(newBox); -        return added; +    private addDocument = (newBox: Document, allowDuplicates: boolean) => { +        newBox.SetNumber(KeyStore.Zoom, this.props.Document.GetNumber(KeyStore.Scale, 1)); +        return this.props.addDocument(this.bringToFront(newBox), false);      } - -    public selectDocuments = (docs: Document[]) => { +    private selectDocuments = (docs: Document[]) => {          SelectionManager.DeselectAll; -        docs.map(doc => { -            const dv = DocumentManager.Instance.getDocumentView(doc); -            if (dv) { -                SelectionManager.SelectDoc(dv, true); -            } -        }); +        docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => +            SelectionManager.SelectDoc(dv!, true));      } -      public getActiveDocuments = () => {          var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);          return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => { @@ -65,61 +79,37 @@ export class CollectionFreeFormView extends CollectionSubView {          }, [] as Document[]);      } -    @observable public DownX: number = 0; -    @observable public DownY: number = 0; -    @observable private _lastX: number = 0; -    @observable private _lastY: number = 0; -    @observable private _pwidth: number = 0; -    @observable private _pheight: number = 0; - -    @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 && 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._pwidth / 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._pheight / 2 : 0; }// shift so pan position is at center of window for non-overlay collections -      @undoBatch      @action      drop = (e: Event, de: DragManager.DropEvent) => { -        if (super.drop(e, de)) { -            if (de.data instanceof DragManager.DocumentDragData) { -                let droppedDocs = de.data.droppedDocuments; -                let xoff = de.data.xOffset as number || 0; -                let yoff = de.data.yOffset as number || 0; -                if (droppedDocs.length) { -                    let screenX = de.x - xoff; -                    let screenY = de.y - yoff; -                    const [x, y] = this.getTransform().transformPoint(screenX, screenY); -                    let dragDoc = droppedDocs[0]; -                    let dragX = dragDoc.GetNumber(KeyStore.X, 0); -                    let dragY = dragDoc.GetNumber(KeyStore.Y, 0); -                    droppedDocs.map(async d => { -                        let docX = d.GetNumber(KeyStore.X, 0); -                        let docY = d.GetNumber(KeyStore.Y, 0); -                        d.SetNumber(KeyStore.X, x + (docX - dragX)); -                        d.SetNumber(KeyStore.Y, y + (docY - dragY)); -                        let docW = await d.GetTAsync(KeyStore.Width, NumberField); -                        let docH = await d.GetTAsync(KeyStore.Height, NumberField); -                        if (!docW) { +        if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) { +            const [x, y] = this.getTransform().transformPoint(de.x - de.data.xOffset, de.y - de.data.yOffset); +            if (de.data.droppedDocuments.length) { +                let dragDoc = de.data.droppedDocuments[0]; +                let dropX = dragDoc.GetNumber(KeyStore.X, 0); +                let dropY = dragDoc.GetNumber(KeyStore.Y, 0); +                de.data.droppedDocuments.map(d => { +                    d.SetNumber(KeyStore.X, x + (d.GetNumber(KeyStore.X, 0)) - dropX); +                    d.SetNumber(KeyStore.Y, y + (d.GetNumber(KeyStore.Y, 0)) - dropY); +                    if (!d.GetBoolean(KeyStore.IsMinimized, false)) { +                        if (!d.GetNumber(KeyStore.Width, 0)) {                              d.SetNumber(KeyStore.Width, 300);                          } -                        if (!docH) { -                            d.SetNumber(KeyStore.Height, 300); +                        if (!d.GetNumber(KeyStore.Height, 0)) { +                            let nw = d.GetNumber(KeyStore.NativeWidth, 0); +                            let nh = d.GetNumber(KeyStore.NativeHeight, 0); +                            d.SetNumber(KeyStore.Height, nw && nh ? nh / nw * d.Width() : 300);                          } -                        this.bringToFront(d); -                    }); -                } +                    } +                    this.bringToFront(d); +                }); +                SelectionManager.ReselectAll();              }              return true;          }          return false;      } -      @action      cleanupInteractions = () => {          document.removeEventListener("pointermove", this.onPointerMove); @@ -128,16 +118,17 @@ export class CollectionFreeFormView extends CollectionSubView {      @action      onPointerDown = (e: React.PointerEvent): void => { -        if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling !== 1)) || e.button === 0) && this.props.active()) { +        let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { +            var dv = DocumentManager.Instance.getDocumentView(doc); +            return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); +        }, false); +        if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || (e.button === 0 && e.altKey)) && (childSelected || this.props.active())) {              document.removeEventListener("pointermove", this.onPointerMove);              document.addEventListener("pointermove", this.onPointerMove);              document.removeEventListener("pointerup", this.onPointerUp);              document.addEventListener("pointerup", this.onPointerUp); -            this._lastX = this.DownX = e.pageX; -            this._lastY = this.DownY = e.pageY; -            if (this.props.isSelected()) { -                e.stopPropagation(); -            } +            this._lastX = e.pageX; +            this._lastY = e.pageY;          }      } @@ -150,61 +141,84 @@ export class CollectionFreeFormView extends CollectionSubView {      @action      onPointerMove = (e: PointerEvent): void => { -        if (!e.cancelBubble && this.props.active()) { -            if ((!this.isAnnotationOverlay || this.zoomScaling !== 1) && !e.shiftKey) { -                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.SetPan(x - dx, y - dy); -                this._lastX = e.pageX; -                this._lastY = e.pageY; -                e.stopPropagation(); -                e.preventDefault(); +        if (!e.cancelBubble) { +            let x = this.props.Document.GetNumber(KeyStore.PanX, 0); +            let y = this.props.Document.GetNumber(KeyStore.PanY, 0); +            let docs = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); +            let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); +            if (!this.isAnnotationOverlay) { +                let minx = docs.length ? docs[0].GetNumber(KeyStore.X, 0) : 0; +                let maxx = docs.length ? docs[0].Width() + minx : minx; +                let miny = docs.length ? docs[0].GetNumber(KeyStore.Y, 0) : 0; +                let maxy = docs.length ? docs[0].Height() + miny : miny; +                let ranges = docs.filter(doc => doc).reduce((range, doc) => { +                    let x = doc.GetNumber(KeyStore.X, 0); +                    let xe = x + doc.GetNumber(KeyStore.Width, 0); +                    let y = doc.GetNumber(KeyStore.Y, 0); +                    let ye = y + doc.GetNumber(KeyStore.Height, 0); +                    return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]], +                    [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]]; +                }, [[minx, maxx], [miny, maxy]]); +                let panelwidth = this._pwidth / this.zoomScaling() / 2; +                let panelheight = this._pheight / this.zoomScaling() / 2; +                if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx; +                if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx; +                if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy; +                if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;              } +            this.setPan(x - dx, y - dy); +            this._lastX = e.pageX; +            this._lastY = e.pageY; +            e.stopPropagation(); +            e.preventDefault();          }      }      @action      onPointerWheel = (e: React.WheelEvent): void => { -        this.props.select(false); +        // if (!this.props.active()) { +        //     return; +        // } +        let childSelected = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((childSelected, doc) => { +            var dv = DocumentManager.Instance.getDocumentView(doc); +            return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); +        }, false); +        if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) { +            return; +        }          e.stopPropagation(); -        let coefficient = 1000; +        const coefficient = 1000;          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); +            this.props.Document.SetNumber(KeyStore.NativeWidth, this.nativeWidth * deltaScale); +            this.props.Document.SetNumber(KeyStore.NativeHeight, this.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; +            if (deltaScale < 0) deltaScale = -deltaScale; +            if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { +                deltaScale = 1 / this.zoomScaling();              } -            let [x, y] = transform.transformPoint(e.clientX, e.clientY); - -            let localTransform = this.getLocalTransform(); -            localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y); -            // console.log(localTransform) +            let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); +            let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); -            this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale); -            this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale); +            let safeScale = Math.abs(localTransform.Scale); +            this.props.Document.SetNumber(KeyStore.Scale, Math.abs(safeScale)); +            this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); +            e.stopPropagation();          }      }      @action -    private SetPan(panX: number, panY: number) { -        Main.Instance.SetTextDoc(undefined, undefined); -        var x1 = this.getLocalTransform().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)); +    setPan(panX: number, panY: number) { +        MainOverlayTextBox.Instance.SetTextDoc(); +        var scale = this.getLocalTransform().inverse().Scale; +        const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); +        const newPanY = Math.min((1 - 1 / scale) * 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);      } @@ -220,39 +234,18 @@ export class CollectionFreeFormView extends CollectionSubView {      @action      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; -            } +        this.props.Document.GetList(this.props.fieldKey, [] as Document[]).slice().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); -        }); -    } - -    @computed get backgroundLayout(): string | undefined { -        let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField); -        if (field && field !== FieldWaiting) { -            return field.Data; -        } -    } -    @computed get overlayLayout(): string | undefined { -        let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField); -        if (field && field !== FieldWaiting) { -            return field.Data; -        } +        }).map((doc, index) => doc.SetNumber(KeyStore.ZIndex, index + 1)); +        return doc;      }      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.setPan( +            doc.GetNumber(KeyStore.X, 0) + doc.Width() / 2, +            doc.GetNumber(KeyStore.Y, 0) + doc.Height() / 2);          this.props.focus(this.props.Document);      } @@ -267,52 +260,101 @@ export class CollectionFreeFormView extends CollectionSubView {              selectOnLoad: document.Id === this._selectOnLoaded,              PanelWidth: document.Width,              PanelHeight: document.Height, -            ContentScaling: this.noScaling, -            ContainingCollectionView: undefined, +            ContentScaling: returnOne, +            ContainingCollectionView: this.props.CollectionView,              focus: this.focusDocument,              parentActive: this.props.active, -            onActiveChanged: this.props.active, +            whenActiveChanged: this.props.active,          };      }      @computed      get views() {          var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1); -        return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => { +        let docviews = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {              var page = doc.GetNumber(KeyStore.Page, -1);              if (page === curPage || page === -1) { -                prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />); +                let minim = doc.GetT(KeyStore.IsMinimized, BooleanField); +                if (minim === undefined || (minim && !minim.Data)) +                    prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);              }              return prev;          }, [] as JSX.Element[]); +<<<<<<< HEAD +======= + +        setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way .... + +        return docviews; +>>>>>>> e47656cdc18aa1fd801a3853fa0f819140a68646      } -    @computed -    get backgroundView() { -        return !this.backgroundLayout ? (null) : -            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)} -                layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); +    @action +    onCursorMove = (e: React.PointerEvent) => { +        super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));      } -    @computed -    get overlayView() { -        return !this.overlayLayout ? (null) : -            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)} -                layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />); + +    render() { +        const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; +        return ( +            <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} +                onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > +                <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} +                    addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} +                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> +                    <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} +                        zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> +                        <CollectionFreeFormBackgroundView {...this.getDocumentViewProps(this.props.Document)} /> +                        <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> +                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > +                                {this.childViews} +                            </InkingCanvas> +                        </CollectionFreeFormLinksView> +                        <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> +                    </CollectionFreeFormViewPannableContents> +                    <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} /> +                </MarqueeView> +            </div> +        );      } +} -    getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform()); -    getContainerTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH); -    getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.scale).translate(this.panX, this.panY); -    noScaling = () => 1; -    childViews = () => this.views; +@observer +class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> { +    @computed get overlayView() { +        let overlayLayout = this.props.Document.GetText(KeyStore.OverlayLayout, ""); +        return !overlayLayout ? (null) : +            (<DocumentContentsView {...this.props} layoutKey={KeyStore.OverlayLayout} +                isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); +    } +    render() { +        return this.overlayView; +    } +} +@observer +class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps> { +    @computed get backgroundView() { +        let backgroundLayout = this.props.Document.GetText(KeyStore.BackgroundLayout, ""); +        return !backgroundLayout ? (null) : +            (<DocumentContentsView {...this.props} layoutKey={KeyStore.BackgroundLayout} +                isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />); +    }      render() { -        let [dx, dy] = [this.centeringShiftX, this.centeringShiftY]; +        return this.backgroundView; +    } +} -        const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); -        const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); +interface CollectionFreeFormViewPannableContentsProps { +    centeringShiftX: () => number; +    centeringShiftY: () => number; +    panX: () => number; +    panY: () => number; +    zoomScaling: () => number; +} +<<<<<<< HEAD          return (              <Measure onResize={(r: any) => runInAction(() => { this._pwidth = r.entry.width; this._pheight = r.entry.height; })}>                  {({ measureRef }) => ( @@ -345,5 +387,18 @@ export class CollectionFreeFormView extends CollectionSubView {                      </div>)}              </Measure>          ); +======= +@observer +class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ +    render() { +        const cenx = this.props.centeringShiftX(); +        const ceny = this.props.centeringShiftY(); +        const panx = -this.props.panX(); +        const pany = -this.props.panY(); +        const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire +        return <div className="collectionfreeformview" style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> +            {this.props.children} +        </div>; +>>>>>>> e47656cdc18aa1fd801a3853fa0f819140a68646      }  }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index 0b406e722..ae0a9fd48 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -1,6 +1,6 @@  .marqueeView { -    position: absolute; +    position: inherit;      top:0;      left:0;      width:100%; @@ -13,4 +13,14 @@      border-width: 1px;      border-color: black;      pointer-events: none; +    .marquee-legend { +        bottom:-18px; +        left:0; +        position: absolute; +        font-size: 9; +        white-space:nowrap; +    } +    .marquee-legend::after { +        content: "Press: C (collection), or Delete" +    }  }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1e6faafb3..bf918beba 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable, trace } from "mobx"; +import { action, computed, observable } from "mobx";  import { observer } from "mobx-react";  import { Document } from "../../../../fields/Document";  import { FieldWaiting } from "../../../../fields/Field"; @@ -7,10 +7,12 @@ import { KeyStore } from "../../../../fields/KeyStore";  import { Documents } from "../../../documents/Documents";  import { SelectionManager } from "../../../util/SelectionManager";  import { Transform } from "../../../util/Transform"; +import { undoBatch } from "../../../util/UndoManager";  import { InkingCanvas } from "../../InkingCanvas"; +import { PreviewCursor } from "../../PreviewCursor";  import { CollectionFreeFormView } from "./CollectionFreeFormView"; +import { MINIMIZED_ICON_SIZE } from '../../../views/globalCssVariables.scss'  import "./MarqueeView.scss"; -import { PreviewCursor } from "./PreviewCursor";  import React = require("react");  interface MarqueeViewProps { @@ -21,6 +23,7 @@ interface MarqueeViewProps {      activeDocuments: () => Document[];      selectDocuments: (docs: Document[]) => void;      removeDocument: (doc: Document) => boolean; +    addLiveTextDocument: (doc: Document) => void;  }  @observer @@ -32,6 +35,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>      @observable _downY: number = 0;      @observable _used: boolean = false;      @observable _visible: boolean = false; +    _showOnUp: boolean = false;      static DRAG_THRESHOLD = 4;      @action @@ -47,11 +51,31 @@ export class MarqueeView extends React.Component<MarqueeViewProps>      }      @action +    onKeyPress = (e: KeyboardEvent) => { +        // Mixing events between React and Native is finicky.  In FormattedTextBox, we set the +        // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore +        // the keyPress here. +        //if not these keys, make a textbox if preview cursor is active! +        if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { +            //make textbox and add it to this collection +            let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); +            let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); +            this.props.addLiveTextDocument(newBox); +            PreviewCursor.Visible = false; +            e.stopPropagation(); +        } +    } +    hideCursor = () => { +        document.removeEventListener("keypress", this.onKeyPress, false); +    } +    @action      onPointerDown = (e: React.PointerEvent): void => {          if (e.buttons === 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) {              this._downX = this._lastX = e.pageX;              this._downY = this._lastY = e.pageY;              this._used = false; +            this._showOnUp = true; +            document.removeEventListener("keypress", this.onKeyPress, false);              document.addEventListener("pointermove", this.onPointerMove, true);              document.addEventListener("pointerup", this.onPointerUp, true);              document.addEventListener("keydown", this.marqueeCommand, true); @@ -63,6 +87,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>          this._lastX = e.pageX;          this._lastY = e.pageY;          if (!e.cancelBubble) { +            if (Math.abs(this._downX - e.clientX) > 4 || Math.abs(this._downY - e.clientY) > 4) { +                this._showOnUp = false; +                PreviewCursor.Visible = false; +            }              if (!this._used && e.buttons === 1 && !e.altKey && !e.metaKey &&                  (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) {                  this._visible = true; @@ -76,11 +104,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps>      onPointerUp = (e: PointerEvent): void => {          this.cleanupInteractions(true);          this._visible = false; -        let mselect = this.marqueeSelect(); -        if (!e.shiftKey) { -            SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); +        if (this._showOnUp) { +            PreviewCursor.Show(this.hideCursor, this._downX, this._downY); +            document.addEventListener("keypress", this.onKeyPress, false); +        } else { +            let mselect = this.marqueeSelect(); +            if (!e.shiftKey) { +                SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); +            } +            this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);          } -        this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);      }      intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -97,6 +130,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>          return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };      } +    @undoBatch      @action      marqueeCommand = (e: KeyboardEvent) => {          if (e.key === "Backspace" || e.key === "Delete") { @@ -114,7 +148,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps>                  d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);                  d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);                  d.SetNumber(KeyStore.Page, -1); -                d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));                  return d;              });              let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); @@ -127,7 +160,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps>                  pany: 0,                  width: bounds.width,                  height: bounds.height, -                backgroundColor: "Transparent",                  ink: inkData ? this.marqueeInkSelect(inkData) : undefined,                  title: "a nested collection"              }); @@ -176,10 +208,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>          let selRect = this.Bounds;          let selection: Document[] = [];          this.props.activeDocuments().map(doc => { +            var z = doc.GetNumber(KeyStore.Zoom, 1);              var x = doc.GetNumber(KeyStore.X, 0);              var y = doc.GetNumber(KeyStore.Y, 0); -            var w = doc.GetNumber(KeyStore.Width, 0); -            var h = doc.GetNumber(KeyStore.Height, 0); +            var w = doc.Width() / z; +            var h = doc.Height() / z;              if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {                  selection.push(doc);              } @@ -191,7 +224,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>      get marqueeDiv() {          let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);          let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY); -        return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />; +        return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} > +            <span className="marquee-legend" /> +        </div>;      }      render() { diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss deleted file mode 100644 index 7a67c29bf..000000000 --- a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss +++ /dev/null @@ -1,27 +0,0 @@ - -.previewCursor { -    color: black; -    position: absolute; -    transform-origin: left top; -    top: 0; -    left:0; -    pointer-events: none; -} -.previewCursorView { -    top: 0; -    left:0; -    position: absolute; -    width:100%; -    height:100%; -} - -//this is an animation for the blinking cursor! -// @keyframes blink { -// 	0% {opacity: 0} -// 	49%{opacity: 0} -// 	50% {opacity: 1} -// } - -// #previewCursor { -//      animation: blink 1s infinite; -// }
\ No newline at end of file diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx deleted file mode 100644 index 8eabb020a..000000000 --- a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { action, observable, trace, computed, reaction } from "mobx"; -import { observer } from "mobx-react"; -import { Document } from "../../../../fields/Document"; -import { Documents } from "../../../documents/Documents"; -import { Transform } from "../../../util/Transform"; -import { CollectionFreeFormView } from "./CollectionFreeFormView"; -import "./PreviewCursor.scss"; -import React = require("react"); -import { interfaceDeclaration } from "babel-types"; - - -export interface PreviewCursorProps { -    getTransform: () => Transform; -    getContainerTransform: () => Transform; -    container: CollectionFreeFormView; -    addLiveTextDocument: (doc: Document) => void; -} - -@observer -export class PreviewCursor extends React.Component<PreviewCursorProps>  { -    @observable _lastX: number = 0; -    @observable _lastY: number = 0; -    @observable public _visible: boolean = false; -    @observable public DownX: number = 0; -    @observable public DownY: number = 0; -    _showOnUp: boolean = false; - -    @action -    cleanupInteractions = () => { -        document.removeEventListener("pointerup", this.onPointerUp, true); -        document.removeEventListener("pointermove", this.onPointerMove, true); -    } - -    @action -    onPointerDown = (e: React.PointerEvent) => { -        if (e.button === 0 && this.props.container.props.active()) { -            document.removeEventListener("keypress", this.onKeyPress, false); -            this._showOnUp = true; -            this.DownX = e.pageX; -            this.DownY = e.pageY; -            document.addEventListener("pointerup", this.onPointerUp, true); -            document.addEventListener("pointermove", this.onPointerMove, true); -        } -    } -    @action -    onPointerMove = (e: PointerEvent): void => { -        if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) { -            this._showOnUp = false; -            this._visible = false; -        } -    } - -    @action -    onPointerUp = (e: PointerEvent): void => { -        if (this._showOnUp) { -            document.addEventListener("keypress", this.onKeyPress, false); -            this._lastX = this.DownX; -            this._lastY = this.DownY; -            this._visible = true; -        } -        this.cleanupInteractions(); -    } - -    @action -    onKeyPress = (e: KeyboardEvent) => { -        // Mixing events between React and Native is finicky.  In FormattedTextBox, we set the -        // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore -        // the keyPress here. -        //if not these keys, make a textbox if preview cursor is active! -        if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { -            //make textbox and add it to this collection -            let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY); -            let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); -            this.props.addLiveTextDocument(newBox); -            document.removeEventListener("keypress", this.onKeyPress, false); -            this._visible = false; -            e.stopPropagation(); -        } -    } - -    getPoint = () => this.props.getContainerTransform().transformPoint(this._lastX, this._lastY); -    getVisible = () => this._visible; -    setVisible = (v: boolean) => { -        this._visible = v; -        document.removeEventListener("keypress", this.onKeyPress, false); -    } -    render() { -        return ( -            <div className="previewCursorView" onPointerDown={this.onPointerDown}> -                {this.props.children} -                <PreviewCursorPrompt setVisible={this.setVisible} getPoint={this.getPoint} getVisible={this.getVisible} /> -            </div> -        ); -    } -} - -export interface PromptProps { -    getPoint: () => number[]; -    getVisible: () => boolean; -    setVisible: (v: boolean) => void; -} - -@observer -export class PreviewCursorPrompt extends React.Component<PromptProps> { -    private _promptRef = React.createRef<HTMLDivElement>(); - -    //when focus is lost, this will remove the preview cursor -    @action onBlur = (): void => this.props.setVisible(false); - -    render() { -        let p = this.props.getPoint(); -        if (this.props.getVisible() && this._promptRef.current) { -            this._promptRef.current.focus(); -        } -        return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._promptRef} -            style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, opacity: this.props.getVisible() ? 1 : 0 }}> -            I -        </div >; -    } -}
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss new file mode 100644 index 000000000..4f68b71b0 --- /dev/null +++ b/src/client/views/globalCssVariables.scss @@ -0,0 +1,33 @@ +@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700"); +// colors +$light-color: #fcfbf7; +$light-color-secondary: rgb(241, 239, 235); +$main-accent: #61aaa3; +// $alt-accent: #cdd5ec; +// $alt-accent: #cdeceb; +$alt-accent: #59dff7; +$lighter-alt-accent: rgb(207, 220, 240); +$intermediate-color: #9c9396; +$dark-color: #121721; +// fonts +$sans-serif: "Noto Sans", sans-serif; +// $sans-serif: "Roboto Slab", sans-serif; +$serif: "Crimson Text", serif; +// misc values +$border-radius: 0.3em; +// + +                            // dragged items +$contextMenu-zindex: 1000;  // context menu shows up over everything +$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc +$docDecorations-zindex: 998; // then doc decorations appear over everything else +$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? +$COLLECTION_BORDER_WIDTH: 1; +$MINIMIZED_ICON_SIZE:25; +$MAX_ROW_HEIGHT: 44px; +:export  { +    contextMenuZindex: $contextMenu-zindex; +    COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH; +    MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; +    MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; +}
\ No newline at end of file diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts new file mode 100644 index 000000000..9788d31f7 --- /dev/null +++ b/src/client/views/globalCssVariables.scss.d.ts @@ -0,0 +1,10 @@ + +interface IGlobalScss { +    contextMenuZindex: string;  // context menu shows up over everything +    COLLECTION_BORDER_WIDTH: string; +    MINIMIZED_ICON_SIZE: string; +    MAX_ROW_HEIGHT: string; +} +declare const globalCssVariables: IGlobalScss; + +export = globalCssVariables;
\ No newline at end of file diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 77f41105f..01a9f26bf 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { computed } from "mobx"; +import { computed, trace } from "mobx";  import { observer } from "mobx-react";  import { KeyStore } from "../../../fields/KeyStore";  import { NumberField } from "../../../fields/NumberField"; @@ -6,28 +6,21 @@ import { Transform } from "../../util/Transform";  import { DocumentView, DocumentViewProps } from "./DocumentView";  import "./DocumentView.scss";  import React = require("react"); -import { thisExpression } from "babel-types"; +import { OmitKeys } from "../../../Utils"; +export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { +}  @observer -export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { +export class CollectionFreeFormDocumentView extends React.Component<CollectionFreeFormDocumentViewProps> {      private _mainCont = React.createRef<HTMLDivElement>(); -    constructor(props: DocumentViewProps) { -        super(props); -    } -    get screenRect(): ClientRect | DOMRect { -        if (this._mainCont.current) { -            return this._mainCont.current.getBoundingClientRect(); -        } -        return new DOMRect(); -    } -      @computed      get transform(): string { -        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)`; +        return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;      } +    @computed get zoom(): number { return 1 / this.props.Document.GetNumber(KeyStore.Zoom, 1); }      @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(); } @@ -52,31 +45,49 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView          this.props.Document.SetData(KeyStore.ZIndex, h, NumberField);      } -    contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; - +    get X() { +        return this.props.Document.GetNumber(KeyStore.X, 0); +    } +    get Y() { +        return this.props.Document.GetNumber(KeyStore.Y, 0); +    }      getTransform = (): Transform =>          this.props.ScreenToLocalTransform() -            .translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)) -            .scale(1 / this.contentScaling()) +            .translate(-this.X, -this.Y) +            .scale(1 / this.contentScaling()).scale(1 / this.zoom) + +    contentScaling = () => (this.nativeWidth > 0 ? this.width / this.nativeWidth : 1); +    panelWidth = () => this.props.PanelWidth(); +    panelHeight = () => this.props.PanelHeight();      @computed      get docView() { -        return <DocumentView {...this.props} +        return <DocumentView {...OmitKeys(this.props, ['zoomFade'])}              ContentScaling={this.contentScaling}              ScreenToLocalTransform={this.getTransform}              PanelWidth={this.panelWidth}              PanelHeight={this.panelHeight}          />;      } -    panelWidth = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelWidth(); -    panelHeight = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelHeight();      render() { +        let zoomFade = 1; +        //var zoom = doc.GetNumber(KeyStore.Zoom, 1); +        // let transform = this.getTransform().scale(this.contentScaling()).inverse(); +        // var [sptX, sptY] = transform.transformPoint(0, 0); +        // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); +        // let w = bptX - sptX; +        // //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; +        // let fadeUp = .75 * 1800; +        // let fadeDown = .075 * 1800; +        // zoomFade = w < fadeDown  /* || w > fadeUp */ ? Math.max(0, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; +          return (              <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ +                opacity: zoomFade,                  transformOrigin: "left top",                  transform: this.transform, -                pointerEvents: "all", +                pointerEvents: (zoomFade < 0.09 ? "none" : "all"),                  width: this.width,                  height: this.height,                  position: "absolute", diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 5836da396..07599c345 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -15,6 +15,7 @@ import { DocumentViewProps } from "./DocumentView";  import "./DocumentView.scss";  import { FormattedTextBox } from "./FormattedTextBox";  import { ImageBox } from "./ImageBox"; +import { IconBox } from "./IconBox";  import { KeyValueBox } from "./KeyValueBox";  import { PDFBox } from "./PDFBox";  import { VideoBox } from "./VideoBox"; @@ -23,7 +24,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";  import React = require("react");  import { Document } from "../../../fields/Document";  import { FieldViewProps } from "./FieldView"; -import { Without } from "../../../Utils"; +import { Without, OmitKeys } from "../../../Utils";  const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?  type BindingProps = Without<FieldViewProps, 'fieldKey'>; @@ -44,34 +45,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {      CreateBindings(): JsxBindings { -        let -            { -                Document, -                isSelected, -                select, -                isTopMost, -                selectOnLoad, -                ScreenToLocalTransform, -                addDocument, -                removeDocument, -                onActiveChanged, -                parentActive: active, -            } = this.props; -        let bindings: JsxBindings = { -            props: { -                Document, -                isSelected, -                select, -                isTopMost, -                selectOnLoad, -                ScreenToLocalTransform, -                active, -                onActiveChanged, -                addDocument, -                removeDocument, -                focus, -            } -        }; +        let bindings: JsxBindings = { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive) }; +          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          } @@ -88,7 +63,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {              return <p>Error loading layout keys</p>;          }          return <JsxParser -            components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }} +            components={{ FormattedTextBox, ImageBox, IconBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}              bindings={this.CreateBindings()}              jsx={this.layout}              showWarnings={true} diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 5126e69f9..7c72fb6e6 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -1,10 +1,11 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -.documentView-node { -  position: absolute; +.documentView-node, .documentView-node-topmost { +  position: inherit;    top: 0;    left:0; -  background: $light-color; //overflow: hidden; + // background: $light-color; //overflow: hidden; +  transform-origin: left top;    &.minimized {      width: 30px; @@ -12,7 +13,6 @@    }    .top { -    background: #232323;      height: 20px;      cursor: pointer;    } @@ -28,16 +28,6 @@      height: calc(100% - 20px);    }  } - -.minimized-box { -  height: 10px; -  width: 10px; -  border-radius: 2px; -  background: $dark-color -} - -.minimized-box:hover { -  background: $main-accent; -  transform: scale(1.15); -  cursor: pointer; +.documentView-node-topmost { +    background: white;  }
\ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 9670ca6b2..c47a56168 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,29 +1,32 @@ -import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx"; +import { action, computed, runInAction } from "mobx";  import { observer } from "mobx-react";  import { Document } from "../../../fields/Document"; -import { Field, FieldWaiting, Opt } from "../../../fields/Field"; +import { Field, Opt } from "../../../fields/Field";  import { Key } from "../../../fields/Key";  import { KeyStore } from "../../../fields/KeyStore";  import { ListField } from "../../../fields/ListField"; -import { BooleanField } from "../../../fields/BooleanField"; -import { TextField } from "../../../fields/TextField";  import { ServerUtils } from "../../../server/ServerUtil"; -import { Utils } from "../../../Utils"; +import { emptyFunction, Utils } from "../../../Utils";  import { Documents } from "../../documents/Documents";  import { DocumentManager } from "../../util/DocumentManager";  import { DragManager } from "../../util/DragManager";  import { SelectionManager } from "../../util/SelectionManager";  import { Transform } from "../../util/Transform"; +import { undoBatch, UndoManager } from "../../util/UndoManager";  import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView";  import { CollectionView } from "../collections/CollectionView";  import { ContextMenu } from "../ContextMenu";  import { DocumentContentsView } from "./DocumentContentsView";  import "./DocumentView.scss";  import React = require("react"); - +import { TextField } from "../../../fields/TextField"; +import { string } from "prop-types"; +import { NumberField } from "../../../fields/NumberField";  export interface DocumentViewProps { -    ContainingCollectionView: Opt<CollectionView>; +    ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;      Document: Document;      addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;      removeDocument?: (doc: Document) => boolean; @@ -36,7 +39,7 @@ export interface DocumentViewProps {      focus: (doc: Document) => void;      selectOnLoad: boolean;      parentActive: () => boolean; -    onActiveChanged: (isActive: boolean) => void; +    whenActiveChanged: (isActive: boolean) => void;  }  export interface JsxArgs extends DocumentViewProps {      Keys: { [name: string]: Key }; @@ -62,14 +65,12 @@ 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; +        Object.defineProperty(emptyFunction, "name", { value: key + "Key" }); +        Keys[key] = emptyFunction;      }      for (const field of fields) { -        let fn = () => { }; -        Object.defineProperty(fn, "name", { value: field }); -        Fields[field] = fn; +        Object.defineProperty(emptyFunction, "name", { value: field }); +        Fields[field] = emptyFunction;      }      let args: JsxArgs = {          Document: function Document() { }, @@ -82,21 +83,25 @@ export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {  @observer  export class DocumentView extends React.Component<DocumentViewProps> { -    private _mainCont = React.createRef<HTMLDivElement>(); -    public get ContentRef() { -        return this._mainCont; -    } +    static _incompleteAnimations: Map<string, boolean> = new Map<string, boolean>();      private _downX: number = 0;      private _downY: number = 0; +    private _mainCont = React.createRef<HTMLDivElement>(); +    private _dropDisposer?: DragManager.DragDropDisposer; + +    public get ContentDiv() { return this._mainCont.current; }      @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }      @computed get topMost(): boolean { return this.props.isTopMost; }      @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.button === 2 && !this.isSelected()) { +            return; +        }          if (e.shiftKey && e.buttons === 2) {              if (this.props.isTopMost) {                  this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); @@ -105,7 +110,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {              }              e.stopPropagation();          } else { -            if (this.active && !e.isDefaultPrevented()) { +            if (this.active) {                  e.stopPropagation();                  document.removeEventListener("pointermove", this.onPointerMove);                  document.addEventListener("pointermove", this.onPointerMove); @@ -115,11 +120,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {          }      } -    private dropDisposer?: DragManager.DragDropDisposer; -      componentDidMount() {          if (this._mainCont.current) { -            this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { +            this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {                  handlers: { drop: this.drop.bind(this) }              });          } @@ -127,29 +130,26 @@ export class DocumentView extends React.Component<DocumentViewProps> {      }      componentDidUpdate() { -        if (this.dropDisposer) { -            this.dropDisposer(); +        if (this._dropDisposer) { +            this._dropDisposer();          }          if (this._mainCont.current) { -            this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { +            this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {                  handlers: { drop: this.drop.bind(this) }              });          }      }      componentWillUnmount() { -        if (this.dropDisposer) { -            this.dropDisposer(); +        if (this._dropDisposer) { +            this._dropDisposer();          }          runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1));      }      startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {          if (this._mainCont.current) { -            const [left, top] = this.props -                .ScreenToLocalTransform() -                .inverse() -                .transformPoint(0, 0); +            const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);              let dragData = new DragManager.DocumentDragData([this.props.Document]);              dragData.aliasOnDrop = dropAliasOfDraggedDoc;              dragData.xOffset = x - left; @@ -157,7 +157,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {              dragData.moveDocument = this.props.moveDocument;              DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {                  handlers: { -                    dragComplete: action(() => { }) +                    dragComplete: action(emptyFunction)                  },                  hideSource: !dropAliasOfDraggedDoc              }); @@ -168,13 +168,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {          if (e.cancelBubble) {              return;          } -        if ( -            Math.abs(this._downX - e.clientX) > 3 || -            Math.abs(this._downY - e.clientY) > 3 -        ) { +        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.topMost || e.buttons === 2 || e.altKey) { +            if (!e.altKey && (!this.topMost || e.buttons === 2)) {                  this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey);              }          } @@ -185,21 +182,23 @@ export class DocumentView extends React.Component<DocumentViewProps> {          document.removeEventListener("pointermove", this.onPointerMove);          document.removeEventListener("pointerup", this.onPointerUp);          e.stopPropagation(); -        if (!SelectionManager.IsSelected(this) && -            Math.abs(e.clientX - this._downX) < 4 && -            Math.abs(e.clientY - this._downY) < 4 -        ) { -            SelectionManager.SelectDoc(this, e.ctrlKey); +        if (!SelectionManager.IsSelected(this) && e.button !== 2 && +            Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { +            this.props.Document.GetTAsync(KeyStore.MaximizedDoc, Document).then(maxdoc => { +                if (maxdoc instanceof Document) { +                    this.props.addDocument && this.props.addDocument(maxdoc, false); +                    this.toggleIcon(); +                } else +                    SelectionManager.SelectDoc(this, e.ctrlKey); +            });          }      } -    stopPropogation = (e: React.SyntheticEvent) => { +    stopPropagation = (e: React.SyntheticEvent) => {          e.stopPropagation();      }      deleteClicked = (): void => { -        if (this.props.removeDocument) { -            this.props.removeDocument(this.props.Document); -        } +        this.props.removeDocument && this.props.removeDocument(this.props.Document);      }      fieldsClicked = (e: React.MouseEvent): void => { @@ -208,70 +207,142 @@ export class DocumentView extends React.Component<DocumentViewProps> {          }      }      fullScreenClicked = (e: React.MouseEvent): void => { -        CollectionDockingView.Instance.OpenFullScreen(this.props.Document); +        CollectionDockingView.Instance.OpenFullScreen((this.props.Document.GetPrototype() as Document).MakeDelegate());          ContextMenu.Instance.clearItems(); -        ContextMenu.Instance.addItem({ -            description: "Close Full Screen", -            event: this.closeFullScreenClicked -        }); +        ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked });          ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);      }      closeFullScreenClicked = (e: React.MouseEvent): void => {          CollectionDockingView.Instance.CloseFullScreen();          ContextMenu.Instance.clearItems(); -        ContextMenu.Instance.addItem({ -            description: "Full Screen", -            event: this.fullScreenClicked -        }); +        ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked });          ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);      } +    @action createIcon = (layoutString: string): Document => { +        let iconDoc = Documents.IconDocument(layoutString); +        iconDoc.SetText(KeyStore.Title, "ICON" + this.props.Document.Title) +        iconDoc.SetBoolean(KeyStore.IsMinimized, false); +        iconDoc.SetNumber(KeyStore.NativeWidth, 0); +        iconDoc.SetNumber(KeyStore.NativeHeight, 0); +        iconDoc.SetNumber(KeyStore.X, this.props.Document.GetNumber(KeyStore.X, 0)); +        iconDoc.SetNumber(KeyStore.Y, this.props.Document.GetNumber(KeyStore.Y, 0) - 24); +        iconDoc.Set(KeyStore.Prototype, this.props.Document); +        iconDoc.Set(KeyStore.MaximizedDoc, this.props.Document); +        this.props.Document.Set(KeyStore.MinimizedDoc, iconDoc); +        this.props.addDocument && this.props.addDocument(iconDoc, false); +        return iconDoc; +    } + +    animateBetweenIcon(icon: number[], targ: number[], width: number, height: number, stime: number, target: Document, maximizing: boolean) { +        setTimeout(() => { +            let now = Date.now(); +            let progress = Math.min(1, (now - stime) / 200); +            let pval = maximizing ? +                [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : +                [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; +            target.SetNumber(KeyStore.Width, maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress); +            target.SetNumber(KeyStore.Height, maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress); +            target.SetNumber(KeyStore.X, pval[0]); +            target.SetNumber(KeyStore.Y, pval[1]); +            if (now < stime + 200) { +                this.animateBetweenIcon(icon, targ, width, height, stime, target, maximizing); +            } +            else { +                if (!maximizing) { +                    target.SetBoolean(KeyStore.IsMinimized, true); +                    target.SetNumber(KeyStore.X, targ[0]); +                    target.SetNumber(KeyStore.Y, targ[1]); +                    target.SetNumber(KeyStore.Width, width); +                    target.SetNumber(KeyStore.Height, height); +                } +                DocumentView._incompleteAnimations.set(target.Id, false); +            } +        }, +            2); +    } +      @action -    public minimize = (): void => { -        this.props.Document.SetData( -            KeyStore.Minimized, -            true as boolean, -            BooleanField -        ); +    public toggleIcon = async (): Promise<void> => {          SelectionManager.DeselectAll(); +        let isMinimized: boolean | undefined; +        let minDoc = await this.props.Document.GetTAsync(KeyStore.MinimizedDoc, Document); +        if (!minDoc) return; +        let minimizedDocSet = await minDoc.GetTAsync(KeyStore.LinkTags, ListField); +        if (!minimizedDocSet) return; +        minimizedDocSet.Data.map(async minimizedDoc => { +            if (minimizedDoc instanceof Document) { +                this.props.addDocument && this.props.addDocument(minimizedDoc, false); +                let maximizedDoc = await minimizedDoc.GetTAsync(KeyStore.MaximizedDoc, Document); +                if (maximizedDoc instanceof Document && !DocumentView._incompleteAnimations.get(maximizedDoc.Id)) { +                    DocumentView._incompleteAnimations.set(maximizedDoc.Id, true); +                    isMinimized = isMinimized === undefined ? maximizedDoc.GetBoolean(KeyStore.IsMinimized, false) : isMinimized; +                    maximizedDoc.SetBoolean(KeyStore.IsMinimized, false); +                    let minx = await minimizedDoc.GetTAsync(KeyStore.X, NumberField); +                    let miny = await minimizedDoc.GetTAsync(KeyStore.Y, NumberField); +                    let maxx = await maximizedDoc.GetTAsync(KeyStore.X, NumberField); +                    let maxy = await maximizedDoc.GetTAsync(KeyStore.Y, NumberField); +                    let maxw = await maximizedDoc.GetTAsync(KeyStore.Width, NumberField); +                    let maxh = await maximizedDoc.GetTAsync(KeyStore.Height, NumberField); +                    if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && +                        maxw !== undefined && maxh !== undefined) +                        this.animateBetweenIcon( +                            [minx.Data, miny.Data], [maxx.Data, maxy.Data], maxw.Data, maxh.Data, +                            Date.now(), maximizedDoc, isMinimized); +                } + +            } +        }) +    } + +    @action +    public getIconDoc = async (): Promise<Document | undefined> => { +        return await this.props.Document.GetTAsync(KeyStore.MinimizedDoc, Document).then(async mindoc => +            mindoc ? mindoc : +                await this.props.Document.GetTAsync(KeyStore.BackgroundLayout, TextField).then(async field => +                    (field instanceof TextField) ? this.createIcon(field.Data) : +                        await this.props.Document.GetTAsync(KeyStore.Layout, TextField).then(field => +                            (field instanceof TextField) ? this.createIcon(field.Data) : undefined)));      } +    @undoBatch      @action      drop = (e: Event, de: DragManager.DropEvent) => {          if (de.data instanceof DragManager.LinkDragData) { -            let sourceDoc: Document = de.data.linkSourceDocumentView.props.Document; +            let sourceDoc: Document = de.data.linkSourceDocument;              let destDoc: Document = this.props.Document; -            if (this.props.isTopMost) { -                return; -            }              let linkDoc: Document = new Document();              destDoc.GetTAsync(KeyStore.Prototype, Document).then(protoDest =>                  sourceDoc.GetTAsync(KeyStore.Prototype, Document).then(protoSrc =>                      runInAction(() => { -                        linkDoc.Set(KeyStore.Title, new TextField("New Link")); -                        linkDoc.Set(KeyStore.LinkDescription, new TextField("")); -                        linkDoc.Set(KeyStore.LinkTags, new TextField("Default")); +                        let batch = UndoManager.StartBatch("document view drop"); +                        linkDoc.SetText(KeyStore.Title, "New Link"); +                        linkDoc.SetText(KeyStore.LinkDescription, ""); +                        linkDoc.SetText(KeyStore.LinkTags, "Default");                          let dstTarg = protoDest ? protoDest : destDoc;                          let srcTarg = protoSrc ? protoSrc : sourceDoc;                          linkDoc.Set(KeyStore.LinkedToDocs, dstTarg);                          linkDoc.Set(KeyStore.LinkedFromDocs, srcTarg); -                        dstTarg.GetOrCreateAsync( +                        const prom1 = new Promise(resolve => dstTarg.GetOrCreateAsync(                              KeyStore.LinkedFromDocs,                              ListField,                              field => {                                  (field as ListField<Document>).Data.push(linkDoc); +                                resolve();                              } -                        ); -                        srcTarg.GetOrCreateAsync( +                        )); +                        const prom2 = new Promise(resolve => srcTarg.GetOrCreateAsync(                              KeyStore.LinkedToDocs,                              ListField,                              field => {                                  (field as ListField<Document>).Data.push(linkDoc); +                                resolve();                              } -                        ); +                        )); +                        Promise.all([prom1, prom2]).finally(() => batch.end());                      })                  )              ); @@ -280,11 +351,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {      }      onDrop = (e: React.DragEvent) => { -        if (e.isDefaultPrevented()) { -            return; -        }          let text = e.dataTransfer.getData("text/plain"); -        if (text && text.startsWith("<div")) { +        if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {              let oldLayout = this.props.Document.GetText(KeyStore.Layout, "");              let layout = text.replace("{layout}", oldLayout);              this.props.Document.SetText(KeyStore.Layout, layout); @@ -296,138 +364,51 @@ export class DocumentView extends React.Component<DocumentViewProps> {      @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()) { +        if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 || +            e.isDefaultPrevented()) {              e.preventDefault();              return;          }          e.preventDefault(); -        if (!this.isMinimized()) { -            ContextMenu.Instance.addItem({ -                description: "Minimize", -                event: this.minimize -            }); -        } -        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: "Copy URL", -            event: () => { -                Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)); -            } -        }); -        ContextMenu.Instance.addItem({ -            description: "Copy ID", -            event: () => { -                Utils.CopyText(this.props.Document.Id); -            } -        }); +        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: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) }); +        ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document.Id) });          //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) +        ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked });          ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); -        if (!this.topMost) { -            // DocumentViews should stop propagation of this event -            e.stopPropagation(); -        } - -        ContextMenu.Instance.addItem({ -            description: "Delete", -            event: this.deleteClicked -        }); -        ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); -        SelectionManager.SelectDoc(this, e.ctrlKey); -    } - -    isMinimized = () => { -        let field = this.props.Document.GetT(KeyStore.Minimized, BooleanField); -        if (field && field !== FieldWaiting) { -            return field.Data; -        } -    } - -    @action -    expand = () => { -        this.props.Document.SetData( -            KeyStore.Minimized, -            false as boolean, -            BooleanField -        ); +        if (!SelectionManager.IsSelected(this)) +            SelectionManager.SelectDoc(this, false);      }      isSelected = () => SelectionManager.IsSelected(this); +    select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); -    select = (ctrlPressed: boolean) => { -        SelectionManager.SelectDoc(this, ctrlPressed); -    } +    @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } +    @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } +    @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={KeyStore.Layout} />); } -    render() { -        if (!this.props.Document) { -            return null; -        } +    render() {          var scaling = this.props.ContentScaling(); -        var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0); -        var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0); +        var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%"; +        var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%"; -        if (this.isMinimized()) { -            return ( -                <div -                    className="minimized-box" -                    ref={this._mainCont} -                    style={{ -                        transformOrigin: "left top", -                        transform: `scale(${scaling} , ${scaling})` -                    }} -                    onClick={this.expand} -                    onDrop={this.onDrop} -                    onPointerDown={this.onPointerDown} -                /> -            ); -        } else { -            var backgroundcolor = this.props.Document.GetText( -                KeyStore.BackgroundColor, -                "" -            ); -            return ( -                <div -                    className="documentView-node" -                    ref={this._mainCont} -                    style={{ -                        background: backgroundcolor, -                        width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%", -                        height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%", -                        transformOrigin: "left top", -                        transform: `scale(${scaling} , ${scaling})` -                    }} -                    onDrop={this.onDrop} -                    onContextMenu={this.onContextMenu} -                    onPointerDown={this.onPointerDown} -                > -                    <DocumentContentsView -                        {...this.props} -                        isSelected={this.isSelected} -                        select={this.select} -                        layoutKey={KeyStore.Layout} -                    /> -                </div> -            ); -        } +        return ( +            <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} +                ref={this._mainCont} +                style={{ +                    background: this.props.Document.GetText(KeyStore.BackgroundColor, ""), +                    width: nativeWidth, height: nativeHeight, +                    transform: `scale(${scaling}, ${scaling})` +                }} +                onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} +            > +                {this.contents} +            </div> +        );      }  } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 07c5b332c..93e385821 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,7 +1,7 @@  import React = require("react");  import { observer } from "mobx-react";  import { computed } from "mobx"; -import { Field, FieldWaiting, FieldValue } from "../../../fields/Field"; +import { Field, FieldWaiting, FieldValue, Opt } from "../../../fields/Field";  import { Document } from "../../../fields/Document";  import { TextField } from "../../../fields/TextField";  import { NumberField } from "../../../fields/NumberField"; @@ -19,7 +19,12 @@ import { ListField } from "../../../fields/ListField";  import { DocumentContentsView } from "./DocumentContentsView";  import { Transform } from "../../util/Transform";  import { KeyStore } from "../../../fields/KeyStore"; -import { returnFalse } from "../../../Utils"; +import { returnFalse, emptyDocFunction, emptyFunction, returnOne } from "../../../Utils"; +import { CollectionView } from "../collections/CollectionView"; +import { CollectionPDFView } from "../collections/CollectionPDFView"; +import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { IconField } from "../../../fields/IconFIeld"; +import { IconBox } from "./IconBox";  // @@ -29,6 +34,7 @@ import { returnFalse } from "../../../Utils";  //  export interface FieldViewProps {      fieldKey: Key; +    ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;      Document: Document;      isSelected: () => boolean;      select: (isCtrlPressed: boolean) => void; @@ -39,7 +45,7 @@ export interface FieldViewProps {      moveDocument?: (document: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;      ScreenToLocalTransform: () => Transform;      active: () => boolean; -    onActiveChanged: (isActive: boolean) => void; +    whenActiveChanged: (isActive: boolean) => void;      focus: (doc: Document) => void;  } @@ -68,6 +74,9 @@ export class FieldView extends React.Component<FieldViewProps> {          else if (field instanceof ImageField) {              return <ImageBox {...this.props} />;          } +        else if (field instanceof IconField) { +            return <IconBox {...this.props} />; +        }          else if (field instanceof VideoField) {              return <VideoBox {...this.props} />;          } @@ -85,13 +94,13 @@ export class FieldView extends React.Component<FieldViewProps> {                      PanelHeight={() => 100}                      isTopMost={true} //TODO Why is this top most?                      selectOnLoad={false} -                    focus={() => { }} -                    isSelected={() => false} -                    select={() => false} +                    focus={emptyDocFunction} +                    isSelected={returnFalse} +                    select={returnFalse}                      layoutKey={KeyStore.Layout} -                    ContainingCollectionView={undefined} +                    ContainingCollectionView={this.props.ContainingCollectionView}                      parentActive={this.props.active} -                    onActiveChanged={this.props.onActiveChanged} /> +                    whenActiveChanged={this.props.whenActiveChanged} />              );          }          else if (field instanceof ListField) { @@ -111,7 +120,7 @@ export class FieldView extends React.Component<FieldViewProps> {          }          else {              return <p> {"Waiting for server..."} </p>; - } +        }      }  }
\ No newline at end of file diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 32da2632e..5eb2bf7ce 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -1,4 +1,4 @@ -@import "../global_variables"; +@import "../globalCssVariables";  .ProseMirror {    width: 100%;    height: auto; @@ -10,7 +10,7 @@    outline: none !important;  } -.formattedTextBox-cont { +.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {    background: $light-color-secondary;    padding: 0.9em;    border-width: 0px; @@ -22,6 +22,11 @@    overflow-x: hidden;    color: initial;    height: 100%; +  pointer-events: all; +} +.formattedTextBox-cont-hidden { +    overflow: hidden; +    pointer-events: none;  }  .menuicon { diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index beca6cdc6..ae05c2dad 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,20 +1,26 @@ -import { action, IReactionDisposer, reaction } from "mobx"; +import { action, IReactionDisposer, reaction, trace, computed } from "mobx";  import { baseKeymap } from "prosemirror-commands"; -import { history, redo, undo } from "prosemirror-history"; +import { history } from "prosemirror-history";  import { keymap } from "prosemirror-keymap";  import { EditorState, Plugin, Transaction } from "prosemirror-state";  import { EditorView } from "prosemirror-view";  import { FieldWaiting, Opt } from "../../../fields/Field";  import { KeyStore } from "../../../fields/KeyStore";  import { RichTextField } from "../../../fields/RichTextField"; +import { TextField } from "../../../fields/TextField"; +import { Document } from "../../../fields/Document"; +import buildKeymap from "../../util/ProsemirrorKeymap";  import { inpRules } from "../../util/RichTextRules";  import { schema } from "../../util/RichTextSchema"; +import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";  import { TooltipTextMenu } from "../../util/TooltipTextMenu";  import { ContextMenu } from "../../views/ContextMenu"; -import { Main } from "../Main"; +import { MainOverlayTextBox } from "../MainOverlayTextBox";  import { FieldView, FieldViewProps } from "./FieldView";  import "./FormattedTextBox.scss";  import React = require("react"); +import { SelectionManager } from "../../util/SelectionManager"; +import { observer } from "mobx-react";  const { buildMenuItems } = require("prosemirror-example-setup");  const { menuBar } = require("prosemirror-menu"); @@ -34,14 +40,22 @@ const { menuBar } = require("prosemirror-menu");  //  specified Key and assigns it to an HTML input node.  When changes are made to this node,  //  this will edit the document and assign the new value to that field.  //] -export class FormattedTextBox extends React.Component<FieldViewProps> { + +export interface FormattedTextBoxOverlay { +    isOverlay?: boolean; +} + +@observer +export class FormattedTextBox extends React.Component<(FieldViewProps & FormattedTextBoxOverlay)> {      public static LayoutString(fieldStr: string = "DataKey") {          return FieldView.LayoutString(FormattedTextBox, fieldStr);      }      private _ref: React.RefObject<HTMLDivElement>;      private _editorView: Opt<EditorView>; +    private _gotDown: boolean = false;      private _reactionDisposer: Opt<IReactionDisposer>;      private _inputReactionDisposer: Opt<IReactionDisposer>; +    private _proxyReactionDisposer: Opt<IReactionDisposer>;      constructor(props: FieldViewProps) {          super(props); @@ -50,74 +64,77 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {          this.onChange = this.onChange.bind(this);      } +    _applyingChange: boolean = false; +      dispatchTransaction = (tx: Transaction) => {          if (this._editorView) {              const state = this._editorView.state.apply(tx);              this._editorView.updateState(state); -            this.FieldDoc.SetDataOnPrototype( -                this.FieldKey, +            this._applyingChange = true; +            this.props.Document.SetDataOnPrototype( +                this.props.fieldKey,                  JSON.stringify(state.toJSON()),                  RichTextField              ); +            this.props.Document.SetDataOnPrototype(KeyStore.DocumentText, state.doc.textBetween(0, state.doc.content.size, "\n\n"), TextField); +            this._applyingChange = false;              // doc.SetData(fieldKey, JSON.stringify(state.toJSON()), RichTextField);          }      } -    get FieldDoc() { return this.props.fieldKey === KeyStore.Archives ? Main.Instance._textDoc! : this.props.Document; } -    get FieldKey() { return this.props.fieldKey === KeyStore.Archives ? KeyStore.Data : this.props.fieldKey; } -      componentDidMount() {          const config = {              schema,              inpRules, //these currently don't do anything, but could eventually be helpful -            plugins: [ +            plugins: this.props.isOverlay ? [                  history(), -                keymap({ "Mod-z": undo, "Mod-y": redo }), +                keymap(buildKeymap(schema)),                  keymap(baseKeymap), -                this.tooltipMenuPlugin() -            ] +                this.tooltipTextMenuPlugin(), +                // this.tooltipLinkingMenuPlugin(), +                new Plugin({ +                    props: { +                        attributes: { class: "ProseMirror-example-setup-style" } +                    } +                }) +            ] : [ +                    history(), +                    keymap(buildKeymap(schema)), +                    keymap(baseKeymap), +                ]          }; -        if (this.props.fieldKey === KeyStore.Archives) { -            this._inputReactionDisposer = reaction(() => Main.Instance._textDoc && Main.Instance._textDoc.Id, +        if (this.props.isOverlay) { +            this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc.Id,                  () => {                      if (this._editorView) {                          this._editorView.destroy();                      } - -                    this.setupEditor(config); +                    this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox                  }              ); +        } else { +            this._proxyReactionDisposer = reaction(() => this.props.isSelected(), +                () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform()));          } +          this._reactionDisposer = reaction(              () => { -                const field = this.FieldDoc.GetT(this.FieldKey, RichTextField); +                const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined;                  return field && field !== FieldWaiting ? field.Data : undefined;              }, -            field => { -                if (field && this._editorView) { -                    this._editorView.updateState( -                        EditorState.fromJSON(config, JSON.parse(field)) -                    ); -                } -            } +            field => field && this._editorView && !this._applyingChange && +                this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))          ); -        this.setupEditor(config); +        this.setupEditor(config, this.props.Document);      } -    private setupEditor(config: any) { - -        let state: EditorState; -        let field = this.FieldDoc.GetT(this.FieldKey, RichTextField); -        if (field && field !== FieldWaiting && field.Data) { -            state = EditorState.fromJSON(config, JSON.parse(field.Data)); -        } else { -            state = EditorState.create(config); -        } +    private setupEditor(config: any, doc?: Document) { +        let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined;          if (this._ref.current) {              this._editorView = new EditorView(this._ref.current, { -                state, +                state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),                  dispatchTransaction: this.dispatchTransaction              });          } @@ -138,10 +155,9 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {          if (this._inputReactionDisposer) {              this._inputReactionDisposer();          } -    } - -    shouldComponentUpdate() { -        return false; +        if (this._proxyReactionDisposer) { +            this._proxyReactionDisposer(); +        }      }      @action @@ -151,23 +167,30 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {          // doc.SetData(fieldKey, e.target.value, RichTextField);      }      onPointerDown = (e: React.PointerEvent): void => { -        if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { +        if (e.button === 1 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { +            console.log("first");              e.stopPropagation();          } +        if (e.button === 2) { +            this._gotDown = true; +            console.log("second"); +            e.preventDefault(); +        }      }      onPointerUp = (e: React.PointerEvent): void => { +        console.log("pointer up");          if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {              e.stopPropagation();          } -        if (this.props.fieldKey !== KeyStore.Archives) { -            e.preventDefault(); -            Main.Instance.SetTextDoc(this.props.Document, this._ref.current!); -        }      }      onFocused = (e: React.FocusEvent): void => { -        if (this.props.fieldKey !== KeyStore.Archives) { -            Main.Instance.SetTextDoc(this.props.Document, this._ref.current!); +        if (!this.props.isOverlay) { +            MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform()); +        } else { +            if (this._ref.current) { +                this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; +            }          }      } @@ -175,6 +198,10 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {      textCapability = (e: React.MouseEvent): void => { };      specificContextMenu = (e: React.MouseEvent): void => { +        if (!this._gotDown) { +            e.preventDefault(); +            return; +        }          ContextMenu.Instance.addItem({              description: "Text Capability",              event: this.textCapability @@ -195,35 +222,52 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {      }      onPointerWheel = (e: React.WheelEvent): void => { -        e.stopPropagation(); +        if (this.props.isSelected()) { +            e.stopPropagation(); +        }      } -    tooltipMenuPlugin() { +    tooltipTextMenuPlugin() { +        let myprops = this.props;          return new Plugin({              view(_editorView) { -                return new TooltipTextMenu(_editorView); +                return new TooltipTextMenu(_editorView, myprops);              }          });      } + +    tooltipLinkingMenuPlugin() { +        let myprops = this.props; +        return new Plugin({ +            view(_editorView) { +                return new TooltipLinkingMenu(_editorView, myprops); +            } +        }); +    } +      onKeyPress(e: React.KeyboardEvent) { +        if (e.key == "Escape") { +            SelectionManager.DeselectAll(); +        }          e.stopPropagation(); +        if (e.keyCode === 9) e.preventDefault();          // stop propagation doesn't seem to stop propagation of native keyboard events.          // so we set a flag on the native event that marks that the event's been handled.          // (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;      }      render() { +        let style = this.props.isOverlay ? "scroll" : "hidden";          return ( -            <div -                className="formattedTextBox-cont" +            <div className={`formattedTextBox-cont-${style}`}                  onKeyDown={this.onKeyPress}                  onKeyPress={this.onKeyPress} +                onFocus={this.onFocused}                  onPointerUp={this.onPointerUp}                  onPointerDown={this.onPointerDown}                  onContextMenu={this.specificContextMenu}                  // tfs: do we need this event handler                  onWheel={this.onPointerWheel} -                ref={this._ref} -            /> +                ref={this._ref} />          );      }  } diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss new file mode 100644 index 000000000..ce0ee2e09 --- /dev/null +++ b/src/client/views/nodes/IconBox.scss @@ -0,0 +1,12 @@ + +@import "../globalCssVariables"; +.iconBox-container { +    position: absolute; +    left:0; +    top:0; +    svg { +        width: 100% !important; +        height: 100%; +        background: white; +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx new file mode 100644 index 000000000..9c90c0a0e --- /dev/null +++ b/src/client/views/nodes/IconBox.tsx @@ -0,0 +1,45 @@ +import React = require("react"); +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { action, computed } from "mobx"; +import { observer } from "mobx-react"; +import { Document } from '../../../fields/Document'; +import { IconField } from "../../../fields/IconFIeld"; +import { KeyStore } from "../../../fields/KeyStore"; +import { SelectionManager } from "../../util/SelectionManager"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./IconBox.scss"; + + +library.add(faCaretUp); +library.add(faObjectGroup); +library.add(faStickyNote); +library.add(faFilePdf); +library.add(faFilm); + +@observer +export class IconBox extends React.Component<FieldViewProps> { +    public static LayoutString() { return FieldView.LayoutString(IconBox); } + +    @computed get maximized() { return this.props.Document.GetT(KeyStore.MaximizedDoc, Document); } +    @computed get layout(): string { return this.props.Document.GetData(this.props.fieldKey, IconField, "<p>Error loading layout data</p>" as string); } +    @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); } + +    public static DocumentIcon(layout: string) { +        let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf : +            layout.indexOf("ImageBox") !== -1 ? faImage : +                layout.indexOf("Formatted") !== -1 ? faStickyNote : +                    layout.indexOf("Video") !== -1 ? faFilm : +                        layout.indexOf("Collection") !== -1 ? faObjectGroup : +                            faCaretUp; +        return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" /> +    } + +    render() { +        return ( +            <div className="iconBox-container"> +                {this.minimizedIcon} +            </div>); +    } +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 487038841..f4b3837ff 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -8,6 +8,16 @@    max-height: 100%;  } +.imageBox-dot { +    position:absolute; +    bottom: 10; +    left: 0; +    border-radius: 10px; +    width:20px; +    height:20px; +    background:gray; +} +  .imageBox-cont img {      height: 100%;      width:100%; @@ -18,4 +28,4 @@    border: none;    width: 100%;    height: 100%; -} +}
\ No newline at end of file diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 6b0a3a799..ce855384c 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,51 +1,87 @@ -import { action, observable, trace } from 'mobx'; +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 { Document } from '../../../fields/Document';  import { FieldWaiting } from '../../../fields/Field';  import { ImageField } from '../../../fields/ImageField';  import { KeyStore } from '../../../fields/KeyStore'; +import { ListField } from '../../../fields/ListField'; +import { Utils } from '../../../Utils'; +import { DragManager } from '../../util/DragManager'; +import { undoBatch } from '../../util/UndoManager';  import { ContextMenu } from "../../views/ContextMenu";  import { FieldView, FieldViewProps } from './FieldView';  import "./ImageBox.scss";  import React = require("react"); -import { Utils } from '../../../Utils';  @observer  export class ImageBox extends React.Component<FieldViewProps> {      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;      @observable private _photoIndex: number = 0;      @observable private _isOpen: boolean = false; +    private dropDisposer?: DragManager.DragDropDisposer;      constructor(props: 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.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); +        if (this._photoIndex === 0) { +            this.props.Document.SetNumber(KeyStore.NativeHeight, this.props.Document.GetNumber(KeyStore.NativeWidth, 0) * h / w); +            this.props.Document.SetNumber(KeyStore.Height, this.props.Document.Width() * h / w); +        }      } -    componentDidMount() { + +    protected createDropTarget = (ele: HTMLDivElement) => { +        if (this.dropDisposer) { +            this.dropDisposer(); +        } +        if (ele) { +            this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); +        } +    } +    onDrop = (e: React.DragEvent) => { +        e.stopPropagation(); +        e.preventDefault(); +        console.log("IMPLEMENT ME PLEASE");      } -    componentWillUnmount() { + +    @undoBatch +    drop = (e: Event, de: DragManager.DropEvent) => { +        if (de.data instanceof DragManager.DocumentDragData) { +            de.data.droppedDocuments.map(action((drop: Document) => { +                let layout = drop.GetText(KeyStore.BackgroundLayout, ""); +                if (layout.indexOf(ImageBox.name) !== -1) { +                    let imgData = this.props.Document.Get(KeyStore.Data); +                    if (imgData instanceof ImageField && imgData) { +                        this.props.Document.SetOnPrototype(KeyStore.Data, new ListField([imgData])); +                    } +                    let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]); +                    if (imgList) { +                        let field = drop.Get(KeyStore.Data); +                        if (field === FieldWaiting) { } +                        else if (field instanceof ImageField) imgList.push(field); +                        else if (field instanceof ListField) imgList.push(field.Data); +                    } +                    e.stopPropagation(); +                } +            })); +            // de.data.removeDocument()  bcz: need to implement +        }      }      onPointerDown = (e: React.PointerEvent): void => { @@ -70,8 +106,7 @@ export class ImageBox extends React.Component<FieldViewProps> {          e.stopPropagation();      } -    lightbox = (path: string) => { -        const images = [path]; +    lightbox = (images: string[]) => {          if (this._isOpen) {              return (<Lightbox                  mainSrc={images[this._photoIndex]} @@ -102,15 +137,35 @@ export class ImageBox extends React.Component<FieldViewProps> {          }      } +    @action +    onDotDown(index: number) { +        this._photoIndex = index; +        this.props.Document.SetNumber(KeyStore.CurPage, index); +    } + +    dots(paths: string[]) { +        let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1); +        let dist = Math.min(nativeWidth / paths.length, 40); +        let left = (nativeWidth - paths.length * dist) / 2; +        return paths.map((p, i) => +            <div className="imageBox-placer" key={i} > +                <div className="imageBox-dot" style={{ background: (i == this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} /> +            </div> +        ); +    } +      render() {          let field = this.props.Document.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 paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; +        if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"]; +        else if (field instanceof ImageField) paths = [field.Data.href]; +        else if (field instanceof ListField) paths = field.Data.filter(val => val as ImageField).map(p => (p as ImageField).Data.href);          let nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 1);          return ( -            <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 className="imageBox-cont" onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> +                <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> +                {paths.length > 1 ? this.dots(paths) : (null)} +                {this.lightbox(paths)}              </div>);      }  }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss index 63ae75424..6ebd73f2c 100644 --- a/src/client/views/nodes/KeyValueBox.scss +++ b/src/client/views/nodes/KeyValueBox.scss @@ -1,6 +1,7 @@ -@import "../global_variables"; +@import "../globalCssVariables";  .keyValueBox-cont {      overflow-y: scroll; +    width:100%;      height: 100%;      background-color: $light-color;      border: 1px solid $intermediate-color; @@ -8,31 +9,58 @@      box-sizing: border-box;      display: inline-block;      .imageBox-cont img { -        max-height: 45px; -        height: auto; -    } -    td { -        padding: 6px 8px; -        border-right: 1px solid $intermediate-color; -        border-top: 1px solid $intermediate-color; -        &:last-child { -            border-right: none; -        } +        width: auto;      }  } +$header-height: 30px; +.keyValueBox-tbody { +    width:100%; +    height:100%; +    position: absolute; +    overflow-y: scroll; +} +.keyValueBox-key { +    display: inline-block; +    height:100%; +    width:50%; +    text-align: center; +} +.keyValueBox-fields { +    display: inline-block; +    height:100%; +    width:50%; +    text-align: center; +}  .keyValueBox-table { -    position: relative; +    position: absolute; +    width:100%; +    height:100%;      border-collapse: collapse;  } - +.keyValueBox-td-key { +    display:inline-block; +    height:30px; +} +.keyValueBox-td-value { +    display:inline-block; +    height:30px; +} +.keyValueBox-valueRow { +    width:100%; +    height:30px; +    display: inline-block; +}  .keyValueBox-header { +    width:100%; +    position: relative; +    display: inline-block;      background: $intermediate-color;      color: $light-color;      text-transform: uppercase;      letter-spacing: 2px;      font-size: 12px; -    height: 30px; +    height: $header-height;      padding-top: 4px;      th {          font-weight: normal; @@ -43,13 +71,50 @@  }  .keyValueBox-evenRow { +    position: relative; +    display: inline-block; +    width:100%; +    height:$header-height;      background: $light-color;      .formattedTextBox-cont {          background: $light-color;      }  } +.keyValueBox-cont { +    .collectionfreeformview-overlay { +        position: relative; +    } +} +.keyValueBox-dividerDraggerThumb{ +    position: relative; +    width: 4px; +    float: left;  +    height: 30px; +    width: 10px; +    z-index: 20; +    right: 0; +    top: 0; +    border-radius: 10px; +    background: gray; +    pointer-events: all; +} +.keyValueBox-dividerDragger{ +    position: relative;  +    width: 100%; +    float: left;  +    height: 37px; +    z-index: 20; +    right: 0; +    top: 0; +    background: transparent; +    pointer-events: none; +}  .keyValueBox-oddRow { +    position: relative; +    display: inline-block; +    width:100%; +    height:30px;      background: $light-color-secondary;      .formattedTextBox-cont {          background: $light-color-secondary; diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index bcac113f0..ddbec014b 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -1,35 +1,31 @@ +import { action, computed, observable } from "mobx";  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, Field } from '../../../fields/Field'; +import { Field, FieldWaiting } from '../../../fields/Field'; +import { Key } from '../../../fields/Key';  import { KeyStore } from '../../../fields/KeyStore'; +import { CompileScript, ToField } from "../../util/Scripting";  import { FieldView, FieldViewProps } from './FieldView';  import "./KeyValueBox.scss";  import { KeyValuePair } from "./KeyValuePair";  import React = require("react"); -import { CompileScript, ToField } from "../../util/Scripting"; -import { Key } from '../../../fields/Key'; -import { observable, action } from "mobx";  @observer  export class KeyValueBox extends React.Component<FieldViewProps> { +    private _mainCont = React.createRef<HTMLDivElement>();      public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr); }      @observable private _keyInput: string = "";      @observable private _valueInput: string = ""; +    @computed get splitPercentage() { return this.props.Document.GetNumber(KeyStore.SchemaSplitPercentage, 50); }      constructor(props: FieldViewProps) {          super(props);      } - - -    shouldComponentUpdate() { -        return false; -    } -      @action      onEnterKey = (e: React.KeyboardEvent): void => {          if (e.key === 'Enter') { @@ -90,7 +86,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {          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} />); +            rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />);          }          return rows;      } @@ -107,24 +103,51 @@ export class KeyValueBox extends React.Component<FieldViewProps> {      newKeyValue = () =>          ( -            <tr> -                <td><input type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /></td> -                <td><input type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /></td> +            <tr className="keyValueBox-valueRow"> +                <td className="keyValueBox-td-key" style={{ width: `${100 - this.splitPercentage}%` }}> +                    <input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} /> +                </td> +                <td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}> +                    <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} /> +                </td>              </tr>          ) +    @action +    onDividerMove = (e: PointerEvent): void => { +        let nativeWidth = this._mainCont.current!.getBoundingClientRect(); +        this.props.Document.SetNumber(KeyStore.SchemaSplitPercentage, Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100))); +    } +    @action +    onDividerUp = (e: PointerEvent): void => { +        document.removeEventListener("pointermove", this.onDividerMove); +        document.removeEventListener('pointerup', this.onDividerUp); +    } +    onDividerDown = (e: React.PointerEvent) => { +        e.stopPropagation(); +        e.preventDefault(); +        document.addEventListener("pointermove", this.onDividerMove); +        document.addEventListener('pointerup', this.onDividerUp); +    } +      render() { -        return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}> +        let dividerDragger = this.splitPercentage === 0 ? (null) : +            <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> +                <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> +            </div>; + +        return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} ref={this._mainCont}>              <table className="keyValueBox-table"> -                <tbody> +                <tbody className="keyValueBox-tbody">                      <tr className="keyValueBox-header"> -                        <th>Key</th> -                        <th>Fields</th> +                        <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th> +                        <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>                      </tr>                      {this.createTable()}                      {this.newKeyValue()}                  </tbody>              </table> +            {dividerDragger}          </div>);      }  }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 64e871e1c..01701e02c 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,12 +1,28 @@ -@import "../global_variables"; +@import "../globalCssVariables"; -.container{ -    display: flex;                  -    flex-direction: row;            -    flex-wrap: nowrap;               -    justify-content: space-between;  -} -.delete{ -    color: red; +.keyValuePair-td-key { +    display:inline-block; +    .keyValuePair-td-key-container{ +        width:100%; +        height:100%; +        display: flex;                  +        flex-direction: row;            +        flex-wrap: nowrap;               +        justify-content: space-between;  +        .keyValuePair-td-key-delete{ +            position: relative; +            background-color: transparent; +            color:red; +        } +        .keyValuePair-keyField { +            width:100%; +            text-align: center; +            position: relative; +            overflow: auto; +        } +    } +} +.keyValuePair-td-value { +    display:inline-block;  }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index a1050dc6e..d480eb5af 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,18 +1,18 @@ -import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import "./KeyValueBox.scss"; -import "./KeyValuePair.scss"; -import React = require("react"); -import { FieldViewProps, FieldView } from './FieldView'; -import { Opt, Field } from '../../../fields/Field'; +import { action, observable } from 'mobx';  import { observer } from "mobx-react"; -import { observable, action } from 'mobx'; +import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app  import { Document } from '../../../fields/Document'; +import { Field, Opt } from '../../../fields/Field';  import { Key } from '../../../fields/Key'; +import { emptyDocFunction, emptyFunction, returnFalse } from '../../../Utils';  import { Server } from "../../Server"; -import { EditableView } from "../EditableView";  import { CompileScript, ToField } from "../../util/Scripting";  import { Transform } from '../../util/Transform'; -import { returnFalse, emptyFunction } from '../../../Utils'; +import { EditableView } from "../EditableView"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./KeyValueBox.scss"; +import "./KeyValuePair.scss"; +import React = require("react");  // Represents one row in a key value plane @@ -20,86 +20,81 @@ export interface KeyValuePairProps {      rowStyle: string;      fieldId: string;      doc: Document; +    keyWidth: number;  }  @observer  export class KeyValuePair extends React.Component<KeyValuePairProps> { -    @observable -    private key: Opt<Key>; +    @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; -                } -            })); +            action((field: Opt<Field>) => field instanceof Key && (this.key = field)));      }      render() {          if (!this.key) { -            return <tr><td>error</td><td></td></tr>; - +            return <tr><td>error</td><td /></tr>;          }          let props: FieldViewProps = {              Document: this.props.doc, +            ContainingCollectionView: undefined,              fieldKey: this.key,              isSelected: returnFalse,              select: emptyFunction,              isTopMost: false,              selectOnLoad: false,              active: returnFalse, -            onActiveChanged: emptyFunction, +            whenActiveChanged: emptyFunction,              ScreenToLocalTransform: Transform.Identity, -            focus: emptyFunction, +            focus: emptyDocFunction,          }; -        let contents = ( -            <FieldView {...props} /> -        ); +        let contents = <FieldView {...props} />;          return (              <tr className={this.props.rowStyle}> -                {/* <button>X</button> */} -                <td> -                    <div className="container"> -                        <div>{this.key.Name}</div> -                        <button className="delete" onClick={() => { +                <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> +                    <div className="keyValuePair-td-key-container"> +                        <button className="keyValuePair-td-key-delete" onClick={() => {                              let field = props.Document.Get(props.fieldKey); -                            if (field && field instanceof Field) { -                                props.Document.Set(props.fieldKey, undefined); -                            } -                        }}>X</button> +                            field && field instanceof Field && props.Document.Set(props.fieldKey, undefined); +                        }}> +                            X +                        </button> +                        <div className="keyValuePair-keyField">{this.key.Name}</div>                      </div>                  </td> -                <td><EditableView contents={contents} height={36} GetValue={() => { -                    let field = props.Document.Get(props.fieldKey); -                    if (field && field instanceof Field) { -                        return field.ToScriptString(); -                    } -                    return field || ""; -                }} -                    SetValue={(value: string) => { -                        let script = CompileScript(value, { addReturn: true }); -                        if (!script.compiled) { -                            return false; +                <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}> +                    <EditableView contents={contents} height={36} GetValue={() => { +                        let field = props.Document.Get(props.fieldKey); +                        if (field && field instanceof Field) { +                            return field.ToScriptString();                          } -                        let res = script.run(); -                        if (!res.success) return false; -                        const field = res.result; -                        if (field instanceof Field) { -                            props.Document.Set(props.fieldKey, field); -                            return true; -                        } else { -                            let dataField = ToField(field); -                            if (dataField) { -                                props.Document.Set(props.fieldKey, dataField); +                        return field || ""; +                    }} +                        SetValue={(value: string) => { +                            let script = CompileScript(value, { addReturn: true }); +                            if (!script.compiled) { +                                return false; +                            } +                            let res = script.run(); +                            if (!res.success) return false; +                            const field = res.result; +                            if (field instanceof Field) { +                                props.Document.Set(props.fieldKey, field);                                  return true; +                            } else { +                                let dataField = ToField(field); +                                if (dataField) { +                                    props.Document.Set(props.fieldKey, dataField); +                                    return true; +                                }                              } -                        } -                        return false; -                    }}></EditableView></td> +                            return false; +                        }}> +                    </EditableView></td>              </tr>          );      } diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index 5d5f782d2..8bc70b48f 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -1,4 +1,4 @@ -@import "../global_variables"; +@import "../globalCssVariables";  .link-container {      width: 100%;      height: 35px; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index b016a3d48..1c0e316e8 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,24 +1,16 @@ -import { observable, computed, action } from "mobx"; -import React = require("react"); -import { SelectionManager } from "../../util/SelectionManager"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';  import { observer } from "mobx-react"; -import './LinkBox.scss'; -import { KeyStore } from '../../../fields/KeyStore'; -import { props } from "bluebird"; -import { DocumentView } from "./DocumentView";  import { Document } from "../../../fields/Document"; +import { KeyStore } from '../../../fields/KeyStore';  import { ListField } from "../../../fields/ListField"; +import { NumberField } from "../../../fields/NumberField";  import { DocumentManager } from "../../util/DocumentManager"; -import { LinkEditor } from "./LinkEditor"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { library } from '@fortawesome/fontawesome-svg-core'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEye } from '@fortawesome/free-solid-svg-icons'; -import { faEdit } from '@fortawesome/free-solid-svg-icons'; -import { faTimes } from '@fortawesome/free-solid-svg-icons';  import { undoBatch } from "../../util/UndoManager"; -import { FieldWaiting } from "../../../fields/Field"; -import { NumberField } from "../../../fields/NumberField"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import './LinkBox.scss'; +import React = require("react");  library.add(faEye); diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss index fb0c69cff..ea2e7289c 100644 --- a/src/client/views/nodes/LinkEditor.scss +++ b/src/client/views/nodes/LinkEditor.scss @@ -1,4 +1,4 @@ -@import "../global_variables"; +@import "../globalCssVariables";  .edit-container {      width: 100%;      height: auto; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 314af64c9..9d7c2bc56 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -66,7 +66,7 @@ export class VideoBox extends React.Component<FieldViewProps> {              <Measure onResize={this.setScaling}>                  {({ measureRef }) =>                      <div style={{ width: "100%", height: "auto" }} ref={measureRef}> -                        <video className="videobox-cont" onClick={() => { }} ref={this.setVideoRef}> +                        <video className="videobox-cont" ref={this.setVideoRef}>                              <source src={path} type="video/mp4" />                              Not supported.                          </video> diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss index c73bc0c47..2ad1129a4 100644 --- a/src/client/views/nodes/WebBox.scss +++ b/src/client/views/nodes/WebBox.scss @@ -9,6 +9,12 @@      overflow: scroll;  } +#webBox-htmlSpan { +    position: absolute; +    top:0; +    left:0; +} +  .webBox-button {      padding : 0vw;      border: none; diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 90ce72c41..1edb4d826 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -18,21 +18,40 @@ export class WebBox extends React.Component<FieldViewProps> {      @computed get html(): string { return this.props.Document.GetHtml(KeyStore.Data, ""); } +    _ignore = 0; +    onPreWheel = (e: React.WheelEvent) => { +        this._ignore = e.timeStamp; +    } +    onPrePointer = (e: React.PointerEvent) => { +        this._ignore = e.timeStamp; +    } +    onPostPointer = (e: React.PointerEvent) => { +        if (this._ignore !== e.timeStamp) { +            e.stopPropagation(); +        } +    } +    onPostWheel = (e: React.WheelEvent) => { +        if (this._ignore !== e.timeStamp) { +            e.stopPropagation(); +        } +    }      render() {          let field = this.props.Document.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" }} />} +        let content = +            <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> +                {this.html ? <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: this.html }} /> : +                    <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }} />}              </div>;          return ( -            <div className="webBox-cont"  > -                {content} -            </div>); +            <> +                <div className="webBox-cont"  > +                    {content} +                </div> +                {this.props.isSelected() ? (null) : <div onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} style={{ width: "100%", height: "100%", position: "absolute" }} />} +            </>);      }  }
\ No newline at end of file | 
