diff options
author | Stanley <syip0810@gmail.com> | 2019-05-19 14:10:53 -0700 |
---|---|---|
committer | Stanley <syip0810@gmail.com> | 2019-05-19 14:10:53 -0700 |
commit | eec769b586d4a1c6e73c4ce4ae78b4b8f2d4762b (patch) | |
tree | 7218f2df63e028391e40a084d41bce72fd43e00c /src | |
parent | 2cc62cd88688ccdec8275fcaaba939d448f9baf5 (diff) | |
parent | 01a223f2e6685506cc1e5db69e9062d5ff0d3246 (diff) |
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into pdf_impl
Diffstat (limited to 'src')
74 files changed, 2075 insertions, 1391 deletions
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index a288d394a..f1b50d5a0 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,7 +1,7 @@ import * as OpenSocket from 'socket.io-client'; import { MessageStore } from "./../server/Message"; import { Opt } from '../new_fields/Doc'; -import { Utils } from '../Utils'; +import { Utils, emptyFunction } from '../Utils'; import { SerializationHelper } from './util/SerializationHelper'; import { RefField, HandleUpdate, Id } from '../new_fields/RefField'; @@ -10,6 +10,12 @@ export namespace DocServer { const _socket = OpenSocket(`${window.location.protocol}//${window.location.hostname}:4321`); const GUID: string = Utils.GenerateGuid(); + export function makeReadOnly() { + _CreateField = emptyFunction; + _UpdateField = emptyFunction; + _respondToUpdate = emptyFunction; + } + export function prepend(extension: string): string { return window.location.origin + extension; } @@ -21,12 +27,10 @@ export namespace DocServer { export async function GetRefField(id: string): Promise<Opt<RefField>> { let cached = _cache[id]; if (cached === undefined) { - const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(fieldJson => { + const prom = Utils.EmitCallback(_socket, MessageStore.GetRefField, id).then(async fieldJson => { const field = SerializationHelper.Deserialize(fieldJson); - if (_cache[id] !== undefined && !(_cache[id] instanceof Promise)) { - id; - } if (field !== undefined) { + await field.proto; _cache[id] = field; } else { delete _cache[id]; @@ -65,6 +69,7 @@ export namespace DocServer { fieldMap[field.id] = SerializationHelper.Deserialize(field); } } + return fieldMap; }); requestedIds.forEach(id => _cache[id] = prom.then(fields => fields[id])); @@ -78,26 +83,40 @@ export namespace DocServer { } map[id] = field; }); + await Promise.all(requestedIds.map(async id => { + const field = fields[id]; + if (field) { + await (field as any).proto; + } + })); const otherFields = await Promise.all(promises); waitingIds.forEach((id, index) => map[id] = otherFields[index]); return map; } - export function UpdateField(id: string, diff: any) { + let _UpdateField = (id: string, diff: any) => { if (id === updatingId) { return; } Utils.Emit(_socket, MessageStore.UpdateField, { id, diff }); + }; + + export function UpdateField(id: string, diff: any) { + _UpdateField(id, diff); } - export function CreateField(field: RefField) { + let _CreateField = (field: RefField) => { _cache[field[Id]] = field; const initialState = SerializationHelper.Serialize(field); Utils.Emit(_socket, MessageStore.CreateField, initialState); + }; + + export function CreateField(field: RefField) { + _CreateField(field); } let updatingId: string | undefined; - function respondToUpdate(diff: any) { + let _respondToUpdate = (diff: any) => { const id = diff.id; if (id === undefined) { return; @@ -119,6 +138,9 @@ export namespace DocServer { } else { update(field); } + }; + function respondToUpdate(diff: any) { + _respondToUpdate(diff); } function connected() { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 654431905..5752bb096 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -33,7 +33,12 @@ import { DocServer } from "../DocServer"; import { StrokeData, InkField } from "../../new_fields/InkField"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; +<<<<<<< HEAD import { PDFBox2 } from "../views/pdf/PDFBox2"; +======= +import { schema } from "prosemirror-schema-basic"; +import { UndoManager } from "../util/UndoManager"; +>>>>>>> 01a223f2e6685506cc1e5db69e9062d5ff0d3246 export interface DocumentOptions { x?: number; @@ -64,6 +69,38 @@ export interface DocumentOptions { } const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; +export namespace DocUtils { + export function MakeLink(source: Doc, target: Doc) { + let protoSrc = source.proto ? source.proto : source; + let protoTarg = target.proto ? target.proto : target; + UndoManager.RunInBatch(() => { + let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); + //let linkDoc = new Doc; + linkDoc.proto!.title = "-link name-"; + linkDoc.proto!.linkDescription = ""; + linkDoc.proto!.linkTags = "Default"; + + linkDoc.proto!.linkedTo = target; + linkDoc.proto!.linkedFrom = source; + + let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc)); + if (!linkedFrom) { + protoTarg.linkedFromDocs = linkedFrom = new List<Doc>(); + } + linkedFrom.push(linkDoc); + + let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc)); + if (!linkedTo) { + protoSrc.linkedToDocs = linkedTo = new List<Doc>(); + } + linkedTo.push(linkDoc); + return linkDoc; + }, "make link"); + } + + +} + export namespace Docs { let textProto: Doc; let histoProto: Doc; @@ -109,8 +146,8 @@ export namespace Docs { deleg.data = value; return Doc.assign(deleg, options); } - function SetDelegateOptions<U extends Field>(doc: Doc, options: DocumentOptions) { - const deleg = Doc.MakeDelegate(doc); + function SetDelegateOptions(doc: Doc, options: DocumentOptions, id?: string) { + const deleg = Doc.MakeDelegate(doc, id); return Doc.assign(deleg, options); } @@ -167,7 +204,7 @@ export namespace Docs { return audioProto; } - function CreateInstance(proto: Doc, data: Field, options: DocumentOptions) { + function CreateInstance(proto: Doc, data: Field, options: DocumentOptions, delegId?: string) { const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys); if (!("author" in protoProps)) { protoProps.author = CurrentUserUtils.email; @@ -177,7 +214,7 @@ export namespace Docs { } protoProps.isPrototype = true; - return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps); + return SetDelegateOptions(SetInstanceOptions(proto, protoProps, data), delegateProps, delegId); } export function ImageDocument(url: string, options: DocumentOptions = {}) { @@ -208,16 +245,18 @@ export namespace Docs { export function PdfDocument(url: string, options: DocumentOptions = {}) { return CreateInstance(pdfProto, new PdfField(new URL(url)), 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 = Docs.TreeDocument([], { ...options, nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: schema.displayName! }); - let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc)); + let schemaDocuments = Cast(schemaDoc.data, listSpec(Doc), []); if (!schemaDocuments) { return; } + CurrentUserUtils.AddNorthstarSchema(schema, schemaDoc); const docs = schemaDocuments; CurrentUserUtils.GetAllNorthstarColumnAttributes(schema).map(attr => { DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { @@ -252,14 +291,14 @@ export namespace Docs { } return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Freeform }); } - export function SchemaDocument(documents: Array<Doc>, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Schema }); + export function SchemaDocument(schemaColumns: string[], documents: Array<Doc>, options: DocumentOptions) { + return CreateInstance(collProto, new List(documents), { schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) { return CreateInstance(collProto, new List(documents), { schemaColumns: new List(["title"]), ...options, viewType: CollectionViewType.Tree }); } - export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions) { - return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }); + export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { + return CreateInstance(collProto, new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id); } export function CaptionDocument(doc: Doc) { diff --git a/src/client/goldenLayout.d.ts b/src/client/goldenLayout.d.ts new file mode 100644 index 000000000..b50240563 --- /dev/null +++ b/src/client/goldenLayout.d.ts @@ -0,0 +1,3 @@ + +declare const GoldenLayout: any; +export = GoldenLayout;
\ No newline at end of file diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 56a71f1ac..ab2bcefed 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -2902,6 +2902,7 @@ * @returns {void} */ _$destroy: function () { + this._layoutManager.emit('tabDestroyed', this); this.element.off('mousedown touchstart', this._onTabClickFn); this.closeElement.off('click touchstart', this._onCloseClickFn); if (this._dragListener) { diff --git a/src/client/northstar/dash-fields/HistogramField.ts b/src/client/northstar/dash-fields/HistogramField.ts index f01f08487..1ee2189b9 100644 --- a/src/client/northstar/dash-fields/HistogramField.ts +++ b/src/client/northstar/dash-fields/HistogramField.ts @@ -9,7 +9,8 @@ import { OmitKeys } from "../../../Utils"; import { Deserializable } from "../../util/SerializationHelper"; function serialize(field: HistogramField) { - return OmitKeys(field.HistoOp, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit; + let obj = OmitKeys(field, ['Links', 'BrushLinks', 'Result', 'BrushColors', 'FilterModels', 'FilterOperand']).omit; + return obj; } function deserialize(jp: any) { @@ -31,10 +32,10 @@ function deserialize(jp: any) { } }); if (X && Y && V) { - return new HistogramField(new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization)); + return new HistogramOperation(jp.SchemaName, X, Y, V, jp.Normalization); } } - return new HistogramField(HistogramOperation.Empty); + return HistogramOperation.Empty; } @Deserializable("histogramField") @@ -50,6 +51,8 @@ export class HistogramField extends ObjectField { } [Copy]() { - return new HistogramField(this.HistoOp.Copy()); + let y = this.HistoOp; + let z = this.HistoOp.Copy; + return new HistogramField(HistogramOperation.Duplicate(this.HistoOp)); } }
\ No newline at end of file diff --git a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts index 6291ec1fc..3e9145a1b 100644 --- a/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts +++ b/src/client/northstar/dash-nodes/HistogramBinPrimitiveCollection.ts @@ -198,8 +198,8 @@ export class HistogramBinPrimitiveCollection { var marginParams = new MarginAggregateParameters(); marginParams.aggregateFunction = axis.AggregateFunction; var marginAggregateKey = ModelHelpers.CreateAggregateKey(this.histoOp.Schema!.distinctAttributeParameters, axis, this.histoResult, brush.brushIndex!, marginParams); - var marginResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey) as MarginAggregateResult; - return !marginResult ? 0 : marginResult.absolutMargin!; + let aggResult = ModelHelpers.GetAggregateResult(bin, marginAggregateKey); + return aggResult instanceof MarginAggregateResult && aggResult.absolutMargin ? aggResult.absolutMargin : 0; } private createBinPrimitive(barAxis: number, brush: Brush, marginRect: PIXIRectangle, diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx index ed556cf45..eb1ad69b7 100644 --- a/src/client/northstar/dash-nodes/HistogramBox.tsx +++ b/src/client/northstar/dash-nodes/HistogramBox.tsx @@ -17,9 +17,8 @@ import "./HistogramBox.scss"; import { HistogramBoxPrimitives } from './HistogramBoxPrimitives'; import { HistogramLabelPrimitives } from "./HistogramLabelPrimitives"; import { StyleConstants } from "../utils/StyleContants"; -import { NumCast, Cast } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; -import { Doc, DocListCast } from "../../../new_fields/Doc"; +import { Cast } from "../../../new_fields/Types"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/RefField"; @@ -119,15 +118,16 @@ export class HistogramBox extends React.Component<FieldViewProps> { if (this.HistoOp !== HistogramOperation.Empty) { reaction(() => DocListCast(this.props.Document.linkedFromDocs), (docs) => this.HistoOp.Links.splice(0, this.HistoOp.Links.length, ...docs), { fireImmediately: true }); reaction(() => DocListCast(this.props.Document.brushingDocs).length, - () => { - let brushingDocs = DocListCast(this.props.Document.brushingDocs); + async () => { + let brushingDocs = await DocListCastAsync(this.props.Document.brushingDocs); const proto = this.props.Document.proto; - if (proto) { - this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...brushingDocs.map((brush, i) => { + if (proto && brushingDocs) { + let mapped = brushingDocs.map((brush, i) => { brush.backgroundColor = StyleConstants.BRUSH_COLORS[i % StyleConstants.BRUSH_COLORS.length]; let brushed = DocListCast(brush.brushingDocs); return { l: brush, b: brushed[0][Id] === proto[Id] ? brushed[1] : brushed[0] }; - })); + }); + this.HistoOp.BrushLinks.splice(0, this.HistoOp.BrushLinks.length, ...mapped); } }, { fireImmediately: true }); reaction(() => this.createOperationParamsCache, () => this.HistoOp.Update(), { fireImmediately: true }); diff --git a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx index 721bf6a89..350987695 100644 --- a/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx +++ b/src/client/northstar/dash-nodes/HistogramBoxPrimitives.tsx @@ -11,9 +11,6 @@ import { StyleConstants } from "../../northstar/utils/StyleContants"; import { HistogramBinPrimitiveCollection, HistogramBinPrimitive } from "./HistogramBinPrimitiveCollection"; import { HistogramBox } from "./HistogramBox"; import "./HistogramBoxPrimitives.scss"; -import { JSXElement } from "babel-types"; -import { Utils } from "../utils/Utils"; -import { all } from "bluebird"; export interface HistogramPrimitivesProps { HistoBox: HistogramBox; diff --git a/src/client/northstar/manager/Gateway.ts b/src/client/northstar/manager/Gateway.ts index d26f2724f..c541cce6a 100644 --- a/src/client/northstar/manager/Gateway.ts +++ b/src/client/northstar/manager/Gateway.ts @@ -23,6 +23,18 @@ export class Gateway { } } + public async PostSchema(csvdata: string, schemaname: string): Promise<string> { + try { + const json = await this.MakePostJsonRequest("postSchema", { csv: csvdata, schema: schemaname }); + // const cat = Catalog.fromJS(json); + // return cat; + return json; + } + catch (error) { + throw new Error("can not reach northstar's backend"); + } + } + public async GetSchema(pathname: string, schemaname: string): Promise<Catalog> { try { const json = await this.MakeGetRequest("schema", undefined, { path: pathname, schema: schemaname }); diff --git a/src/client/northstar/model/ModelHelpers.ts b/src/client/northstar/model/ModelHelpers.ts index ac807b41f..88e6e72b8 100644 --- a/src/client/northstar/model/ModelHelpers.ts +++ b/src/client/northstar/model/ModelHelpers.ts @@ -31,7 +31,12 @@ export class ModelHelpers { } public static GetAggregateParametersIndex(histogramResult: HistogramResult, aggParameters?: AggregateParameters): number { - return ArrayUtil.IndexOfWithEqual(histogramResult.aggregateParameters!, aggParameters); + return Array.from(histogramResult.aggregateParameters!).findIndex((value, i, set) => { + if (set[i] instanceof CountAggregateParameters && value instanceof CountAggregateParameters) return true; + if (set[i] instanceof MarginAggregateParameters && value instanceof MarginAggregateParameters) return true; + if (set[i] instanceof SumAggregateParameters && value instanceof SumAggregateParameters) return true; + return false; + }); } public static GetAggregateParameter(distinctAttributeParameters: AttributeParameters | undefined, atm: AttributeTransformationModel): AggregateParameters | undefined { diff --git a/src/client/northstar/model/idea/idea.ts b/src/client/northstar/model/idea/idea.ts index 9d9d60678..c73a822c7 100644 --- a/src/client/northstar/model/idea/idea.ts +++ b/src/client/northstar/model/idea/idea.ts @@ -22,6 +22,9 @@ export abstract class AggregateParameters implements IAggregateParameters { protected _discriminator: string; + public Equals(other: Object): boolean { + return this == other; + } constructor(data?: IAggregateParameters) { if (data) { for (var property in data) { @@ -204,6 +207,9 @@ export interface IAverageAggregateParameters extends ISingleDimensionAggregatePa export abstract class AttributeParameters implements IAttributeParameters { visualizationHints?: VisualizationHint[] | undefined; rawName?: string | undefined; + public Equals(other: Object): boolean { + return this == other; + } protected _discriminator: string; diff --git a/src/client/northstar/operations/BaseOperation.ts b/src/client/northstar/operations/BaseOperation.ts index c6d5f0a15..0d1361ebf 100644 --- a/src/client/northstar/operations/BaseOperation.ts +++ b/src/client/northstar/operations/BaseOperation.ts @@ -25,23 +25,6 @@ export abstract class BaseOperation { @computed public get FilterString(): string { - - // let filterModels: FilterModel[] = []; - // return FilterModel.GetFilterModelsRecursive(this, new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true) - // if (this.OverridingFilters.length > 0) { - // return "(" + this.OverridingFilters.filter(fm => fm !== null).map(fm => fm.ToPythonString()).join(" || ") + ")"; - // } - // let rdg = MainManager.Instance.MainViewModel.FilterReverseDependencyGraph; - // let sliceModel = this.TypedViewModel.IncomingSliceModel; - // if (sliceModel !== null && sliceModel.Source !== null && instanceOfIBaseFilterProvider(sliceModel.Source) && rdg.has(sliceModel.Source)) { - // let filterModels = sliceModel.Source.FilterModels.map(f => f); - // return FilterModel.GetFilterModelsRecursive(rdg.get(sliceModel.Source), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, false); - // } - - // if (rdg.has(this.TypedViewModel)) { - // let filterModels = []; - // return FilterModel.GetFilterModelsRecursive(rdg.get(this.TypedViewModel), new Set<GraphNode<BaseOperationViewModel, FilterLinkViewModel>>(), filterModels, true) - // } return ""; } diff --git a/src/client/northstar/operations/HistogramOperation.ts b/src/client/northstar/operations/HistogramOperation.ts index 78b206bdc..74e23ea48 100644 --- a/src/client/northstar/operations/HistogramOperation.ts +++ b/src/client/northstar/operations/HistogramOperation.ts @@ -30,7 +30,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons @observable public V: AttributeTransformationModel; @observable public SchemaName: string; @observable public QRange: QuantitativeBinRange | undefined; - @computed public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); } + public get Schema() { return CurrentUserUtils.GetNorthstarSchema(this.SchemaName); } constructor(schemaName: string, x: AttributeTransformationModel, y: AttributeTransformationModel, v: AttributeTransformationModel, normalized?: number) { super(); @@ -41,7 +41,11 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons this.SchemaName = schemaName; } - Copy(): HistogramOperation { + public static Duplicate(op: HistogramOperation) { + + return new HistogramOperation(op.SchemaName, op.X, op.Y, op.V, op.Normalization); + } + public Copy(): HistogramOperation { return new HistogramOperation(this.SchemaName, this.X, this.Y, this.V, this.Normalization); } @@ -50,7 +54,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons } - @computed public get FilterModels() { + public get FilterModels() { return this.BarFilterModels; } @action @@ -71,9 +75,7 @@ export class HistogramOperation extends BaseOperation implements IBaseFilterCons return FilterModel.GetFilterModelsRecursive(this, new Set<IBaseFilterProvider>(), filterModels, true); } - @computed public get BrushString(): string[] { - trace(); let brushes: string[] = []; this.BrushLinks.map(brushLink => { let brushHistogram = Cast(brushLink.b.data, HistogramField); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 9a7a94228..d06af6dd5 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,10 +1,14 @@ import { computed, observable } from 'mobx'; import { DocumentView } from '../views/nodes/DocumentView'; -import { Doc, DocListCast } from '../../new_fields/Doc'; +import { Doc, DocListCast, Opt } from '../../new_fields/Doc'; import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types'; import { listSpec } from '../../new_fields/Schema'; import { undoBatch } from './UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; +import { Id } from '../../new_fields/RefField'; +import { CollectionView } from '../views/collections/CollectionView'; +import { CollectionPDFView } from '../views/collections/CollectionPDFView'; +import { CollectionVideoView } from '../views/collections/CollectionVideoView'; export class DocumentManager { @@ -26,28 +30,35 @@ export class DocumentManager { // this.DocumentViews = new Array<DocumentView>(); } - public getDocumentView(toFind: Doc): DocumentView | null { + public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { let toReturn: DocumentView | null = null; + let passes = preferredCollection ? [preferredCollection, undefined] : [undefined]; - //gets document view that is in a freeform canvas collection - DocumentManager.Instance.DocumentViews.map(view => { - if (view.props.Document === toFind) { - toReturn = view; - return; - } - }); - if (!toReturn) { + for (let i = 0; i < passes.length; i++) { DocumentManager.Instance.DocumentViews.map(view => { - let doc = view.props.Document.proto; - if (doc && Object.is(doc, toFind)) { + if (view.props.Document[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { toReturn = view; + return; } }); + if (!toReturn) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document.proto; + if (doc && doc[Id] === id && (!passes[i] || view.props.ContainingCollectionView === preferredCollection)) { + toReturn = view; + } + }); + } } return toReturn; } + + public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null { + return this.getDocumentViewById(toFind[Id], preferredCollection); + } + public getDocumentViews(toFind: Doc): DocumentView[] { let toReturn: DocumentView[] = []; @@ -104,20 +115,21 @@ export class DocumentManager { } @undoBatch - public jumpToDocument = async (doc: Doc): Promise<void> => { + public jumpToDocument = async (docDelegate: Doc, makeCopy: boolean = true): Promise<void> => { + let doc = docDelegate.proto ? docDelegate.proto : docDelegate; + const page = NumCast(doc.page, undefined); + const contextDoc = await Cast(doc.annotationOn, Doc); + if (contextDoc) { + const curPage = NumCast(contextDoc.curPage, page); + if (page !== curPage) contextDoc.curPage = page; + } let docView = DocumentManager.Instance.getDocumentView(doc); if (docView) { docView.props.focus(docView.props.Document); } else { - const contextDoc = await Cast(doc.annotationOn, Doc); if (!contextDoc) { - CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(doc)); + CollectionDockingView.Instance.AddRightSplit(docDelegate ? (makeCopy ? Doc.MakeCopy(docDelegate) : docDelegate) : Doc.MakeDelegate(doc)); } else { - const page = NumCast(doc.page, undefined); - const curPage = NumCast(contextDoc.curPage, undefined); - if (page !== curPage) { - contextDoc.curPage = page; - } let contextView = DocumentManager.Instance.getDocumentView(contextDoc); if (contextView) { contextDoc.panTransformType = "Ease"; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 2da0d5b51..7f75a95f0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -41,7 +41,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) { let srcTarg = sourceDoc.proto; let draggedDocs: Doc[] = []; - let draggedFromDocs: Doc[] = [] + let draggedFromDocs: Doc[] = []; if (srcTarg) { let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs); let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs); @@ -105,7 +105,8 @@ export namespace DragManager { constructor( readonly x: number, readonly y: number, - readonly data: { [id: string]: any } + readonly data: { [id: string]: any }, + readonly mods: string ) { } } @@ -334,7 +335,8 @@ export namespace DragManager { detail: { x: e.x, y: e.y, - data: dragData + data: dragData, + mods: e.altKey ? "AltKey" : "" } }) ); diff --git a/src/client/util/History.ts b/src/client/util/History.ts new file mode 100644 index 000000000..545ea8629 --- /dev/null +++ b/src/client/util/History.ts @@ -0,0 +1,122 @@ +import { Doc, Opt, Field } from "../../new_fields/Doc"; +import { DocServer } from "../DocServer"; +import { RouteStore } from "../../server/RouteStore"; +import { MainView } from "../views/MainView"; + +export namespace HistoryUtil { + export interface DocInitializerList { + [key: string]: string | number; + } + + export interface DocUrl { + type: "doc"; + docId: string; + initializers: { + [docId: string]: DocInitializerList; + }; + } + + export type ParsedUrl = DocUrl; + + // const handlers: ((state: ParsedUrl | null) => void)[] = []; + function onHistory(e: PopStateEvent) { + if (window.location.pathname !== RouteStore.home) { + const url = e.state as ParsedUrl || parseUrl(window.location.pathname); + if (url) { + switch (url.type) { + case "doc": + onDocUrl(url); + break; + } + } + } + // for (const handler of handlers) { + // handler(e.state); + // } + } + + export function pushState(state: ParsedUrl) { + history.pushState(state, "", createUrl(state)); + } + + export function replaceState(state: ParsedUrl) { + history.replaceState(state, "", createUrl(state)); + } + + function copyState(state: ParsedUrl): ParsedUrl { + return JSON.parse(JSON.stringify(state)); + } + + export function getState(): ParsedUrl { + return copyState(history.state); + } + + // export function addHandler(handler: (state: ParsedUrl | null) => void) { + // handlers.push(handler); + // } + + // export function removeHandler(handler: (state: ParsedUrl | null) => void) { + // const index = handlers.indexOf(handler); + // if (index !== -1) { + // handlers.splice(index, 1); + // } + // } + + export function parseUrl(pathname: string): ParsedUrl | undefined { + let pathnameSplit = pathname.split("/"); + if (pathnameSplit.length !== 2) { + return undefined; + } + const type = pathnameSplit[0]; + const data = pathnameSplit[1]; + + if (type === "doc") { + const s = data.split("?"); + if (s.length < 1 || s.length > 2) { + return undefined; + } + const docId = s[0]; + const initializers = s.length === 2 ? JSON.parse(decodeURIComponent(s[1])) : {}; + return { + type: "doc", + docId, + initializers + }; + } + + return undefined; + } + + export function createUrl(params: ParsedUrl): string { + let baseUrl = DocServer.prepend(`/${params.type}`); + switch (params.type) { + case "doc": + const initializers = encodeURIComponent(JSON.stringify(params.initializers)); + const id = params.docId; + let url = baseUrl + `/${id}`; + if (Object.keys(params.initializers).length) { + url += `?${initializers}`; + } + return url; + } + return ""; + } + + export async function initDoc(id: string, initializer: DocInitializerList) { + const doc = await DocServer.GetRefField(id); + if (!(doc instanceof Doc)) { + return; + } + Doc.assign(doc, initializer); + } + + async function onDocUrl(url: DocUrl) { + const field = await DocServer.GetRefField(url.docId); + await Promise.all(Object.keys(url.initializers).map(id => initDoc(id, url.initializers[id]))); + if (field instanceof Doc) { + MainView.Instance.openWorkspace(field, true); + } + } + + window.onpopstate = onHistory; +} diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index c0e6f7899..3e3e98206 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -97,13 +97,13 @@ export const nodes: { [index: string]: NodeSpec } = { title: dom.getAttribute("title"), alt: dom.getAttribute("alt"), width: Math.min(100, Number(dom.getAttribute("width"))), - } + }; } }], // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why? toDOM(node) { - const attrs = { style: `width: ${node.attrs.width}` } - return ["img", { ...node.attrs, ...attrs }] + const attrs = { style: `width: ${node.attrs.width}` }; + return ["img", { ...node.attrs, ...attrs }]; } }, @@ -375,7 +375,7 @@ export class ImageResizeView { const currentX = e.pageX; const diffInPx = currentX - startX; self._outer.style.width = `${startWidth + diffInPx}`; - } + }; const onpointerup = () => { document.removeEventListener("pointermove", onpointermove); @@ -384,11 +384,11 @@ export class ImageResizeView { view.state.tr.setNodeMarkup(getPos(), null, { src: node.attrs.src, width: self._outer.style.width }) .setSelection(view.state.selection)); - } + }; - document.addEventListener("pointermove", onpointermove) - document.addEventListener("pointerup", onpointerup) - } + document.addEventListener("pointermove", onpointermove); + document.addEventListener("pointerup", onpointerup); + }; this._outer.appendChild(this._handle); this._outer.appendChild(this._img); diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts new file mode 100644 index 000000000..4ccff0d1b --- /dev/null +++ b/src/client/util/SearchUtil.ts @@ -0,0 +1,25 @@ +import * as rp from 'request-promise'; +import { DocServer } from '../DocServer'; +import { Doc } from '../../new_fields/Doc'; +import { Id } from '../../new_fields/RefField'; + +export namespace SearchUtil { + export function Search(query: string, returnDocs: true): Promise<Doc[]>; + export function Search(query: string, returnDocs: false): Promise<string[]>; + export async function Search(query: string, returnDocs: boolean) { + const ids = JSON.parse(await rp.get(DocServer.prepend("/search"), { + qs: { query } + })); + if (!returnDocs) { + return ids; + } + const docMap = await DocServer.GetRefFields(ids); + return ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc); + } + + export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]> { + const proto = await Doc.GetT(doc, "proto", Doc, true); + const protoId = (proto || doc)[Id]; + return Search(`{!join from=id to=proto_i}id:${protoId}`, true); + } +}
\ No newline at end of file diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index 6eb654319..4d40d09b2 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -175,7 +175,7 @@ export class TooltipTextMenu { this.linkText.style.width = "150px"; this.linkText.style.overflow = "hidden"; this.linkText.style.color = "white"; - this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); } + this.linkText.onpointerdown = (e: PointerEvent) => { e.stopPropagation(); }; let linkBtn = document.createElement("div"); linkBtn.textContent = ">>"; linkBtn.style.width = "20px"; @@ -186,14 +186,14 @@ export class TooltipTextMenu { let node = this.view.state.selection.$from.nodeAfter; let link = node && node.marks.find(m => m.type.name === "link"); if (link) { - console.log("Link to : " + link.attrs.href); let href: string = link.attrs.href; if (href.indexOf(DocServer.prepend("/doc/")) === 0) { let docid = href.replace(DocServer.prepend("/doc/"), ""); DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); + } else CollectionDockingView.Instance.AddRightSplit(f); } })); @@ -201,7 +201,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.linkDrag = document.createElement("img"); this.linkDrag.src = "https://seogurusnyc.com/wp-content/uploads/2016/12/link-1.png"; this.linkDrag.style.width = "20px"; @@ -216,12 +216,12 @@ export class TooltipTextMenu { { handlers: { dragComplete: action(() => { - let m = dragData.droppedDocuments as Doc[]; + let m = dragData.droppedDocuments; this.makeLink(DocServer.prepend("/doc/" + m[0][Id])); }), }, hideSource: false - }) + }); }; this.linkEditor.appendChild(this.linkDrag); this.linkEditor.appendChild(this.linkText); @@ -239,7 +239,7 @@ export class TooltipTextMenu { e.stopPropagation(); e.preventDefault(); } - } + }; this.tooltip.appendChild(this.linkEditor); } diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss index fe884ca85..0a14c8ce7 100644 --- a/src/client/views/ContextMenu.scss +++ b/src/client/views/ContextMenu.scss @@ -1,55 +1,70 @@ @import "globalCssVariables"; + .contextMenu-cont { - position: absolute; - display: flex; - z-index: $contextMenu-zindex; - box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; - flex-direction: column; + position: absolute; + display: flex; + z-index: $contextMenu-zindex; + box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw; + flex-direction: column; } .contextMenu-item:first-child { - background: $intermediate-color; - color: $light-color; + background: $intermediate-color; + color: $light-color; } .contextMenu-item:first-child::placeholder { - color: $light-color; + color: $light-color; } .contextMenu-item:first-child:hover { - background: $intermediate-color; - color: $light-color; + background: $intermediate-color; + color: $light-color; +} + +.subMenu-cont { + position: absolute; + display: flex; + z-index: 1000; + box-shadow: #AAAAAA .2vw .2vw .4vw; + flex-direction: column; } .contextMenu-item { - width: auto; - height: auto; - background: $light-color-secondary; - display: flex; - justify-content: left; - align-items: center; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - transition: all 0.1s; - border-width: 0.11px; - border-style: none; - border-color: $intermediate-color; - border-bottom-style: solid; - padding: 10px; - white-space: nowrap; - font-size: 13px; + width: 11vw; //10vw + height: 2.5vh; //2vh + background: #DDDDDD; + display: flex; //comment out to allow search icon to be inline with search text + justify-content: left; + align-items: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .1s; + border-width: .11px; + border-style: none; + border-color: $intermediate-color; // rgb(187, 186, 186); + border-bottom-style: solid; + padding: 10px; + white-space: nowrap; + font-size: 1vw; } .contextMenu-item:hover { - transition: all 0.1s; - background: $lighter-alt-accent; + transition: all 0.1s; + background: $lighter-alt-accent; } .contextMenu-description { - text-align: left; - width: 8vw; + font-size: 1vw; + text-align: left; + width: 8vw; + display: inline; //need this? } + +.icon-background { + background-color: #DDDDDD; +}
\ No newline at end of file diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 615a928ad..9143c012e 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -1,14 +1,20 @@ import React = require("react"); import { ContextMenuItem, ContextMenuProps } from "./ContextMenuItem"; import { observable, action } from "mobx"; -import { observer } from "mobx-react"; -import "./ContextMenu.scss"; +import { observer } from "mobx-react" +import "./ContextMenu.scss" +import { library } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSearch, faCircle } from '@fortawesome/free-solid-svg-icons'; + +library.add(faSearch); +library.add(faCircle); @observer export class ContextMenu extends React.Component { static Instance: ContextMenu; - @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault() }]; + @observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }]; @observable private _pageX: number = 0; @observable private _pageY: number = 0; @observable private _display: string = "none"; @@ -82,7 +88,13 @@ export class ContextMenu extends React.Component { return ( <div className="contextMenu-cont" style={style} ref={this.ref}> - <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange}></input> + <span> + <span className="icon-background"> + <FontAwesomeIcon icon="circle" size="lg" /> + <FontAwesomeIcon icon="search" size="lg" /> + </span> + <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} /> + </span> {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1). map(prop => <ContextMenuItem {...prop} key={prop.description} />)} </div> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 70813f0dd..ed583942a 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -1,8 +1,13 @@ import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -export interface ContextMenuProps { +export interface OriginalMenuProps { description: string; event: (e: React.MouseEvent<HTMLDivElement>) => void; + icon?: IconProp; //maybe should be optional (icon?) } export interface SubmenuProps { @@ -13,13 +18,48 @@ export interface SubmenuProps { export interface ContextMenuItemProps { type: ContextMenuProps | SubmenuProps; } +export type ContextMenuProps = OriginalMenuProps | SubmenuProps; +@observer export class ContextMenuItem extends React.Component<ContextMenuProps> { + @observable private _items: Array<ContextMenuProps> = []; + @observable private overItem = false; + + constructor(props: ContextMenuProps | SubmenuProps) { + super(props); + if ("subitems" in this.props) { + this.props.subitems.forEach(i => this._items.push(i)); + } + } + render() { - return ( - <div className="contextMenu-item" onClick={this.props.event}> - <div className="contextMenu-description">{this.props.description}</div> - </div> - ); + if ("event" in this.props) { + return ( + <div className="contextMenu-item" onClick={this.props.event}> + <span className="icon-background"> + <FontAwesomeIcon icon="circle" size="sm" /> + {this.props.icon ? <FontAwesomeIcon icon={this.props.icon} size="sm" /> : null} + </span> + <div className="contextMenu-description"> {this.props.description}</div> + </div> + ); + } + else { + let submenu = null; + if (this.overItem) { + submenu = ( + <div className="subMenu-cont" style={{ marginLeft: "100.5%", left: "0px" }}> + {this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} />)} + </div> + ); + } + return ( + <div className="contextMenu-item" onMouseEnter={action(() => { this.overItem = true; })} + onMouseLeave={action(() => this.overItem = false)}> + <div className="contextMenu-description"> {this.props.description}</div> + {submenu} + </div> + ); + } } }
\ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 4786b4de6..5ec090f05 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -96,7 +96,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._downX = e.clientX; this._downY = e.clientY; e.stopPropagation(); - this.onBackgroundDown(e); document.removeEventListener("pointermove", this.onTitleMove); document.removeEventListener("pointerup", this.onTitleUp); document.addEventListener("pointermove", this.onTitleMove); @@ -239,6 +238,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> this._removeIcon = snapped; } } + @undoBatch @action onMinimizeUp = (e: PointerEvent): void => { e.stopPropagation(); @@ -250,9 +250,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc); } if (!this._removeIcon) { - if (selectedDocs.length === 1) + if (selectedDocs.length === 1) { this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized()); - else { + } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) { let docViews = SelectionManager.ViewsSortedVertically(); let topDocView = docViews[0]; let ind = topDocView.templates.indexOf(Templates.Bullet.Layout); @@ -261,7 +262,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> topDocView.props.Document.subBulletDocs = undefined; } else { topDocView.addTemplate(Templates.Bullet); - topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document)); + topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); } } } @@ -270,19 +271,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> runInAction(() => this._minimizedX = this._minimizedY = 0); } + @undoBatch @action createIcon = (selected: DocumentView[], layoutString: string): Doc => { let doc = selected[0].props.Document; let iconDoc = Docs.IconDocument(layoutString); iconDoc.isButton = true; - iconDoc.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title); - iconDoc.labelField = this._fieldKey; - iconDoc[this._fieldKey] = selected.length > 1 ? "collection" : undefined; - iconDoc.isMinimized = false; + iconDoc.proto!.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title); + iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey; + iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined; + iconDoc.proto!.isMinimized = false; iconDoc.width = Number(MINIMIZED_ICON_SIZE); iconDoc.height = Number(MINIMIZED_ICON_SIZE); iconDoc.x = NumCast(doc.x); iconDoc.y = NumCast(doc.y) - 24; - iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document)); + iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!)); doc.minimizedDoc = iconDoc; selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false); return iconDoc; @@ -429,31 +431,29 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> SelectionManager.SelectedDocuments().forEach(element => { const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect(); - if (rect.width !== 0) { + if (rect.width !== 0 && (dX != 0 || dY != 0 || dW != 0 || dH != 0)) { let doc = PositionDocument(element.props.Document); - let width = FieldValue(doc.width, 0); - let nwidth = FieldValue(doc.nativeWidth, 0); - let nheight = FieldValue(doc.nativeHeight, 0); - let height = FieldValue(doc.height, nwidth ? nheight / nwidth * width : 0); - let x = FieldValue(doc.x, 0); - let y = FieldValue(doc.y, 0); + let nwidth = doc.nativeWidth || 0; + let nheight = doc.nativeHeight || 0; + let zoomBasis = NumCast(doc.zoomBasis, 1); + let width = (doc.width || 0) / zoomBasis; + let height = (doc.height || (nheight / nwidth * width)) / zoomBasis; let scale = width / rect.width; let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); - x += dX * (actualdW - width); - y += dY * (actualdH - height); - doc.x = x; - doc.y = y; - var nativeWidth = FieldValue(doc.nativeWidth, 0); - var nativeHeight = FieldValue(doc.nativeHeight, 0); - if (nativeWidth > 0 && nativeHeight > 0) { + doc.x = (doc.x || 0) + dX * (actualdW - width); + doc.y = (doc.y || 0) + dY * (actualdH - height); + if (nwidth > 0 && nheight > 0) { if (Math.abs(dW) > Math.abs(dH)) { - actualdH = nativeHeight / nativeWidth * actualdW; + doc.zoomBasis = zoomBasis * width / actualdW; + } + else { + doc.zoomBasis = zoomBasis * height / actualdH; } - else actualdW = nativeWidth / nativeHeight * actualdH; + } else { + doc.width = zoomBasis * actualdW; + doc.height = zoomBasis * actualdH; } - doc.width = actualdW; - doc.height = actualdH; } }); } @@ -534,9 +534,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (temp !== Templates.Bullet.Layout || i === 0) { res.push(temp); } - }) + }); } - return res + return res; }, [] as string[]); let checked = false; docTemps.forEach(temp => { diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx index 4b3dbd4e0..17d4a1e49 100644 --- a/src/client/views/InkingControl.tsx +++ b/src/client/views/InkingControl.tsx @@ -35,12 +35,9 @@ export class InkingControl extends React.Component { @action switchColor = (color: ColorResult): void => { this._selectedColor = color.hex; - if (SelectionManager.SelectedDocuments().length === 1) { - var sdoc = SelectionManager.SelectedDocuments()[0]; - if (sdoc.props.ContainingCollectionView) { - Doc.SetOnPrototype(sdoc.props.Document, "backgroundColor", color.hex); - } - } + SelectionManager.SelectedDocuments().forEach(doc => + doc.props.ContainingCollectionView && Doc.SetOnPrototype(doc.props.Document, "backgroundColor", color.hex) + ); } @action diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 2430e8f6c..d63b0147b 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -27,25 +27,6 @@ div { z-index: 9999; } -h1 { - font-size: 50px; - position: fixed; - top: 30px; - left: 50%; - transform: translateX(-50%); - color: $dark-color; - text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; - z-index: 9999; - font-family: $sans-serif; - font-weight: 700; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} - .jsx-parser { width: 100%; pointer-events: none; diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 158de31f5..3d9750a85 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,330 +1,11 @@ -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 } from 'mobx'; -import { observer } from 'mobx-react'; -import "normalize.css"; -import * as React from 'react'; +import { MainView } from "./MainView"; +import { Docs } from "../documents/Documents"; +import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; import * as ReactDOM from 'react-dom'; -import Measure from 'react-measure'; -import * as request from 'request'; -import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; -import { RouteStore } from '../../server/RouteStore'; -import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; -import { Docs } from '../documents/Documents'; -import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel'; -import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; -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 { SetupDrag, DragManager } from '../util/DragManager'; -import { Transform } from '../util/Transform'; -import { UndoManager } from '../util/UndoManager'; -import { PresentationView } from './PresentationView'; -import { CollectionDockingView } from './collections/CollectionDockingView'; -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 { PreviewCursor } from './PreviewCursor'; -import { SearchBox } from './SearchBox'; -import { SelectionManager } from '../util/SelectionManager'; -import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc'; -import { Cast, FieldValue, StrCast } from '../../new_fields/Types'; -import { DocServer } from '../DocServer'; -import { listSpec } from '../../new_fields/Schema'; -import { Id } from '../../new_fields/RefField'; - -@observer -export class Main extends React.Component { - public static Instance: Main; - @observable private _workspacesShown: boolean = false; - @observable public pwidth: number = 0; - @observable public pheight: number = 0; - - @computed private get mainContainer(): Opt<Doc> { - return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); - } - private set mainContainer(doc: Opt<Doc>) { - if (doc) { - CurrentUserUtils.UserDocument.activeWorkspace = doc; - } - } - - constructor(props: Readonly<{}>) { - super(props); - Main.Instance = this; - // causes errors to be generated when modifying an observable outside of an action - configure({ enforceActions: "observed" }); - if (window.location.pathname !== RouteStore.home) { - let pathname = window.location.pathname.split("/"); - if (pathname.length > 1 && pathname[pathname.length - 2] === 'doc') { - CurrentUserUtils.MainDocId = pathname[pathname.length - 1]; - } - } - - CurrentUserUtils.loadCurrentUser(); - - library.add(faFont); - library.add(faImage); - library.add(faFilePdf); - library.add(faObjectGroup); - library.add(faTable); - library.add(faGlobeAsia); - library.add(faUndoAlt); - library.add(faRedoAlt); - library.add(faPenNib); - library.add(faFilm); - library.add(faMusic); - library.add(faTree); - - this.initEventListeners(); - this.initAuthenticationRouters(); - - // 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("/"); - DocServer.GetRefField(pathname[pathname.length - 1]).then(action((field: Opt<Field>) => { - if (field instanceof Doc) { - this.openWorkspace(field, true); - } - })); - } - } - - 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)) { - ContextMenu.Instance.clearItems(); - } - }), true); - } - - initAuthenticationRouters = async () => { - // Load the user's active workspace, or create a new one if initial session after signup - if (!CurrentUserUtils.MainDocId) { - const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc); - if (doc) { - this.openWorkspace(doc); - } else { - this.createNewWorkspace(); - } - } else { - DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => - field instanceof Doc ? this.openWorkspace(field) : - this.createNewWorkspace(CurrentUserUtils.MainDocId)); - } - } - - @action - createNewWorkspace = async (id?: string) => { - const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); - if (list) { - let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` }); - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; - let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }); - list.push(mainDoc); - // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) - setTimeout(() => { - this.openWorkspace(mainDoc); - let pendingDocument = Docs.SchemaDocument([], { title: "New Mobile Uploads" }); - mainDoc.optionalRightCollection = pendingDocument; - }, 0); - } - } - - @action - openWorkspace = async (doc: Doc, fromHistory = false) => { - CurrentUserUtils.MainDocId = doc[Id]; - this.mainContainer = doc; - fromHistory || window.history.pushState(null, StrCast(doc.title), "/doc/" + doc[Id]); - const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc); - // 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(async () => { - if (col) { - const l = Cast(col.data, listSpec(Doc)); - if (l && l.length > 0) { - CollectionDockingView.Instance.AddRightSplit(col); - } - } - }, 100); - } - @computed - get mainContent() { - let pwidthFunc = () => this.pwidth; - let pheightFunc = () => this.pheight; - let noScaling = () => 1; - let mainCont = this.mainContainer; - return <Measure offset onResize={action((r: any) => { this.pwidth = r.offset.width; this.pheight = r.offset.height; })}> - {({ measureRef }) => - <div ref={measureRef} id="mainContent-div"> - {!mainCont ? (null) : - <DocumentView Document={mainCont} - toggleMinimized={emptyFunction} - addDocument={undefined} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - ContentScaling={noScaling} - PanelWidth={pwidthFunc} - PanelHeight={pheightFunc} - isTopMost={true} - selectOnLoad={false} - focus={emptyFunction} - parentActive={returnTrue} - whenActiveChanged={emptyFunction} - bringToFront={emptyFunction} - ContainingCollectionView={undefined} />} - <PresentationView key="presentation" /> - </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. */ - 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/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"; - - let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" })); - let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); - let addSchemaNode = action(() => Docs.SchemaDocument([], { width: 200, height: 200, title: "a schema collection" })); - let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); - // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" })); - let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" })); - let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" })); - let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); - let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })); - - let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ - [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode], - [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode], - [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode], - [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode], - [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode], - [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode], - [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], - [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], - [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], - ]; - - return < div id="add-nodes-menu" > - <input type="checkbox" id="add-menu-toggle" /> - <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> - - <div id="add-options-content"> - <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])}> - <FontAwesomeIcon icon={btn[1]} size="sm" /> - </button> - </div></li>)} - </ul> - </div> - </div >; - } - - /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */ - @computed - get miscButtons() { - let logoutRef = React.createRef<HTMLDivElement>(); - - return [ - <button className="clear-db-button" key="clear-db" onClick={DocServer.DeleteDatabase}>Clear Database</button>, - <div id="toolbar" key="toolbar"> - <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> - <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> - <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> - </div >, - <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>, - <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}> - <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div> - ]; - - } - - render() { - return ( - <div id="main-div"> - <DocumentDecorations /> - {this.mainContent} - <PreviewCursor /> - <ContextMenu /> - {this.nodesMenu()} - {this.miscButtons} - <InkingControl /> - <MainOverlayTextBox /> - </div> - ); - } - - // --------------- Northstar hooks ------------- / - private _northstarSchemas: Doc[] = []; - - @action SetNorthstarCatalog(ctlog: Catalog) { - CurrentUserUtils.NorthstarDBCatalog = ctlog; - if (ctlog && ctlog.schemas) { - ctlog.schemas.map(schema => { - let schemaDocuments: Doc[] = []; - let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema); - Promise.all(attributesToBecomeDocs.reduce((promises, attr) => { - promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { - if (field instanceof Doc) { - 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(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! })); - } - }))); - return promises; - }, [] as Promise<void>[])).finally(() => - this._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! }))); - }); - } - } - async initializeNorthstar(): Promise<void> { - 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())); - } -} +import * as React from 'react'; (async () => { await Docs.initProtos(); await CurrentUserUtils.loadCurrentUser(); - ReactDOM.render(<Main />, document.getElementById('root')); + ReactDOM.render(<MainView />, document.getElementById('root')); })(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx new file mode 100644 index 000000000..cdb105e21 --- /dev/null +++ b/src/client/views/MainView.tsx @@ -0,0 +1,288 @@ +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 { observer } from 'mobx-react'; +import "normalize.css"; +import * as React from 'react'; +import Measure from 'react-measure'; +import * as request from 'request'; +import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils'; +import { RouteStore } from '../../server/RouteStore'; +import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; +import { Docs } from '../documents/Documents'; +import { SetupDrag, DragManager } from '../util/DragManager'; +import { Transform } from '../util/Transform'; +import { UndoManager } from '../util/UndoManager'; +import { PresentationView } from './PresentationView'; +import { CollectionDockingView } from './collections/CollectionDockingView'; +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 { PreviewCursor } from './PreviewCursor'; +import { SearchBox } from './SearchBox'; +import { SelectionManager } from '../util/SelectionManager'; +import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc'; +import { Cast, FieldValue, StrCast } from '../../new_fields/Types'; +import { DocServer } from '../DocServer'; +import { listSpec } from '../../new_fields/Schema'; +import { Id } from '../../new_fields/RefField'; +import { HistoryUtil } from '../util/History'; +import { CollectionBaseView } from './collections/CollectionBaseView'; + + +@observer +export class MainView extends React.Component { + public static Instance: MainView; + @observable private _workspacesShown: boolean = false; + @observable public pwidth: number = 0; + @observable public pheight: number = 0; + + @computed private get mainContainer(): Opt<Doc> { + return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc)); + } + private set mainContainer(doc: Opt<Doc>) { + if (doc) { + if (!("presentationView" in doc)) { + doc.presentationView = new Doc(); + } + CurrentUserUtils.UserDocument.activeWorkspace = doc; + } + } + + constructor(props: Readonly<{}>) { + super(props); + MainView.Instance = this; + // causes errors to be generated when modifying an observable outside of an action + configure({ enforceActions: "observed" }); + if (window.location.search.includes("readonly")) { + DocServer.makeReadOnly(); + } + if (window.location.search.includes("safe")) { + if (!window.location.search.includes("nro")) { + DocServer.makeReadOnly(); + } + CollectionBaseView.SetSafeMode(true); + } + if (window.location.pathname !== RouteStore.home) { + let pathname = window.location.pathname.substr(1).split("/"); + if (pathname.length > 1) { + let type = pathname[0]; + if (type === "doc") { + CurrentUserUtils.MainDocId = pathname[1]; + } + } + } + + library.add(faFont); + library.add(faImage); + library.add(faFilePdf); + library.add(faObjectGroup); + library.add(faTable); + library.add(faGlobeAsia); + library.add(faUndoAlt); + library.add(faRedoAlt); + library.add(faPenNib); + library.add(faFilm); + library.add(faMusic); + library.add(faTree); + this.initEventListeners(); + this.initAuthenticationRouters(); + } + + 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)) { + ContextMenu.Instance.clearItems(); + } + }), true); + } + + initAuthenticationRouters = async () => { + // Load the user's active workspace, or create a new one if initial session after signup + if (!CurrentUserUtils.MainDocId) { + const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc); + if (doc) { + this.openWorkspace(doc); + } else { + this.createNewWorkspace(); + } + } else { + DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => + field instanceof Doc ? this.openWorkspace(field) : + this.createNewWorkspace(CurrentUserUtils.MainDocId)); + } + } + + @action + createNewWorkspace = async (id?: string) => { + const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc)); + if (list) { + let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` }); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] }; + let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id); + list.push(mainDoc); + // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) + setTimeout(() => { + this.openWorkspace(mainDoc); + let pendingDocument = Docs.SchemaDocument(["title"], [], { title: "New Mobile Uploads" }); + mainDoc.optionalRightCollection = pendingDocument; + }, 0); + } + } + + @action + openWorkspace = async (doc: Doc, fromHistory = false) => { + CurrentUserUtils.MainDocId = doc[Id]; + this.mainContainer = doc; + fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} }); + const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc); + // 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(async () => { + if (col) { + const l = Cast(col.data, listSpec(Doc)); + if (l && l.length > 0) { + CollectionDockingView.Instance.AddRightSplit(col); + } + } + }, 100); + } + @action + onResize = (r: any) => { + this.pwidth = r.offset.width; + this.pheight = r.offset.height; + } + getPWidth = () => { + return this.pwidth; + } + getPHeight = () => { + return this.pheight; + } + @computed + get mainContent() { + let mainCont = this.mainContainer; + let content = !mainCont ? (null) : + <DocumentView Document={mainCont} + toggleMinimized={emptyFunction} + addDocument={undefined} + addDocTab={emptyFunction} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + ContentScaling={returnOne} + PanelWidth={this.getPWidth} + PanelHeight={this.getPHeight} + isTopMost={true} + selectOnLoad={false} + focus={emptyFunction} + parentActive={returnTrue} + whenActiveChanged={emptyFunction} + bringToFront={emptyFunction} + ContainingCollectionView={undefined} />; + const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined; + return <Measure offset onResize={this.onResize}> + {({ measureRef }) => + <div ref={measureRef} id="mainContent-div"> + {content} + {pres ? <PresentationView Document={pres} key="presentation" /> : null} + </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. */ + 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/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"; + + let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" })); + let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); + let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" })); + let addTreeNode = action(() => CurrentUserUtils.UserDocument); + //let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" })); + // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" })); + let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" })); + let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" })); + let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" })); + let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })); + + let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [ + [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode], + [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode], + [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode], + [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode], + [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode], + [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode], + [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode], + [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode], + [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode], + ]; + + return < div id="add-nodes-menu" > + <input type="checkbox" id="add-menu-toggle" /> + <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label> + + <div id="add-options-content"> + <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])}> + <FontAwesomeIcon icon={btn[1]} size="sm" /> + </button> + </div></li>)} + </ul> + </div> + </div >; + } + + /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */ + @computed + get miscButtons() { + let logoutRef = React.createRef<HTMLDivElement>(); + + return [ + <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>, + <div id="toolbar" key="toolbar"> + <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button> + <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button> + </div >, + <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>, + <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}> + <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div> + ]; + + } + + render() { + return ( + <div id="main-div"> + <DocumentDecorations /> + {this.mainContent} + <PreviewCursor /> + <ContextMenu /> + {this.nodesMenu()} + {this.miscButtons} + <InkingControl /> + <MainOverlayTextBox /> + </div> + ); + } +} diff --git a/src/client/views/PresentationView.scss b/src/client/views/PresentationView.scss index 7c5677f0d..fb4a851c4 100644 --- a/src/client/views/PresentationView.scss +++ b/src/client/views/PresentationView.scss @@ -4,15 +4,14 @@ z-index: 1; box-shadow: #AAAAAA .2vw .2vw .4vw; right: 0; - top:0; - bottom:0; + top: 0; + bottom: 0; } .presentationView-item { - width: 220px; - height: 40px; - vertical-align: center; - padding-top: 15px; + padding: 10px; + display: inline-block; + width: 100%; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -22,47 +21,59 @@ transition: all .1s; } +.presentationView-listCont { + padding-left: 10px; + padding-right: 10px; +} + .presentationView-item:hover { transition: all .1s; background: #AAAAAA } +.presentationView-selected { + background: gray; +} + .presentationView-heading { - margin-top: 0px; - height: 40px; background: lightseagreen; - padding: 30px; + padding: 10px; + display: inline-block; + width: 100%; } + .presentationView-title { - padding-top: 3px; - padding-bottom: 3px; - font-size: 25px; - float:left; + padding-top: 3px; + padding-bottom: 3px; + font-size: 25px; + display: inline-block; } -.presentation-icon{ + +.presentation-icon { float: right; - display: inline; - width: 10px; - margin-top: 7px; } -.presentationView-header { - padding-top: 1px; - padding-bottom: 1px; + +.presentationView-name { font-size: 15px; - float:left; - } + display: inline-block; +} + +.presentation-button { + margin-right: 12.5%; + margin-left: 12.5%; + width: 25%; +} - .presentation-next{ - float: right; - } - .presentation-back{ - float: left; - } - .presentation-next:hover{ +.presentation-buttons { + padding: 10px; +} + +.presentation-next:hover { transition: all .1s; background: #AAAAAA } -.presentation-back:hover{ + +.presentation-back:hover { transition: all .1s; background: #AAAAAA }
\ No newline at end of file diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx index 3fb24a339..9c37e9000 100644 --- a/src/client/views/PresentationView.tsx +++ b/src/client/views/PresentationView.tsx @@ -1,19 +1,23 @@ import { observer } from "mobx-react"; -import React = require("react") +import React = require("react"); import { observable, action, runInAction, reaction } from "mobx"; -import "./PresentationView.scss" -import "./Main.tsx"; +import "./PresentationView.scss"; import { DocumentManager } from "../util/DocumentManager"; import { Utils } from "../../Utils"; -import { Doc } from "../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc"; import { listSpec } from "../../new_fields/Schema"; -import { Cast, NumCast, FieldValue, PromiseValue } from "../../new_fields/Types"; +import { Cast, NumCast, FieldValue, PromiseValue, StrCast } from "../../new_fields/Types"; import { Id } from "../../new_fields/RefField"; import { List } from "../../new_fields/List"; import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils"; export interface PresViewProps { - //Document: Doc; + Document: Doc; +} + +interface PresListProps extends PresViewProps { + deleteDocument(index: number): void; + gotoDocument(index: number): void; } @@ -21,72 +25,40 @@ export interface PresViewProps { /** * Component that takes in a document prop and a boolean whether it's collapsed or not. */ -class PresentationViewItem extends React.Component<PresViewProps> { - - @observable Document: Doc; - constructor(props: PresViewProps) { - super(props); - this.Document = FieldValue(Cast(FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc))!.presentationView, Doc))!; - } - //look at CollectionFreeformView.focusDocument(d) - @action - openDoc = (doc: Doc) => { - let docView = DocumentManager.Instance.getDocumentView(doc); - if (docView) { - docView.props.focus(docView.props.Document); - } - } - - /** - * Removes a document from the presentation view - **/ - @action - public RemoveDoc(doc: Doc) { - const value = Cast(this.Document.data, listSpec(Doc), []); - let index = -1; - for (let i = 0; i < value.length; i++) { - if (value[i][Id] === doc[Id]) { - index = i; - break; - } - } - if (index !== -1) { - value.splice(index, 1); - } - } +class PresentationViewList extends React.Component<PresListProps> { /** * Renders a single child document. It will just append a list element. * @param document The document to render. */ - renderChild(document: Doc) { + renderChild = (document: Doc, index: number) => { let title = document.title; //to get currently selected presentation doc - let selected = NumCast(this.Document.selectedDoc, 0); + let selected = NumCast(this.props.Document.selectedDoc, 0); - // finally, if it's a normal document, then render it as such. - const children = Cast(this.Document.data, listSpec(Doc)); - const styles: any = {}; - if (children && children[selected] === document) { + let className = "presentationView-item"; + if (selected === index) { //this doc is selected - styles.background = "gray"; + className += " presentationView-selected"; } return ( - <li className="presentationView-item" style={styles} key={Utils.GenerateGuid()}> - <div className="presentationView-header" onClick={() => this.openDoc(document)}>{title}</div> - <div className="presentation-icon" onClick={() => this.RemoveDoc(document)}>X</div> - </li> + <div className={className} key={document[Id] + index} onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}> + <strong className="presentationView-name"> + {`${index + 1}. ${title}`} + </strong> + <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button> + </div> ); } render() { - const children = Cast(this.Document.data, listSpec(Doc), []); + const children = DocListCast(this.props.Document.data); return ( - <div> - {children.map(value => this.renderChild(value))} + <div className="presentationView-listCont"> + {children.map(this.renderChild)} </div> ); } @@ -100,59 +72,42 @@ export class PresentationView extends React.Component<PresViewProps> { //observable means render is re-called every time variable is changed @observable collapsed: boolean = false; - closePresentation = action(() => this.Document!.width = 0); + closePresentation = action(() => this.props.Document.width = 0); next = () => { - const current = NumCast(this.Document!.selectedDoc); - const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc))); - if (allDocs && current < allDocs.length + 1) { - //can move forwards - this.Document!.selectedDoc = current + 1; - const doc = allDocs[current + 1]; - let docView = DocumentManager.Instance.getDocumentView(doc); - if (docView) { - docView.props.focus(docView.props.Document); - } - } + const current = NumCast(this.props.Document.selectedDoc); + this.gotoDocument(current + 1); } back = () => { - const current = NumCast(this.Document!.selectedDoc); - const allDocs = FieldValue(Cast(this.Document!.data, listSpec(Doc))); - if (allDocs && current - 1 >= 0) { - //can move forwards - this.Document!.selectedDoc = current - 1; - const doc = allDocs[current - 1]; - let docView = DocumentManager.Instance.getDocumentView(doc); - if (docView) { - docView.props.focus(docView.props.Document); - } + const current = NumCast(this.props.Document.selectedDoc); + this.gotoDocument(current - 1); + } + + @action + public RemoveDoc = (index: number) => { + const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); + if (value) { + value.splice(index, 1); } } - private ref = React.createRef<HTMLDivElement>(); + public gotoDocument = async (index: number) => { + const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); + if (!list) { + return; + } + if (index < 0 || index >= list.length) { + return; + } + + this.props.Document.selectedDoc = index; + const doc = await list[index]; + DocumentManager.Instance.jumpToDocument(doc); + } - @observable Document?: Doc; //initilize class variables constructor(props: PresViewProps) { super(props); - let self = this; - reaction(() => - CurrentUserUtils.UserDocument.activeWorkspace, - (activeW) => { - if (activeW && activeW instanceof Doc) { - PromiseValue(Cast(activeW.presentationView, Doc)). - then(pv => runInAction(() => { - if (pv) self.Document = pv; - else { - pv = new Doc(); - pv.title = "Presentation Doc"; - activeW.presentationView = pv; - self.Document = pv; - } - })) - } - }, - { fireImmediately: true }); PresentationView.Instance = this; } @@ -162,36 +117,32 @@ export class PresentationView extends React.Component<PresViewProps> { @action public PinDoc(doc: Doc) { //add this new doc to props.Document - const data = Cast(this.Document!.data, listSpec(Doc)); + const data = Cast(this.props.Document.data, listSpec(Doc)); if (data) { data.push(doc); } else { - this.Document!.data = new List([doc]); + this.props.Document.data = new List([doc]); } - this.Document!.width = 300; + this.props.Document.width = 300; } render() { - if (!this.Document) - return (null); - let titleStr = this.Document.Title; - let width = NumCast(this.Document.width); + let titleStr = StrCast(this.props.Document.title); + let width = NumCast(this.props.Document.width); //TODO: next and back should be icons return ( <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}> <div className="presentationView-heading"> <div className="presentationView-title">{titleStr}</div> - <div className='presentation-icon' onClick={this.closePresentation}>X</div></div> - <div> - <div className="presentation-back" onClick={this.back}>back</div> - <div className="presentation-next" onClick={this.next}>next</div> - + <button className='presentation-icon' onClick={this.closePresentation}>X</button> + </div> + <div className="presentation-buttons"> + <button className="presentation-button" onClick={this.back}>back</button> + <button className="presentation-button" onClick={this.next}>next</button> </div> - <ul> - <PresentationViewItem /> - </ul> + <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} /> </div> ); } diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 4218ea7c9..7c1d00eb0 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -22,9 +22,11 @@ export class PreviewCursor extends React.Component<{}> { } paste = (e: ClipboardEvent) => { console.log(e.clipboardData); - console.log(e.clipboardData.getData("text/html")); - console.log(e.clipboardData.getData("text/csv")); - console.log(e.clipboardData.getData("text/plain")); + if (e.clipboardData) { + console.log(e.clipboardData.getData("text/html")); + console.log(e.clipboardData.getData("text/csv")); + console.log(e.clipboardData.getData("text/plain")); + } } @action diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx index 0da0bdae8..01c7316d6 100644 --- a/src/client/views/SearchItem.tsx +++ b/src/client/views/SearchItem.tsx @@ -40,6 +40,14 @@ export class SearchItem extends React.Component<SearchProps> { faCaretUp; return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />; } + onPointerEnter = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = true; + Doc.SetOnPrototype(this.props.doc, "protoBrush", true); + } + onPointerLeave = (e: React.PointerEvent) => { + this.props.doc.libraryBrush = false; + Doc.SetOnPrototype(this.props.doc, "protoBrush", false); + } collectionRef = React.createRef<HTMLDivElement>(); startDocDrag = () => { @@ -53,7 +61,9 @@ export class SearchItem extends React.Component<SearchProps> { } render() { return ( - <div className="search-item" ref={this.collectionRef} id="result" onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} > + <div className="search-item" ref={this.collectionRef} id="result" + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} + onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} > <div className="search-title" id="result" >title: {this.props.doc.title}</div> {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */} {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */} diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index e2b3bd07a..e5b679e24 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -38,22 +38,21 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { constructor(props: TemplateMenuProps) { super(props); - console.log(""); } @action toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => { if (event.target.checked) { - if (template.Name == "Bullet") { + if (template.Name === "Bullet") { let topDocView = this.props.docs[0]; topDocView.addTemplate(template); - topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document)); + topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!)); } else { this.props.docs.map(d => d.addTemplate(template)); } this.props.templates.set(template, true); } else { - if (template.Name == "Bullet") { + if (template.Name === "Bullet") { let topDocView = this.props.docs[0]; topDocView.removeTemplate(template); topDocView.props.Document.subBulletDocs = undefined; diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx index a98870b04..5a99b3d90 100644 --- a/src/client/views/Templates.tsx +++ b/src/client/views/Templates.tsx @@ -40,8 +40,11 @@ export namespace Templates { // export const BasicLayout = new Template("Basic layout", "{layout}"); export const Caption = new Template("Caption", TemplatePosition.OutterBottom, - `<div id="screenSpace" style="top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"> - <FormattedTextBox {...props} fieldKey={"caption"} /> + `<div> + <div style="height:100%; width:100%;position:absolute;">{layout}</div> + <div id="screenSpace" style="top: 100%; font-size:14px; background:yellow; width:100%; position:absolute"> + <FormattedTextBox {...props} fieldKey={"caption"} /> + </div> </div>` ); export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop, @@ -59,19 +62,20 @@ export namespace Templates { </div></div>` ); export const Bullet = new Template("Bullet", TemplatePosition.InnerTop, - `<div><div style="height:100%; width:100%;position:absolute;">{layout}</div> - <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;"> - <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg==" - width="15px" height="15px"/> - </div> + `<div> + <div style="height:100%; width:100%;position:absolute;">{layout}</div> + <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;"> + <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg==" + width="15px" height="15px"/> + </div> </div>` ); export function ImageOverlay(width: number, height: number, field: string = "thumbnail") { return (`<div> - <div style="height:100%; width:100%;position:absolute;">{layout}</div> - <div style="width:${width}px; height:${height}px; top:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;"> - <ImageBox id="isExpander" {...props} PanelWidth={${width}} fieldKey={"${field}"} /> + <div style="height:100%; width:100%; position:absolute;">{layout}</div> + <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;"> + <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} /> </div> </div>`); } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 645296d27..84ffbac36 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -1,4 +1,4 @@ -import { action, computed } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; @@ -36,9 +36,20 @@ export interface CollectionViewProps extends FieldViewProps { @observer export class CollectionBaseView extends React.Component<CollectionViewProps> { + @observable private static _safeMode = false; + static InSafeMode() { return this._safeMode; } + static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } get collectionViewType(): CollectionViewType | undefined { let Document = this.props.Document; let viewField = Cast(Document.viewType, "number"); + if (CollectionBaseView._safeMode) { + if (viewField === CollectionViewType.Freeform) { + return CollectionViewType.Tree; + } + if (viewField === CollectionViewType.Invalid) { + return CollectionViewType.Freeform; + } + } if (viewField !== undefined) { return viewField; } else { @@ -82,12 +93,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { } return false; } - @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } + @computed get isAnnotationOverlay() { return this.props.fieldKey === "annotations"; } @action.bound addDocument(doc: Doc, allowDuplicates: boolean = false): boolean { let props = this.props; - var curPage = Cast(props.Document.curPage, "number", -1); + var curPage = NumCast(props.Document.curPage, -1); Doc.SetOnPrototype(doc, "page", curPage); if (curPage >= 0) { Doc.SetOnPrototype(doc, "annotationOn", props.Document); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index efcee9c02..06b262d79 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,11 +1,11 @@ import 'golden-layout/src/css/goldenlayout-base.css'; import 'golden-layout/src/css/goldenlayout-dark-theme.css'; -import { action, observable, reaction } from "mobx"; +import { action, observable, reaction, Lambda } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; import * as GoldenLayout from "../../../client/goldenLayout"; -import { Doc, Field, Opt } from "../../../new_fields/Doc"; +import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc"; import { FieldId, Id } from "../../../new_fields/RefField"; import { listSpec } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; @@ -18,6 +18,9 @@ import { DocumentView } from "../nodes/DocumentView"; import "./CollectionDockingView.scss"; import { SubCollectionViewProps } from "./CollectionSubView"; import React = require("react"); +import { ParentDocSelector } from './ParentDocumentSelector'; +import { DocumentManager } from '../../util/DocumentManager'; +import { CollectionViewType } from './CollectionBaseView'; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -72,32 +75,40 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp @undoBatch @action - public CloseRightSplit(document: Doc) { + public CloseRightSplit(document: Doc): boolean { + let retVal = false; if (this._goldenLayout.root.contentItems[0].isRow) { - this._goldenLayout.root.contentItems[0].contentItems.map((child: any, i: number) => { + retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => { if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" && - child.contentItems[0].config.props.documentId == document[Id]) { + Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) { child.contentItems[0].remove(); this.layoutChanged(document); - this.stateChanged(); - } else - child.contentItems.map((tab: any, j: number) => { - if (tab.config.component === "DocumentFrameRenderer" && tab.config.props.documentId === document[Id]) { + return true; + } else { + Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => { + if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) { child.contentItems[j].remove(); child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0); let docs = Cast(this.props.Document.data, listSpec(Doc)); docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1); - this.stateChanged(); + return true; } + return false; }); - }) + } + return false; + }); + } + if (retVal) { + this.stateChanged(); } + return retVal; } @action layoutChanged(removed?: Doc) { this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]); - this._goldenLayout.emit('sbcreteChanged'); + this._goldenLayout.emit('stateChanged'); this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); if (removed) CollectionDockingView.Instance._removedDocs.push(removed); this.stateChanged(); @@ -143,6 +154,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp return newContentItem; } + @action + public AddTab(stack: any, document: Doc) { + let docs = Cast(this.props.Document.data, listSpec(Doc)); + if (docs) { + docs.push(document); + } + let docContentConfig = CollectionDockingView.makeDocumentConfig(document); + var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout); + stack.addChild(newContentItem.contentItems[0], undefined); + this.layoutChanged(); + } setupGoldenLayout() { var config = StrCast(this.props.Document.dockingConfig); @@ -157,6 +179,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp try { this._goldenLayout.unbind('itemDropped', this.itemDropped); this._goldenLayout.unbind('tabCreated', this.tabCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); this._goldenLayout.unbind('stackCreated', this.stackCreated); } catch (e) { } this._goldenLayout.destroy(); @@ -164,6 +187,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } this._goldenLayout.on('itemDropped', this.itemDropped); this._goldenLayout.on('tabCreated', this.tabCreated); + this._goldenLayout.on('tabDestroyed', this.tabDestroyed); this._goldenLayout.on('stackCreated', this.stackCreated); this._goldenLayout.registerComponent('DocumentFrameRenderer', DockedFrameRenderer); this._goldenLayout.container = this._containerRef.current; @@ -177,12 +201,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.init(); } } + reactionDisposer?: Lambda; componentDidMount: () => void = () => { if (this._containerRef.current) { - reaction( + this.reactionDisposer = reaction( () => StrCast(this.props.Document.dockingConfig), () => { if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) { + // Because this is in a set timeout, if this component unmounts right after mounting, + // we will leak a GoldenLayout, because we try to destroy it before we ever create it setTimeout(() => this.setupGoldenLayout(), 1); } this._ignoreStateChange = ""; @@ -196,12 +223,17 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp this._goldenLayout.unbind('itemDropped', this.itemDropped); this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('stackCreated', this.stackCreated); + this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); } catch (e) { } if (this._goldenLayout) this._goldenLayout.destroy(); this._goldenLayout = null; window.removeEventListener('resize', this.onResize); + + if (this.reactionDisposer) { + this.reactionDisposer(); + } } @action onResize = (event: any) => { @@ -238,6 +270,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp let y = e.clientY; let docid = (e.target as any).DashDocId; let tab = (e.target as any).parentElement as HTMLElement; + let glTab = (e.target as any).Tab; + if (glTab && glTab.contentItem && glTab.contentItem.parent) { + glTab.contentItem.parent.setActiveContentItem(glTab.contentItem); + } DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y, @@ -292,21 +328,23 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp } DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => { if (doc instanceof Doc) { - let counter: any = this.htmlToElement(`<div class="messageCounter">0</div>`); + let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`); tab.element.append(counter); + let upDiv = document.createElement("span"); + ReactDOM.render(<ParentDocSelector Document={doc} />, upDiv); + tab.reactComponents = [upDiv]; + tab.element.append(upDiv); counter.DashDocId = tab.contentItem.config.props.documentId; tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title], () => { - const lf = Cast(doc.linkedFromDocs, listSpec(Doc), []); - const lt = Cast(doc.linkedToDocs, listSpec(Doc), []); - let count = (lf ? lf.length : 0) + (lt ? lt.length : 0); - counter.innerHTML = count; + counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length; tab.titleElement[0].textContent = doc.title; }, { fireImmediately: true }); tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId; } }); } + tab.titleElement[0].Tab = tab; tab.closeElement.off('click') //unbind the current click handler .click(async function () { if (tab.reactionDisposer) { @@ -320,6 +358,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp tab.contentItem.remove(); }); } + + tabDestroyed = (tab: any) => { + if (tab.reactComponents) { + for (const ele of tab.reactComponents) { + ReactDOM.unmountComponentAtNode(ele); + } + } + } _removedDocs: Doc[] = []; stackCreated = (stack: any) => { @@ -366,9 +412,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { @observable private _panelWidth = 0; @observable private _panelHeight = 0; @observable private _document: Opt<Doc>; - + _stack: any; constructor(props: any) { super(props); + this._stack = (this.props as any).glContainer.parent.parent; DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); } @@ -385,19 +432,37 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { 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 CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(1 / scale); } return Transform.Identity(); } + get scaleToFitMultiplier() { + let docWidth = NumCast(this._document!.width); + let docHeight = NumCast(this._document!.height); + if (NumCast(this._document!.nativeWidth) || !docWidth || !this._panelWidth || !this._panelHeight) return 1; + if (StrCast(this._document!.layout).indexOf("Collection") === -1 || + NumCast(this._document!.viewType) !== CollectionViewType.Freeform) return 1; + let scaling = Math.max(1, this._panelWidth / docWidth * docHeight > this._panelHeight ? + this._panelHeight / docHeight : this._panelWidth / docWidth); + console.log("Scaling = " + scaling); + return scaling; + } get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; } + addDocTab = (doc: Doc, location: string) => { + if (location === "onRight") { + CollectionDockingView.Instance.AddRightSplit(doc); + } else { + CollectionDockingView.Instance.AddTab(this._stack, doc); + } + } get content() { if (!this._document) { return (null); } return ( <div className="collectionDockingView-content" ref={this._mainCont} - style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> + style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}> <DocumentView key={this._document[Id]} Document={this._document} toggleMinimized={emptyFunction} bringToFront={emptyFunction} @@ -412,6 +477,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { parentActive={returnTrue} whenActiveChanged={emptyFunction} focus={emptyFunction} + addDocTab={this.addDocTab} ContainingCollectionView={undefined} /> </div >); } diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx index b3762206a..a6614da21 100644 --- a/src/client/views/collections/CollectionPDFView.tsx +++ b/src/client/views/collections/CollectionPDFView.tsx @@ -61,7 +61,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: emptyFunction }); + ContextMenu.Instance.addItem({ description: "PDFOptions", event: emptyFunction, icon: "file-pdf" }); } } diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index cfdb3ab22..df5c52d01 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -4,7 +4,7 @@ .collectionSchemaView-container { border-width: $COLLECTION_BORDER_WIDTH; - border-color : $intermediate-color; + border-color: $intermediate-color; border-style: solid; border-radius: $border-radius; box-sizing: border-box; @@ -14,42 +14,52 @@ .collectionSchemaView-cellContents { height: $MAX_ROW_HEIGHT; + + img { + width: auto; + max-height: $MAX_ROW_HEIGHT; + } } - + .collectionSchemaView-previewRegion { position: relative; background: $light-color; float: left; height: 100%; + .collectionSchemaView-previewDoc { height: 100%; - width: 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; height: 15px; - width: 15px; - z-index: 20; - right: 0; - top: 20px; - background: Black ; + width: 15px; + z-index: 20; + right: 0; + top: 20px; + background: Black; } - .collectionSchemaView-dividerDragger{ - position: relative; - background: black; - float: left; + + .collectionSchemaView-dividerDragger { + position: relative; + background: black; + float: left; height: 37px; width: 20px; z-index: 20; @@ -57,6 +67,7 @@ top: 0; background: $main-accent; } + .collectionSchemaView-columnsHandle { position: absolute; height: 37px; @@ -66,6 +77,7 @@ bottom: 0; background: $main-accent; } + .collectionSchemaView-colDividerDragger { position: relative; box-sizing: border-box; @@ -74,6 +86,7 @@ float: top; width: 100%; } + .collectionSchemaView-dividerDragger { position: relative; box-sizing: border-box; @@ -82,11 +95,13 @@ float: left; height: 100%; } + .collectionSchemaView-tableContainer { position: relative; float: left; height: 100%; } + .ReactTable { // position: absolute; // display: inline-block; // overflow: auto; @@ -95,6 +110,7 @@ background: $light-color; box-sizing: border-box; border: none !important; + .rt-table { overflow-y: auto; overflow-x: auto; @@ -103,42 +119,50 @@ direction: ltr; // direction:rtl; // display:block; } + .rt-tbody { //direction: ltr; direction: rtl; } + .rt-tr-group { direction: ltr; max-height: $MAX_ROW_HEIGHT; } + .rt-td { border-width: 1px; border-right-color: $intermediate-color; + .imageBox-cont { position: relative; max-height: 100%; } + .imageBox-cont img { object-fit: contain; max-width: 100%; height: 100%; } - .videobox-cont { + + .videoBox-cont { object-fit: contain; width: auto; height: 100%; } } } + .ReactTable .rt-thead.-header { background: $intermediate-color; color: $light-color; - text-transform: uppercase; + // text-transform: uppercase; letter-spacing: 2px; font-size: 12px; height: 30px; padding-top: 4px; } + .ReactTable .rt-th, .ReactTable .rt-td { max-height: $MAX_ROW_HEIGHT; @@ -146,32 +170,36 @@ font-size: 13px; text-align: center; } + .ReactTable .rt-tbody .rt-tr-group:last-child { border-bottom: $intermediate-color; border-bottom-style: solid; border-bottom-width: 1; } + .documentView-node-topmost { - text-align:left; + text-align: left; transform-origin: center top; display: inline-block; } + .documentView-node:first-child { background: $light-color; } } + //options menu styling #schemaOptionsMenuBtn { position: absolute; height: 20px; width: 20px; border-radius: 50%; - z-index: 21; + z-index: 21; right: 4px; - top: 4px; + top: 4px; pointer-events: auto; - background-color:black; - display:inline-block; + background-color: black; + display: inline-block; padding: 0px; font-size: 100%; } @@ -185,10 +213,12 @@ ul { padding: 0px; margin: 0px; } + .schema-options-subHeader { color: $intermediate-color; margin-bottom: 5px; } + #schemaOptionsMenuBtn:hover { transform: scale(1.15); } @@ -198,15 +228,15 @@ ul { font-size: 12px; } - #options-flyout-div { +#options-flyout-div { text-align: left; - padding:0px; + padding: 0px; z-index: 100; font-family: $sans-serif; padding-left: 5px; - } +} - #schema-col-checklist { +#schema-col-checklist { overflow: scroll; text-align: left; //background-color: $light-color-secondary; @@ -214,8 +244,8 @@ ul { max-height: 175px; font-family: $sans-serif; font-size: 12px; - } - +} + .Resizer { box-sizing: border-box; @@ -223,6 +253,7 @@ ul { opacity: 0.5; z-index: 1; background-clip: padding-box; + &.horizontal { height: 11px; margin: -5px 0; @@ -230,22 +261,26 @@ ul { border-bottom: 5px solid rgba(255, 255, 255, 0); cursor: row-resize; width: 100%; + &:hover { border-top: 5px solid rgba(0, 0, 0, 0.5); border-bottom: 5px solid rgba(0, 0, 0, 0.5); } } + &.vertical { width: 11px; margin: 0 -5px; border-left: 5px solid rgba(255, 255, 255, 0); border-right: 5px solid rgba(255, 255, 255, 0); cursor: col-resize; + &:hover { border-left: 5px solid rgba(0, 0, 0, 0.5); border-right: 5px solid rgba(0, 0, 0, 0.5); } } + &:hover { -webkit-transition: all 2s ease; transition: all 2s ease; @@ -266,10 +301,12 @@ ul { -ms-flex-direction: column; flex-direction: column; } + header { padding: 1rem; background: #eee; } + footer { padding: 1rem; background: #eee; @@ -283,10 +320,12 @@ ul { display: flex; flex-direction: column; } + header { padding: 1rem; background: #eee; } + footer { padding: 1rem; background: #eee; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 18319dc77..f15da41ff 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -20,10 +20,13 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { List } from "../../../new_fields/List"; import { Id } from "../../../new_fields/RefField"; +import { Gateway } from "../../northstar/manager/Gateway"; +import { Docs } from "../../documents/Documents"; +import { ContextMenu } from "../ContextMenu"; // bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657 @@ -61,10 +64,25 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } + @computed get tableColumns() { + return this.columns.map(col => { + const ref = React.createRef<HTMLParagraphElement>(); + return { + Header: <p ref={ref} onPointerDown={SetupDrag(ref, () => this.onHeaderDrag(col))}>{col}</p>, + accessor: (doc: Doc) => doc ? doc[col] : 0, + id: col + }; + }); + } + + onHeaderDrag = (columnName: string) => { + return this.props.Document; + } + renderCell = (rowProps: CellInfo) => { let props: FieldViewProps = { - Document: rowProps.value[0], - fieldKey: rowProps.value[1], + Document: rowProps.original, + fieldKey: rowProps.column.id as string, ContainingCollectionView: this.props.CollectionView, isSelected: returnFalse, select: emptyFunction, @@ -76,24 +94,24 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { whenActiveChanged: emptyFunction, PanelHeight: returnZero, PanelWidth: returnZero, + addDocTab: this.props.addDocTab, }; - let contents = ( - <FieldView {...props} /> - ); + let fieldContentView = <FieldView {...props} />; let reference = React.createRef<HTMLDivElement>(); - let onItemDown = SetupDrag(reference, () => props.Document, this.props.moveDocument); + let onItemDown = (e: React.PointerEvent) => + (this.props.CollectionView.props.isSelected() ? + SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined); let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => { const res = run({ this: doc }); if (!res.success) return false; - const field = res.result; - doc[props.fieldKey] = field; + doc[props.fieldKey] = res.result; return true; }; return ( <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}> <EditableView display={"inline"} - contents={contents} + contents={fieldContentView} height={Number(MAX_ROW_HEIGHT)} GetValue={() => { let field = props.Document[props.fieldKey]; @@ -118,7 +136,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } const run = script.run; //TODO This should be able to be refactored to compile the script once - const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]) + const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]); val && val.forEach(doc => applyToDoc(doc, run)); }}> </EditableView> @@ -207,6 +225,32 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { } } + 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: "Make DB", event: this.makeDB }); + } + } + + @action + makeDB = async () => { + let csv: string = this.columns.reduce((val, col) => val + col + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + let self = this; + DocListCast(this.props.Document.data).map(doc => { + csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", ""); + csv = csv.substr(0, csv.length - 1) + "\n"; + }); + csv.substring(0, csv.length - 1); + let dbName = StrCast(this.props.Document.title); + let res = await Gateway.Instance.PostSchema(csv, dbName); + if (self.props.CollectionView.props.addDocument) { + let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }); + if (schemaDoc) { + self.props.CollectionView.props.addDocument(schemaDoc, false); + } + } + } + @action addColumn = () => { this.columns.push(this._newKeyName); @@ -224,10 +268,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { this.previewScript = e.currentTarget.value; } + @computed get previewDocument(): Doc | undefined { - const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []); + const children = DocListCast(this.props.Document[this.props.fieldKey]); const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined; - return selected ? (this.previewScript ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined; + return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : 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; } @@ -253,8 +298,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { get previewPanel() { // let doc = CompileScript(this.previewScript, { this: selected }, true)(); const previewDoc = this.previewDocument; - return !previewDoc || !this.previewRegionWidth ? (null) : ( - <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> + return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}> + {!previewDoc || !this.previewRegionWidth ? (null) : ( <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false} toggleMinimized={emptyFunction} @@ -267,12 +312,12 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { parentActive={this.props.active} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} + addDocTab={this.props.addDocTab} /> - </div> - <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} - style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} /> - </div> - ); + </div>)} + <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange} + style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} /> + </div>); } get documentKeysCheckList() { @@ -319,20 +364,18 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />; } + + render() { library.add(faCog); library.add(faPlus); - const children = this.children; + const children = this.childDocs; return ( <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel} - onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}> + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} 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, - accessor: (doc: Doc) => [doc, col], - id: col - }))} + columns={this.tableColumns} column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }} getTrProps={this.getTrProps} /> diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a86d250bd..864fdfa4b 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -18,6 +18,7 @@ import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; import { ObjectField } from "../../../new_fields/ObjectField"; import CursorField, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField"; +import { url } from "inspector"; export interface CollectionViewProps extends FieldViewProps { addDocument: (document: Doc, allowDuplicates?: boolean) => boolean; @@ -46,7 +47,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.createDropTarget(ele); } - get children() { + get childDocs() { //TODO tfs: This might not be what we want? //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue) return DocListCast(this.props.Document[this.props.fieldKey]); @@ -168,6 +169,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { this.props.addDocument(htmlDoc, false); return; } + if (text && text.indexOf("www.youtube.com/watch") !== -1) { + const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/"); + this.props.addDocument(Docs.WebDocument(url, { ...options, width: 300, height: 300 })); + return; + } let batch = UndoManager.StartBatch("collection view drop"); let promises: Promise<void>[] = []; diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index 411d67ff7..5f82137c6 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -29,23 +29,24 @@ } .bullet { - float:left; + float: left; position: relative; width: 15px; display: block; color: $intermediate-color; margin-top: 3px; - transform: scale(1.3,1.3); + transform: scale(1.3, 1.3); } .docContainer { margin-left: 10px; display: block; - // width:100%;//width: max-content; + // width:100%;//width: max-content; } + .docContainer:hover { .treeViewItem-openRight { - display:inline; + display: inline; } } @@ -61,10 +62,12 @@ // margin-top: 3px; display: inline; } + .treeViewItem-openRight { margin-left: 5px; - display:none; + display: none; } + .docContainer:hover { .delete-button { display: inline; @@ -73,14 +76,16 @@ } .coll-title { - width:max-content; + width: max-content; display: block; font-size: 24px; } + .collection-child { margin-top: 10px; margin-bottom: 10px; } + .collectionTreeView-keyHeader { font-style: italic; font-size: 8pt; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index d22418b2c..72fa69cb1 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -14,12 +14,11 @@ import { Doc, DocListCast } from '../../../new_fields/Doc'; import { Id } from '../../../new_fields/RefField'; import { ContextMenu } from '../ContextMenu'; import { undoBatch } from '../../util/UndoManager'; -import { Main } from '../Main'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; import { CollectionDockingView } from './CollectionDockingView'; import { DocumentManager } from '../../util/DocumentManager'; -import { List } from '../../../new_fields/List'; import { Docs } from '../../documents/Documents'; +import { MainView } from '../MainView'; export interface TreeViewProps { @@ -52,11 +51,11 @@ class TreeView extends React.Component<TreeViewProps> { @undoBatch openRight = async () => { if (this.props.document.dockingConfig) { - Main.Instance.openWorkspace(this.props.document); + MainView.Instance.openWorkspace(this.props.document); } else { CollectionDockingView.Instance.AddRightSplit(this.props.document); } - }; + } get children() { return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc)); @@ -112,7 +111,7 @@ class TreeView extends React.Component<TreeViewProps> { let editableView = (titleString: string) => (<EditableView oneLine={!this._isOver ? true : false} - display={"block"} + display={"inline-block"} contents={titleString} height={36} GetValue={() => StrCast(this.props.document.title)} @@ -130,7 +129,7 @@ class TreeView extends React.Component<TreeViewProps> { </div>); return ( <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} - style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} + style={{ background: BoolCast(this.props.document.protoBrush, false) ? "#06123232" : BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}> {editableView(StrCast(this.props.document.title))} {openRight} @@ -140,7 +139,7 @@ class TreeView extends React.Component<TreeViewProps> { onWorkspaceContextMenu = (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 - ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => Main.Instance.openWorkspace(this.props.document)) }); + ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) }); ContextMenu.Instance.addItem({ description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document, @@ -184,8 +183,9 @@ class TreeView extends React.Component<TreeViewProps> { {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction)} </div> </ul >); - } else + } else { bulletType = BulletType.Collapsed; + } } }); return <div className="treeViewItem-container" @@ -198,8 +198,8 @@ class TreeView extends React.Component<TreeViewProps> { </div>; } public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) { - return docs.filter(child => child instanceof Doc && !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).filter(doc => FieldValue(doc)).map(child => - <TreeView document={child as Doc} key={(child as Doc)[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); + return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child => + <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />); } } @@ -214,7 +214,7 @@ export class CollectionTreeView extends CollectionSubView(Document) { } 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 - ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => Main.Instance.createNewWorkspace()) }); + ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) }); } if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) { ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) }); @@ -222,16 +222,16 @@ export class CollectionTreeView extends CollectionSubView(Document) { } render() { let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType; - if (!this.children) { + if (!this.childDocs) { return (null); } - let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction); + let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction); return ( <div id="body" className="collectionTreeView-dropTarget" style={{ borderRadius: "inherit" }} onContextMenu={this.onContextMenu} - onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onWheel={(e: React.WheelEvent) => this.props.isSelected() && e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> <div className="coll-title"> <EditableView diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss index ed56ad268..db8b84832 100644 --- a/src/client/views/collections/CollectionVideoView.scss +++ b/src/client/views/collections/CollectionVideoView.scss @@ -2,7 +2,7 @@ .collectionVideoView-cont{ width: 100%; height: 100%; - position: absolute; + position: inherit; top: 0; left:0; diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index cb3fd1ba4..9ab959f3c 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -9,27 +9,26 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import { emptyFunction } from "../../../Utils"; import { Id } from "../../../new_fields/RefField"; import { VideoBox } from "../nodes/VideoBox"; +import { NumCast } from "../../../new_fields/Types"; @observer export class CollectionVideoView extends React.Component<FieldViewProps> { - private _videoBox: VideoBox | undefined = undefined; - @observable _playTimer?: NodeJS.Timeout = undefined; - - @observable _currentTimecode: number = 0; + private _videoBox?: VideoBox; public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(CollectionVideoView, fieldKey); } private get uIButtons() { let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale); + let curTime = NumCast(this.props.Document.curPage); return ([ <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - <span>{"" + Math.round(this._currentTimecode)}</span> - <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span> + <span>{"" + Math.round(curTime)}</span> + <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span> </div>, <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> - {this._playTimer ? "\"" : ">"} + {this._videoBox && this._videoBox.Playing ? "\"" : ">"} </div>, <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}> F @@ -38,36 +37,20 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } @action - updateTimecode = () => { - if (this._videoBox && this._videoBox.player) { - this._currentTimecode = this._videoBox.player.currentTime; - this.props.Document.curPage = Math.round(this._currentTimecode); - } - } - - componentDidMount() { this.updateTimecode(); } - - componentWillUnmount() { if (this._playTimer) clearInterval(this._playTimer); } - - @action onPlayDown = () => { if (this._videoBox && this._videoBox.player) { - if (this._videoBox.player.paused) { - this._videoBox.player.play(); - if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); + if (this._videoBox.Playing) { + this._videoBox.Pause(); } else { - this._videoBox.player.pause(); - if (this._playTimer) clearInterval(this._playTimer); - this._playTimer = undefined; - + this._videoBox.Play(); } } } @action onFullDown = (e: React.PointerEvent) => { - if (this._videoBox && this._videoBox.player) { - this._videoBox.player.requestFullscreen(); + if (this._videoBox) { + this._videoBox.FullScreen(); e.stopPropagation(); e.preventDefault(); } @@ -75,22 +58,18 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action onResetDown = () => { - if (this._videoBox && this._videoBox.player) { - this._videoBox.player.pause(); - this._videoBox.player.currentTime = 0; - if (this._playTimer) clearInterval(this._playTimer); - this._playTimer = undefined; - this.updateTimecode(); + if (this._videoBox) { + this._videoBox.Pause(); + this.props.Document.curPage = 0; } } 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: emptyFunction }); } } - setVideoBox = (player: VideoBox) => { this._videoBox = player; }; + setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; }; private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => { let props = { ...this.props, ...renderProps }; @@ -101,6 +80,7 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { } render() { + trace(); return ( <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}> {this.subView} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 8c1442d38..b9ffc11a2 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,17 +1,23 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faProjectDiagram, faSquare, faTh, faTree } from '@fortawesome/free-solid-svg-icons'; +import { observer } from "mobx-react"; import * as React from 'react'; -import { FieldViewProps, FieldView } from '../nodes/FieldView'; -import { CollectionBaseView, CollectionViewType, CollectionRenderProps } from './CollectionBaseView'; -import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionSchemaView } from './CollectionSchemaView'; -import { CollectionDockingView } from './CollectionDockingView'; -import { CollectionTreeView } from './CollectionTreeView'; -import { ContextMenu } from '../ContextMenu'; +import { Id } from '../../../new_fields/RefField'; import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils'; -import { observer } from 'mobx-react'; import { undoBatch } from '../../util/UndoManager'; -import { trace } from 'mobx'; -import { Id } from '../../../new_fields/RefField'; -import { Main } from '../Main'; +import { ContextMenu } from "../ContextMenu"; +import { FieldView, FieldViewProps } from '../nodes/FieldView'; +import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView'; +import { CollectionDockingView } from "./CollectionDockingView"; +import { CollectionSchemaView } from "./CollectionSchemaView"; +import { CollectionTreeView } from "./CollectionTreeView"; +import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +export const COLLECTION_BORDER_WIDTH = 2; + +library.add(faTh); +library.add(faTree); +library.add(faSquare); +library.add(faProjectDiagram); @observer export class CollectionView extends React.Component<FieldViewProps> { @@ -34,9 +40,12 @@ export class CollectionView extends React.Component<FieldViewProps> { onContextMenu = (e: React.MouseEvent): void => { if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 - ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform) }); - ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema) }); - ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree) }); + ContextMenu.Instance.addItem({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "project-diagram" }); + if (CollectionBaseView.InSafeMode()) { + ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" }); + } + ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "project-diagram" }); + ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" }); } } diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss new file mode 100644 index 000000000..f3c605f3e --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.scss @@ -0,0 +1,8 @@ +.PDS-flyout { + position: absolute; + z-index: 9999; + background-color: #d3d3d3; + box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); + min-width: 150px; + color: black; +}
\ No newline at end of file diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx new file mode 100644 index 000000000..52f7914f3 --- /dev/null +++ b/src/client/views/collections/ParentDocumentSelector.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import './ParentDocumentSelector.scss'; +import { Doc } from "../../../new_fields/Doc"; +import { observer } from "mobx-react"; +import { observable, action, runInAction } from "mobx"; +import { Id } from "../../../new_fields/RefField"; +import { SearchUtil } from "../../util/SearchUtil"; +import { CollectionDockingView } from "./CollectionDockingView"; + +@observer +export class SelectorContextMenu extends React.Component<{ Document: Doc }> { + @observable private _docs: Doc[] = []; + + constructor(props: { Document: Doc }) { + super(props); + + this.fetchDocuments(); + } + + async fetchDocuments() { + const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true); + runInAction(() => this._docs = docs); + } + + render() { + return ( + <> + {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)} + </> + ); + } +} + +@observer +export class ParentDocSelector extends React.Component<{ Document: Doc }> { + @observable hover = false; + + @action + onMouseLeave = () => { + this.hover = false; + } + + @action + onMouseEnter = () => { + this.hover = true; + } + + render() { + let flyout; + if (this.hover) { + flyout = ( + <div className="PDS-flyout"> + <SelectorContextMenu Document={this.props.Document} /> + </div> + ); + } + return ( + <span style={{ position: "relative", display: "inline-block" }} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave}> + <p>^</p> + {flyout} + </span> + ); + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index dc86f38b6..9cb8443f4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -20,10 +20,11 @@ import React = require("react"); import v5 = require("uuid/v5"); import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc"; -import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types"; +import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types"; import { pageSchema } from "../../nodes/ImageBox"; import { Id } from "../../../../new_fields/RefField"; import { InkField, StrokeData } from "../../../../new_fields/InkField"; +import { HistoryUtil } from "../../../util/History"; export const panZoomSchema = createSchema({ panX: "number", @@ -36,23 +37,22 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema) @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { - public static RIGHT_BTN_DRAG = false; 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(); } - @computed get nativeWidth() { return NumCast(this.Document.nativeWidth, 0); } - @computed get nativeHeight() { return NumCast(this.Document.nativeHeight, 0); } + @computed get nativeWidth() { return this.Document.nativeWidth || 0; } + @computed get nativeHeight() { return this.Document.nativeHeight || 0; } + public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } - private panX = () => FieldValue(this.Document.panX, 0); - private panY = () => FieldValue(this.Document.panY, 0); - private zoomScaling = () => FieldValue(this.Document.scale, 1); + private panX = () => this.Document.panX || 0; + private panY = () => this.Document.panY || 0; + private zoomScaling = () => this.Document.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 getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).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: Doc) => { @@ -65,13 +65,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { return true; } private selectDocuments = (docs: Doc[]) => { - SelectionManager.DeselectAll; + SelectionManager.DeselectAll(); docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => SelectionManager.SelectDoc(dv!, true)); } public getActiveDocuments = () => { const curPage = FieldValue(this.Document.curPage, -1); - return this.children.filter(doc => { + return this.childDocs.filter(doc => { var page = NumCast(doc.page, -1); return page === curPage || page === -1; }); @@ -89,7 +89,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let y = yp - de.data.yOffset / zoom; let dropX = NumCast(de.data.droppedDocuments[0].x); let dropY = NumCast(de.data.droppedDocuments[0].y); - de.data.droppedDocuments.map(d => { + de.data.droppedDocuments.forEach(d => { d.x = x + NumCast(d.x) - dropX; d.y = y + NumCast(d.y) - dropY; if (!NumCast(d.width)) { @@ -111,11 +111,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerDown = (e: React.PointerEvent): void => { - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && - (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || - (e.button === 0 && e.altKey)) && this.props.active())) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && - ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) { + if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointermove", this.onPointerMove); @@ -133,20 +129,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action onPointerMove = (e: PointerEvent): void => { if (!e.cancelBubble) { - let x = NumCast(this.props.Document.panX); - let y = NumCast(this.props.Document.panY); - let docs = this.children || []; + let x = this.Document.panX || 0; + let y = this.Document.panY || 0; + let docs = this.childDocs || []; let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); if (!this.isAnnotationOverlay) { let minx = docs.length ? NumCast(docs[0].x) : 0; - let maxx = docs.length ? NumCast(docs[0].width) + minx : minx; + let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis) + minx : minx; let miny = docs.length ? NumCast(docs[0].y) : 0; - let maxy = docs.length ? NumCast(docs[0].height) + miny : miny; + let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis) + miny : miny; let ranges = docs.filter(doc => doc).reduce((range, doc) => { let x = NumCast(doc.x); - let xe = x + NumCast(doc.width); + let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis); let y = NumCast(doc.y); - let ye = y + NumCast(doc.height); + let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis); 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]]); @@ -179,7 +175,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // if (!this.props.active()) { // return; // } - let childSelected = this.children.some(doc => { + let childSelected = this.childDocs.some(doc => { var dv = DocumentManager.Instance.getDocumentView(doc); return dv && SelectionManager.IsSelected(dv) ? true : false; }); @@ -210,7 +206,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); - let safeScale = Math.abs(localTransform.Scale); + let safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); this.props.Document.scale = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale); e.stopPropagation(); @@ -219,6 +215,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { + this.panDisposer && clearTimeout(this.panDisposer); + this.props.Document.panTransformType = "None"; 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)); @@ -236,7 +234,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { } bringToFront = (doc: Doc) => { - const docs = this.children; + const docs = this.childDocs; docs.slice().sort((doc1, doc2) => { if (doc1 === doc) return 1; if (doc2 === doc) return -1; @@ -245,16 +243,37 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { doc.zIndex = docs.length + 1; } + panDisposer?: NodeJS.Timeout; focusDocument = (doc: Doc) => { + const panX = this.Document.panX; + const panY = this.Document.panY; + const id = this.Document[Id]; + const state = HistoryUtil.getState(); + // TODO This technically isn't correct if type !== "doc", as + // currently nothing is done, but we should probably push a new state + if (state.type === "doc" && panX !== undefined && panY !== undefined) { + const init = state.initializers[id]; + if (!init) { + state.initializers[id] = { + panX, panY + }; + HistoryUtil.pushState(state); + } else if (init.panX !== panX || init.panY !== panY) { + init.panX = panX; + init.panY = panY; + HistoryUtil.pushState(state); + } + } SelectionManager.DeselectAll(); + const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2; + const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2; + const newState = HistoryUtil.getState(); + newState.initializers[id] = { panX: newPanX, panY: newPanY }; + HistoryUtil.pushState(newState); + this.setPan(newPanX, newPanY); this.props.Document.panTransformType = "Ease"; - this.setPan( - NumCast(doc.x) + NumCast(doc.width) / 2, - NumCast(doc.y) + NumCast(doc.height) / 2); this.props.focus(this.props.Document); - if (this.props.Document.panTransformType === "Ease") { - setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false - } + this.panDisposer = setTimeout(() => this.props.Document.panTransformType = "None", 2000); // wait 3 seconds, then reset to false } @@ -276,17 +295,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { parentActive: this.props.active, whenActiveChanged: this.props.whenActiveChanged, bringToFront: this.bringToFront, + addDocTab: this.props.addDocTab, }; } @computed.struct get views() { let curPage = FieldValue(this.Document.curPage, -1); - let docviews = this.children.reduce((prev, doc) => { + let docviews = this.childDocs.reduce((prev, doc) => { if (!(doc instanceof Doc)) return prev; var page = NumCast(doc.page, -1); if (page === curPage || page === -1) { - let minim = Cast(doc.isMinimized, "boolean"); + let minim = BoolCast(doc.isMinimized, false); if (minim === undefined || !minim) { prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />); } @@ -350,7 +370,7 @@ class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />); } render() { - return this.backgroundView; + return this.props.Document.backgroundLayout ? this.backgroundView : (null); } } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 080c484f4..4587c2227 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -17,6 +17,9 @@ import { InkField, StrokeData } from "../../../../new_fields/InkField"; import { List } from "../../../../new_fields/List"; import { ImageField } from "../../../../new_fields/URLField"; import { Template, Templates } from "../../Templates"; +import { Gateway } from "../../../northstar/manager/Gateway"; +import { DocServer } from "../../../DocServer"; +import { Id } from "../../../../new_fields/RefField"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -60,7 +63,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); for (let i = 0; i < ns.length - 1; i++) { while (!(ns[i].trim() === "" || ns[i].endsWith("-\r") || ns[i].endsWith("-") || ns[i].endsWith(";\r") || ns[i].endsWith(";") || @@ -77,9 +80,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line }); this.props.addDocument(newBox, false); y += 40 * this.props.getTransform().Scale; - }) + }); })(); - } else if (e.key === "t" && e.ctrlKey) { + } else if (e.key === "b" && e.ctrlKey) { //heuristically converts pasted text into a table. // assumes each entry is separated by a tab // skips all rows until it gets to a row with more than one entry @@ -90,9 +93,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps> e.preventDefault(); (async () => { let text: string = await navigator.clipboard.readText(); - let ns = text.split("\n").filter(t => t.trim() != "\r" && t.trim() != ""); - while (ns.length > 0 && ns[0].split("\t").length < 2) + let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== ""); + while (ns.length > 0 && ns[0].split("\t").length < 2) { ns.splice(0, 1); + } if (ns.length > 0) { let columns = ns[0].split("\t"); let docList: Doc[] = []; @@ -104,18 +108,15 @@ export class MarqueeView extends React.Component<MarqueeViewProps> continue; } let doc = new Doc(); - columns.forEach((col, i) => { - console.log(values[i] + " " + Number(values[i]).toString()); - doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined); - }); + columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined)); if (groupAttr) { - doc["_group"] = groupAttr; + doc._group = groupAttr; } doc.title = i.toString(); docList.push(doc); } - let newCol = Docs.SchemaDocument(docList, { x: x, y: y, title: "-dropped table-", width: 300, height: 100 }); - newCol.proto!.schemaColumns = new List<string>([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)]); + let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 }); + this.props.addDocument(newCol, false); } })(); @@ -131,17 +132,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._downY = this._lastY = e.pageY; this._commandExecuted = false; PreviewCursor.Visible = false; - if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || - (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { + if (e.button === 2 || (e.button === 0 && e.altKey)) { + if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]); document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); - // bcz: do we need this? it kills the context menu on the main collection + if (e.altKey) { + e.stopPropagation(); + e.preventDefault(); + } + // bcz: do we need this? it kills the context menu on the main collection if !altKey // e.stopPropagation(); } - if (e.altKey) { - e.preventDefault(); - } } @action @@ -207,11 +209,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action marqueeCommand = async (e: KeyboardEvent) => { - if (this._commandExecuted) { + if (this._commandExecuted || (e as any).propagationIsStopped) { return; } if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") { this._commandExecuted = true; + e.stopPropagation(); + (e as any).propagationIsStopped = true; this.marqueeSelect().map(d => this.props.removeDocument(d)); let ink = Cast(this.props.container.props.Document.ink, InkField); if (ink) { @@ -221,26 +225,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.cleanupInteractions(false); e.stopPropagation(); } - if (e.key === "c" || e.key === "r" || e.key === "s" || e.key === "e" || e.key === "p") { + if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") { this._commandExecuted = true; e.stopPropagation(); + (e as any).propagationIsStopped = true; let bounds = this.Bounds; - let selected = this.marqueeSelect().map(d => { - if (e.key === "s") { - let dCopy = Doc.MakeCopy(d); - dCopy.x = NumCast(d.x) - bounds.left - bounds.width / 2; - dCopy.y = NumCast(d.y) - bounds.top - bounds.height / 2; - dCopy.page = -1; - return dCopy; - } - else if (e.key !== "r") { + let selected = this.marqueeSelect(); + if (e.key === "c") { + selected.map(d => { this.props.removeDocument(d); d.x = NumCast(d.x) - bounds.left - bounds.width / 2; d.y = NumCast(d.y) - bounds.top - bounds.height / 2; d.page = -1; - } - return d; - }); + return d; + }); + } let ink = Cast(this.props.container.props.Document.ink, InkField); let inkData = ink ? ink.inkData : undefined; let zoomBasis = NumCast(this.props.container.props.Document.scale, 1); @@ -250,39 +249,36 @@ export class MarqueeView extends React.Component<MarqueeViewProps> panX: 0, panY: 0, borderRounding: e.key === "e" ? -1 : undefined, + backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white", scale: zoomBasis, width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined, - title: "a nested collection", + title: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection", }); - this.marqueeInkDelete(inkData); - // SelectionManager.DeselectAll(); - if (e.key === "s" || e.key === "r" || e.key === "p") { - e.preventDefault(); - let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top); - let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - let dataUrl = await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width, height: bounds.height, quality: 1 }); - summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); + if (e.key === "s" || e.key === "p") { - summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); - if (e.key === "s" || e.key === "p") { - summary.proto!.maximizeOnRight = true; + htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).then((dataUrl) => { + selected.map(d => { + this.props.removeDocument(d); + d.x = NumCast(d.x) - bounds.left - bounds.width / 2; + d.y = NumCast(d.y) - bounds.top - bounds.height / 2; + d.page = -1; + return d; + }); + let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + summary.proto!.thumbnail = new ImageField(new URL(dataUrl)); + summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]); newCollection.proto!.summaryDoc = summary; selected = [newCollection]; - } - summary.proto!.summarizedDocs = new List<Doc>(selected); - //summary.proto!.isButton = true; - selected.map(summarizedDoc => { - let maxx = NumCast(summarizedDoc.x, undefined); - let maxy = NumCast(summarizedDoc.y, undefined); - let maxw = NumCast(summarizedDoc.width, undefined); - let maxh = NumCast(summarizedDoc.height, undefined); - summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]) + newCollection.x = bounds.left + bounds.width; + //this.props.addDocument(newCollection, false); + summary.proto!.summarizedDocs = new List<Doc>(selected); + summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight" + this.props.addLiveTextDocument(summary); }); - this.props.addLiveTextDocument(summary); } else { this.props.addDocument(newCollection, false); @@ -290,20 +286,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this.props.selectDocuments([newCollection]); } this.cleanupInteractions(false); - } else - if (e.key === "s") { - // this._commandExecuted = true; - // e.stopPropagation(); - // e.preventDefault(); - // let bounds = this.Bounds; - // let selected = this.marqueeSelect(); - // SelectionManager.DeselectAll(); - // let summary = Docs.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); - // this.props.addLiveTextDocument(summary); - // selected.forEach(select => Doc.MakeLink(summary.proto!, select.proto!)); - - // this.cleanupInteractions(false); - } + } } @action marqueeInkSelect(ink: Map<any, any>) { diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 817a23ce8..fa44ec9f3 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,25 +1,26 @@ -import { computed, trace, action, reaction, IReactionDisposer } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; +import { UndoManager } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocComponent } from "../DocComponent"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; -import { FieldValue, Cast, NumCast, BoolCast, StrCast } from "../../../new_fields/Types"; -import { OmitKeys, Utils } from "../../../Utils"; -import { SelectionManager } from "../../util/SelectionManager"; -import { Doc, DocListCastAsync, DocListCast, } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; -import { UndoManager } from "../../util/UndoManager"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } const schema = createSchema({ zoomBasis: "number", - zIndex: "number" + zIndex: "number", }); //TODO Types: The import order is wrong, so positionSchema is undefined @@ -31,6 +32,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF private _mainCont = React.createRef<HTMLDivElement>(); private _downX: number = 0; private _downY: number = 0; + private _doubleTap = false; + private _lastTap: number = 0; _bringToFrontDisposer?: IReactionDisposer; @computed get transform() { @@ -42,9 +45,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } - @computed get width(): number { return FieldValue(this.Document.width, 0); } - @computed get height(): number { return FieldValue(this.Document.height, 0); } - @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); } + @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); } + @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); } set width(w: number) { this.Document.width = w; @@ -58,10 +60,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this.Document.width = this.nativeWidth / this.nativeHeight * h; } } - set zIndex(h: number) { - this.Document.zIndex = h; - } - contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); @@ -86,7 +84,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this.props.bringToFront(this.props.Document); if (values instanceof List) { let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]); - this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false); + this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0], + this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false); } }, { fireImmediately: true }); } @@ -95,32 +94,33 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (this._bringToFrontDisposer) this._bringToFrontDisposer(); } - animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) { - if (first) { - if (maximizing) target.width = target.height = 1; - } + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, 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.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - target.x = pval[0]; - target.y = pval[1]; + this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + this.props.Document.x = pval[0]; + this.props.Document.y = pval[1]; + if (first) { + this.props.Document.proto!.willMaximize = false; + } if (now < stime + 200) { - this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing); + this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); } else { if (!maximizing) { - target.isMinimized = true; - target.x = targ[0]; - target.y = targ[1]; - target.width = width; - target.height = height; + this.props.Document.proto!.isMinimized = true; + this.props.Document.x = targ[0]; + this.props.Document.y = targ[1]; + this.props.Document.width = width; + this.props.Document.height = height; } - target.isIconAnimating = undefined; + this.props.Document.proto!.isIconAnimating = undefined; } }, 2); @@ -139,23 +139,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (!CollectionFreeFormDocumentView._undoBatch) { CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); } - maximizedDocs.forEach(maximizedDoc => { + maximizedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { + if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { isMinimized = BoolCast(maximizedDoc.isMinimized, false); } - let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2; - let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2; - let maxx = NumCast(maximizedDoc.x, undefined); - let maxy = NumCast(maximizedDoc.y, undefined); - let maxw = NumCast(maximizedDoc.width, undefined); - let maxh = NumCast(maximizedDoc.height, undefined); - if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && - maxw !== undefined && maxh !== undefined) { + let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2; + let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2; + if (minx !== undefined && miny !== undefined) { let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny); - if (isMinimized) maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0]) + maximizedDoc.willMaximize = isMinimized; + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); } } }); @@ -169,36 +165,81 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; - // e.stopPropagation(); + this._doubleTap = false; + if (e.button === 0 && e.altKey) { + e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started + } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document + if (Date.now() - this._lastTap < 300) { + if (e.buttons === 1) { + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } else { + this._lastTap = Date.now(); + } + } + onPointerUp = (e: PointerEvent): void => { + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this._doubleTap = true; + } } onClick = async (e: React.MouseEvent) => { e.stopPropagation(); + if (this._doubleTap) { + this.props.addDocTab(this.props.Document, "inTab"); + SelectionManager.DeselectAll(); + } let altKey = e.altKey; if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { let isExpander = (e.target as any).id === "isExpander"; if (BoolCast(this.props.Document.isButton, false) || isExpander) { + SelectionManager.DeselectAll(); let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : [])]; - if (expandedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents - if ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight)) { + let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []); + let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []); + let expandedDocs: Doc[] = []; + expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents + let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc)) + let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace"); + let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target); + if (altKey) { + maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"); + if (!maxLocation || maxLocation === "inPlace") { + let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); + let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false); + expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false); + let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView); + if (!hasView) { + this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false)); + } + expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized); + } + } + if (maxLocation && maxLocation !== "inPlace") { let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); if (dataDocs) { - SelectionManager.DeselectAll(); - expandedDocs.forEach(maxDoc => { - maxDoc.isMinimized = false; - if (!dataDocs || dataDocs.indexOf(maxDoc) == -1) { - CollectionDockingView.Instance.AddRightSplit(maxDoc); - } else { - CollectionDockingView.Instance.CloseRightSplit(maxDoc); - } - }); + expandedDocs.forEach(maxDoc => + (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) && + this.props.addDocTab(getDispDoc(maxDoc), maxLocation))); } } else { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(await maxDoc, false)); - this.toggleIcon(expandedDocs); + this.toggleIcon(expandedProtoDocs); + } + } + else if (linkedToDocs.length || linkedFromDocs.length) { + let linkedFwdDocs = [ + linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + if (linkedFwdDocs) { + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], altKey); } } } @@ -236,9 +277,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter} onClick={this.onClick} style={{ - outlineColor: "black", + outlineColor: "maroon", outlineStyle: "dashed", - outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px", + outlineWidth: BoolCast(this.props.Document.libraryBrush, false) || + BoolCast(this.props.Document.protoBrush, false) ? + `${1 * this.getTransform().Scale}px` : "0px", opacity: zoomFade, borderRadius: `${this.borderRounding()}px`, transformOrigin: "left top", @@ -247,7 +290,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this.width, height: this.height, position: "absolute", - zIndex: this.zIndex, + zIndex: this.Document.zIndex || 0, backgroundColor: "transparent" }} > {this.docView} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 11df9162a..8e08385a4 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -21,7 +21,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; -import { Cast, StrCast } from "../../../new_fields/Types"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { PDFBox2 } from "../pdf/PDFBox2"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -44,9 +44,20 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any; export class DocumentContentsView extends React.Component<DocumentViewProps & { isSelected: () => boolean, select: (ctrl: boolean) => void, - layoutKey: string + layoutKey: string, }> { - @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", this.props.layoutKey === "layout" ? "<p>Error loading layout data</p>" : ""); } + @computed get layout(): string { + const layout = Cast(this.props.Document[this.props.layoutKey], "string"); + if (layout === undefined) { + return this.props.Document.data ? + "<FieldView {...props} fieldKey='data' />" : + KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""); + } else if (typeof layout === "string") { + return layout; + } else { + return "<p>Loading layout</p>"; + } + } CreateBindings(): JsxBindings { return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit }; @@ -60,7 +71,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { return new List<string>(); } @computed get finalLayout() { - const baseLayout = this.layout; + const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout; let base = baseLayout; let layout = baseLayout; @@ -72,9 +83,13 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) { this.templates.forEach(template => { let self = this; + // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller + // than the width/height of the containing document function convertConstantsToNative(match: string, offset: number, x: string) { let px = Number(match.replace("px", "")); - return `${px * self.props.ScreenToLocalTransform().Scale}px`; + return `${Math.min(NumCast(self.props.Document.height, 0), + Math.min(NumCast(self.props.Document.width, 0), + px * self.props.ScreenToLocalTransform().Scale))}px`; } let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative); layout = nativizedTemplate.replace("{layout}", base); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 90f67db7c..38f3db19f 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,31 +1,44 @@ -import { action, computed, runInAction, reaction, IReactionDisposer } from "mobx"; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { Copy, ObjectField } from "../../../new_fields/ObjectField"; +import { Id } from "../../../new_fields/RefField"; +import { createSchema, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; import { emptyFunction, Utils } from "../../../Utils"; -import { Docs } from "../../documents/Documents"; +import { DocServer } from "../../DocServer"; +import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager, dropActionType } from "../../util/DragManager"; +import { SearchUtil } from "../../util/SearchUtil"; import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; -import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { undoBatch } from "../../util/UndoManager"; import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { DocComponent } from "../DocComponent"; +import { PresentationView } from "../PresentationView"; import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { Opt, Doc, WidthSym, HeightSym, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { DocServer } from "../../DocServer"; -import { Id } from "../../../new_fields/RefField"; -import { PresentationView } from "../PresentationView"; +const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? + +library.add(faTrash); +library.add(faExpandArrowsAlt); +library.add(faCompressArrowsAlt); +library.add(faLayerGroup); +library.add(faAlignCenter); +library.add(faCaretSquareRight); +library.add(faSquare); const linkSchema = createSchema({ title: "string", @@ -55,6 +68,7 @@ export interface DocumentViewProps { whenActiveChanged: (isActive: boolean) => void; toggleMinimized: () => void; bringToFront: (doc: Doc) => void; + addDocTab: (doc: Doc, where: string) => void; } const schema = createSchema({ @@ -111,12 +125,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], () => { let maxDoc = DocListCast(this.props.Document.maximizedDocs); - if (maxDoc && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { - this.props.Document.title = (maxDoc && maxDoc.length === 1 ? maxDoc[0].title + ".icon" : ""); + if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) { + this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon"; } let sumDoc = Cast(this.props.Document.summaryDoc, Doc); - if (sumDoc instanceof Doc) { - this.props.Document.title = sumDoc.title + ".expanded"; + if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) { + this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded"; } }, { fireImmediately: true }); DocumentManager.Instance.DocumentViews.push(this); @@ -173,18 +187,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; - if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { - return; - } this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; if (e.shiftKey && e.buttons === 1) { - if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander); - } else if (this.props.Document) { - CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); - } + CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); e.stopPropagation(); } else if (this.active) { + //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -196,7 +204,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu 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 (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { + if (!e.altKey && !this.topMost && e.buttons === 1) { this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); } } @@ -220,12 +228,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; doc.isButton = !BoolCast(doc.isButton, false); if (doc.isButton && !doc.nativeWidth) { - doc.nativeWidth = doc[WidthSym](); - doc.nativeHeight = doc[HeightSym](); + doc.nativeWidth = this.props.Document[WidthSym](); + doc.nativeHeight = this.props.Document[HeightSym](); + } else { + + doc.nativeWidth = doc.nativeHeight = undefined; } } fullScreenClicked = (e: React.MouseEvent): void => { - const doc = Doc.MakeDelegate(FieldValue(this.Document.proto)); + const doc = Doc.MakeCopy(this.props.Document, false); if (doc) { CollectionDockingView.Instance.OpenFullScreen(doc); } @@ -240,10 +251,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); - de.data.droppedDocuments.push(destDoc); + if (de.mods === "AltKey") { + const protoDest = destDoc.proto; + const protoSrc = sourceDoc.proto; + let src = protoSrc ? protoSrc : sourceDoc; + let dst = protoDest ? protoDest : destDoc; + dst.data = (src.data! as ObjectField)[Copy](); + dst.nativeWidth = src.nativeWidth; + dst.nativeHeight = src.nativeHeight; + } + else { + DocUtils.MakeLink(sourceDoc, destDoc); + de.data.droppedDocuments.push(destDoc); + } e.stopPropagation(); } } @@ -287,16 +307,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } e.preventDefault(); - ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked }); - ContextMenu.Instance.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton }); - 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(DocServer.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: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) }); - ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }); + const cm = ContextMenu.Instance; + cm.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" }); + cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton, icon: "expand-arrows-alt" }); + cm.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" }); + cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" }); + cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" }); + cm.addItem({ + description: "Find aliases", event: async () => { + const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document); + CollectionDockingView.Instance.AddRightSplit(Docs.SchemaDocument(["title"], aliases, {})); + }, icon: "expand-arrows-alt" + }); + cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "expand-arrows-alt" }); + cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" }); if (!this.topMost) { // DocumentViews should stop propagation of this event e.stopPropagation(); @@ -316,8 +343,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu render() { var scaling = this.props.ContentScaling(); - var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%"; - var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%"; + var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto"); + var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%"; return ( <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 8bdf34181..5c149af99 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -7,7 +7,7 @@ import { VideoBox } from "./VideoBox"; import { AudioBox } from "./AudioBox"; import { DocumentContentsView } from "./DocumentContentsView"; import { Transform } from "../../util/Transform"; -import { returnFalse, emptyFunction } from "../../../Utils"; +import { returnFalse, emptyFunction, returnOne } from "../../../Utils"; import { CollectionView } from "../collections/CollectionView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; @@ -18,6 +18,7 @@ import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField import { IconField } from "../../../new_fields/IconField"; import { RichTextField } from "../../../new_fields/RichTextField"; import { DateField } from "../../../new_fields/DateField"; +import { NumCast } from "../../../new_fields/Types"; // @@ -34,6 +35,7 @@ export interface FieldViewProps { isTopMost: boolean; selectOnLoad: boolean; addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean; + addDocTab: (document: Doc, where: string) => boolean; removeDocument?: (document: Doc) => boolean; moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean; ScreenToLocalTransform: () => Transform; @@ -82,14 +84,16 @@ export class FieldView extends React.Component<FieldViewProps> { return <p>{field.date.toLocaleString()}</p>; } else if (field instanceof Doc) { + let returnHundred = () => 100; return ( <DocumentContentsView Document={field} addDocument={undefined} + addDocTab={this.props.addDocTab} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => 100} - PanelHeight={() => 100} + ContentScaling={returnOne} + PanelWidth={returnHundred} + PanelHeight={returnHundred} isTopMost={true} //TODO Why is this top most? selectOnLoad={false} focus={emptyFunction} diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 7ebc31b0a..d15813f9a 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,3 +1,5 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; @@ -5,7 +7,7 @@ import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Doc, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc"; import { RichTextField } from "../../../new_fields/RichTextField"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; @@ -26,6 +28,10 @@ import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; import React = require("react"); +import { DocUtils } from '../../documents/Documents'; + +library.add(faEdit); +library.add(faSmile); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // @@ -111,9 +117,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe let sourceDoc = de.data.linkSourceDocument; let destDoc = this.props.Document; - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); + DocUtils.MakeLink(sourceDoc, destDoc); de.data.droppedDocuments.push(destDoc); e.stopPropagation(); } @@ -184,7 +188,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { - image(node, view, getPos) { return new ImageResizeView(node, view, getPos) } + image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } } }); let text = StrCast(this.props.Document.documentText); @@ -229,12 +233,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.target && (e.target as any).href) { let href = (e.target as any).href; if (href.indexOf(DocServer.prepend("/doc/")) === 0) { - let docid = href.replace(DocServer.prepend("/doc/"), ""); + let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; DocServer.GetRefField(docid).then(action((f: Opt<Field>) => { if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) + if (DocumentManager.Instance.getDocumentView(f)) { DocumentManager.Instance.getDocumentView(f)!.props.focus(f); - else CollectionDockingView.Instance.AddRightSplit(f); + } else { + CollectionDockingView.Instance.AddRightSplit(f); + } } })); } @@ -269,14 +275,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE - textCapability = (e: React.MouseEvent): void => { + freezeNativeDimensions = (e: React.MouseEvent): void => { if (NumCast(this.props.Document.nativeWidth)) { - this.props.Document.nativeWidth = undefined; - this.props.Document.nativeHeight = undefined; + this.props.Document.proto!.nativeWidth = undefined; + this.props.Document.proto!.nativeHeight = undefined; } else { - this.props.Document.nativeWidth = this.props.Document[WidthSym](); - this.props.Document.nativeHeight = this.props.Document[HeightSym](); + this.props.Document.proto!.nativeWidth = this.props.Document[WidthSym](); + this.props.Document.proto!.nativeHeight = this.props.Document[HeightSym](); } } specificContextMenu = (e: React.MouseEvent): void => { @@ -286,7 +292,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } ContextMenu.Instance.addItem({ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", - event: this.textCapability + event: this.freezeNativeDimensions, + icon: "edit" }); } diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss index 85bbdeb59..893dc2d36 100644 --- a/src/client/views/nodes/IconBox.scss +++ b/src/client/views/nodes/IconBox.scss @@ -1,20 +1,20 @@ @import "../globalCssVariables"; .iconBox-container { - position: absolute; + position: inherit; left:0; top:0; - height: 100%; + height: auto; width: max-content; // overflow: hidden; pointer-events: all; svg { width: $MINIMIZED_ICON_SIZE !important; - height: 100%; + height: auto; background: white; } .iconBox-label { - position: inherit; + position: absolute; width:max-content; font-size: 14px; margin-top: 3px; diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index 071930633..b42eb44a5 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; import { FieldView, FieldViewProps } from './FieldView'; import "./IconBox.scss"; import { Cast, StrCast, BoolCast } from "../../../new_fields/Types"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast } from "../../../new_fields/Doc"; import { IconField } from "../../../new_fields/IconField"; import { ContextMenu } from "../ContextMenu"; import Measure from "react-measure"; @@ -41,25 +41,40 @@ export class IconBox extends React.Component<FieldViewProps> { setLabelField = (e: React.MouseEvent): void => { this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel); } + setUseOwnTitleField = (e: React.MouseEvent): void => { + this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle); + } specificContextMenu = (e: React.MouseEvent): void => { ContextMenu.Instance.addItem({ - description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label", + description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon", event: this.setLabelField }); + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) { + ContextMenu.Instance.addItem({ + description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label", + event: this.setUseOwnTitleField + }); + } } @observable _panelWidth: number = 0; @observable _panelHeight: number = 0; render() { let labelField = StrCast(this.props.Document.labelField); let hideLabel = BoolCast(this.props.Document.hideLabel); - let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []); - let firstDoc = maxDoc && maxDoc.length > 0 && maxDoc[0] instanceof Doc ? maxDoc[0] as Doc : undefined; - let label = !hideLabel && firstDoc && labelField ? firstDoc[labelField] : ""; + let maxDocs = DocListCast(this.props.Document.maximizedDocs); + let firstDoc = maxDocs.length ? maxDocs[0] : undefined; + let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title); return ( <div className="iconBox-container" onContextMenu={this.specificContextMenu}> {this.minimizedIcon} - <Measure offset onResize={(r) => runInAction(() => { if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); })}> + <Measure offset onResize={(r) => runInAction(() => { + if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) { + this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE)); + if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth; + } + })}> {({ measureRef }) => <span ref={measureRef} className="iconBox-label">{label}</span> } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 2316a050e..f1b73a676 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -25,7 +25,11 @@ } .imageBox-cont img { - height: 100%; + height: auto; + width:100%; +} +.imageBox-cont-interactive img { + height: auto; width:100%; } diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index f9659a4b2..828ac9bc8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,5 +1,4 @@ - -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 @@ -42,8 +41,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; var w = this._imgRef.current!.naturalWidth; - if (this._photoIndex === 0) { - this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w; + if (this._photoIndex === 0 && (this.props as any).id !== "isExpander") { + Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w); this.Document.height = FieldValue(this.Document.width, 0) * h / w; } } @@ -134,7 +133,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD ContextMenu.Instance.addItem({ description: "Copy path", event: () => { Utils.CopyText(url); - } + }, icon: "expand-arrows-alt" }); } } @@ -157,16 +156,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD } render() { - trace(); let field = this.Document[this.props.fieldKey]; let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; if (field instanceof ImageField) paths = [field.url.href]; else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href); let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50); let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; - let id = this.props.id; + let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx return ( - <div id={id} className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <div id={id} className={`imageBox-cont${interactive}`} + // onPointerDown={this.onPointerDown} + onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <img id={id} src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 876a3c173..86437a6c1 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -18,7 +18,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @observable private _keyInput: string = ""; @observable private _valueInput: string = ""; @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); } - + get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; } constructor(props: FieldViewProps) { super(props); @@ -28,7 +28,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { if (this._keyInput && this._valueInput) { - let doc = FieldValue(Cast(this.props.Document.data, Doc)); + let doc = this.fieldDocToLayout; if (!doc) { return; } @@ -60,7 +60,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } createTable = () => { - let doc = FieldValue(Cast(this.props.Document.data, Doc)); + let doc = this.fieldDocToLayout; if (!doc) { return <tr><td>Loading...</td></tr>; } diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index ff6885965..a1c5d5537 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -26,4 +26,12 @@ .keyValuePair-td-value { display:inline-block; overflow: scroll; + img { + max-height: 36px; + width: auto; + } + .videoBox-cont{ + width: auto; + max-height: 36px; + } }
\ No newline at end of file diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 5de660d57..4f7919f50 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -46,8 +46,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button className="keyValuePair-td-key-delete" onClick={() => { - if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) + if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { props.Document[props.fieldKey] = undefined; + } else props.Document.proto![props.fieldKey] = undefined; }}> X diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 611cb66b6..68b692aad 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -31,7 +31,7 @@ export class LinkBox extends React.Component<Props> { @undoBatch onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - DocumentManager.Instance.jumpToDocument(this.props.pairedDoc); + DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey); } onEditButtonPressed = (e: React.PointerEvent): void => { diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 7bf13d5f9..4cf798249 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -6,8 +6,7 @@ import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; +import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; import { Id } from "../../../new_fields/RefField"; interface Props { @@ -24,7 +23,7 @@ export class LinkMenu extends React.Component<Props> { return links.map(link => { let doc = FieldValue(Cast(link[key], Doc)); if (doc) { - return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; + return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; } }); } @@ -32,11 +31,11 @@ export class LinkMenu extends React.Component<Props> { render() { //get list of links from document let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs); - let linkTo = DocListCast(this.props.docView.props.Document.linkedToDoc); + let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs); if (this._editingLink === undefined) { return ( <div id="linkMenu-container"> - <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> + {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */} <div id="linkMenu-list"> {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")} {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index bf3f299bc..9b0207d0c 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -1,5 +1,5 @@ import * as htmlToImage from "html-to-image"; -import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace, runInAction } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; @@ -54,10 +54,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen public static LayoutString() { return FieldView.LayoutString(PDFBox); } private _mainDiv = React.createRef<HTMLDivElement>(); + private renderHeight = 2400; @observable private _renderAsSvg = true; + @observable private _alt = false; - private _reactionDisposer: Opt<IReactionDisposer>; + private _reactionDisposer?: IReactionDisposer; @observable private _perPageInfo: Object[] = []; //stores pageInfo @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno @@ -66,26 +68,25 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @observable private _interactive: boolean = false; @observable private _loaded: boolean = false; - @computed private get curPage() { return FieldValue(this.Document.curPage, 1); } - @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); } + @computed private get curPage() { return NumCast(this.Document.curPage, 1); } + @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); } componentDidMount() { - // this._reactionDisposer = reaction( - // () => [SelectionManager.SelectedDocuments().slice()], - // () => { - // if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage !== this.thumbnailPage && !this.props.isSelected()) { - // this.saveThumbnail(); - // this._interactive = true; - // } - // }, - // { fireImmediately: true }); + let wasSelected = false; + this._reactionDisposer = reaction( + () => this.props.isSelected(), + () => { + if (this.curPage > 0 && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) { + this.saveThumbnail(); + } + wasSelected = this._interactive = this.props.isSelected(); + }, + { fireImmediately: true }); } componentWillUnmount() { - if (this._reactionDisposer) { - this._reactionDisposer(); - } + if (this._reactionDisposer) this._reactionDisposer(); } /** @@ -163,10 +164,8 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen let index: any; this._pageInfo.divs.forEach((obj: any) => { obj.spans.forEach((element: any) => { - if (element === span) { - if (!index) { - index = this._pageInfo.divs.indexOf(obj); - } + if (element === span && !index) { + index = this._pageInfo.divs.indexOf(obj); } }); }); @@ -224,7 +223,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen document.addEventListener("pointerup", this.onPointerUp); } if (this.props.isSelected() && e.buttons === 2) { - this._alt = true; + runInAction(() => this._alt = true); document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); } @@ -244,7 +243,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } - @action saveThumbnail = () => { this._renderAsSvg = false; @@ -260,7 +258,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen .catch(function (error: any) { console.error('oops, something went wrong!', error); }); - }, 250); + }, 1250); } @action @@ -285,29 +283,28 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen this.props.Document.nativeHeight = nativeHeight; } } - renderHeight = 2400; @computed get pdfPage() { - return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />; + return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />; } @computed get pdfContent() { - let page = this.curPage; - const renderHeight = 2400; + trace(); let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); if (!pdfUrl) { return <p>No pdf url to render</p>; } - let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight; - let body = NumCast(this.props.Document.nativeHeight) ? - this.pdfPage : + let pdfpage = this.pdfPage; + let body = this.Document.nativeHeight ? + pdfpage : <Measure offset onResize={this.setScaling}> {({ measureRef }) => <div className="pdfBox-page" ref={measureRef}> - {this.pdfPage} + {pdfpage} </div> } </Measure>; + let xf = (this.Document.nativeHeight || 0) / this.renderHeight; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}> {body} @@ -340,19 +337,8 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } return (null); } - @observable _alt = false; - @action - onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Alt") { - this._alt = true; - } - } - @action - onKeyUp = (e: React.KeyboardEvent) => { - if (e.key === "Alt") { - this._alt = false; - } - } + @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true); + @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false); render() { trace(); const pdfUrl = window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"; diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 76bbeb37c..35db64cf4 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -1,4 +1,8 @@ -.videobox-cont{ +.videoBox-cont, .videoBox-cont-fullScreen{ width: 100%; height: Auto; +} + +.videoBox-cont-fullScreen { + pointer-events: all; }
\ No newline at end of file diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 97c5d8818..81c429a02 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,27 +1,31 @@ import React = require("react"); import { observer } from "mobx-react"; import { FieldView, FieldViewProps } from './FieldView'; +import * as rp from "request-promise"; import "./VideoBox.scss"; -import { action, computed, trace } from "mobx"; +import { action, IReactionDisposer, reaction, observable } from "mobx"; import { DocComponent } from "../DocComponent"; import { positionSchema } from "./DocumentView"; import { makeInterface } from "../../../new_fields/Schema"; import { pageSchema } from "./ImageBox"; -import { Cast, FieldValue, NumCast, ToConstructor, ListSpec } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; import Measure from "react-measure"; import "./VideoBox.scss"; -import { Field, FieldResult, Opt } from "../../../new_fields/Doc"; +import { RouteStore } from "../../../server/RouteStore"; +import { DocServer } from "../../DocServer"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @observer export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) { - + private _reactionDisposer?: IReactionDisposer; private _videoRef: HTMLVideoElement | null = null; private _loaded: boolean = false; - private get initialTimecode() { return FieldValue(this.Document.curPage, -1); } + @observable _playTimer?: NodeJS.Timeout = undefined; + @observable _fullScreen = false; + @observable public Playing: boolean = false; public static LayoutString() { return FieldView.LayoutString(VideoBox); } public get player(): HTMLVideoElement | undefined { @@ -45,29 +49,106 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } } + @action public Play() { + this.Playing = true; + if (this.player) this.player.play(); + if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); + } + + @action public Pause() { + this.Playing = false; + if (this.player) this.player.pause(); + if (this._playTimer) { + clearInterval(this._playTimer); + this._playTimer = undefined; + } + } + + @action public FullScreen() { + this._fullScreen = true; + this.player && this.player.requestFullscreen(); + } + + @action + updateTimecode = () => this.player && (this.props.Document.curPage = this.player.currentTime) + componentDidMount() { if (this.props.setVideoBox) this.props.setVideoBox(this); } + componentWillUnmount() { + this.Pause(); + if (this._reactionDisposer) this._reactionDisposer(); + } @action setVideoRef = (vref: HTMLVideoElement | null) => { this._videoRef = vref; - if (this.initialTimecode >= 0 && vref) { - vref.currentTime = this.initialTimecode; + if (vref) { + vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); + if (this._reactionDisposer) this._reactionDisposer(); + this._reactionDisposer = reaction(() => this.props.Document.curPage, () => + vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true }); } } videoContent(path: string) { - return <video className="videobox-cont" ref={this.setVideoRef}> + let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : ""); + return <video className={`${style}`} ref={this.setVideoRef} onPointerDown={this.onPointerDown}> <source src={path} type="video/mp4" /> Not supported. </video>; } + getMp4ForVideo(videoId: string = "JN5beCVArMs") { + return new Promise(async (resolve, reject) => { + const videoInfoRequestConfig = { + headers: { + connection: 'keep-alive', + "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0', + }, + + }; + try { + let responseSchema: any = {}; + const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig); + const dataHtml = videoInfoResponse; + const start = dataHtml.indexOf('ytplayer.config = ') + 18; + const end = dataHtml.indexOf(';ytplayer.load'); + const subString = dataHtml.substring(start, end); + const subJson = JSON.parse(subString); + const stringSub = subJson.args.player_response; + const stringSubJson = JSON.parse(stringSub); + const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats; + const videoDetails = stringSubJson.videoDetails; + responseSchema.adaptiveFormats = adaptiveFormats; + responseSchema.videoDetails = videoDetails; + resolve(responseSchema); + } + catch (err) { + console.log(` + --- Youtube --- + Function: getMp4ForVideo + Error: `, err); + reject(err); + } + }); + } + onPointerDown = (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + } + render() { let field = Cast(this.Document[this.props.fieldKey], VideoField); if (!field) { return <div>Loading</div>; } + + // this.getMp4ForVideo().then((mp4) => { + // console.log(mp4); + // }).catch(e => { + // console.log("") + // }); + // // let content = this.videoContent(field.url.href); return NumCast(this.props.Document.nativeHeight) ? content : diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index 4cac09dee..720e1640a 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -3,184 +3,142 @@ import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { observer } from 'mobx-react'; - -// configure({ -// enforceActions: "observed" -// }); - -// @observer -// class FieldViewer extends React.Component<{ field: BasicField<any> }> { -// render() { -// return <span>{JSON.stringify(this.props.field.Data)} ({this.props.field.Id})</span>; -// } -// } - -// @observer -// class KeyViewer extends React.Component<{ field: Key }> { -// render() { -// return this.props.field.Name; -// } -// } - -// @observer -// class ListViewer extends React.Component<{ field: ListField<Field> }>{ -// @observable -// expanded = false; - -// render() { -// let content; -// if (this.expanded) { -// content = ( -// <div> -// {this.props.field.Data.map(field => <DebugViewer fieldId={field.Id} key={field.Id} />)} -// </div> -// ); -// } else { -// content = <>[...] ({this.props.field.Id})</>; -// } -// return ( -// <div> -// <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button> -// {content} -// </div > -// ); -// } -// } - -// @observer -// class DocumentViewer extends React.Component<{ field: Document }> { -// private keyMap: ObservableMap<string, Key> = new ObservableMap; - -// private disposer?: Lambda; - -// componentDidMount() { -// let f = () => { -// Array.from(this.props.field._proxies.keys()).forEach(id => { -// if (!this.keyMap.has(id)) { -// Server.GetField(id, (field) => { -// if (field && field instanceof Key) { -// this.keyMap.set(id, field); -// } -// }); -// } -// }); -// }; -// this.disposer = this.props.field._proxies.observe(f); -// f(); -// } - -// componentWillUnmount() { -// if (this.disposer) { -// this.disposer(); -// } -// } - -// render() { -// let fields = Array.from(this.props.field._proxies.entries()).map(kv => { -// let key = this.keyMap.get(kv[0]); -// return ( -// <div key={kv[0]}> -// <b>({key ? key.Name : kv[0]}): </b> -// <DebugViewer fieldId={kv[1]}></DebugViewer> -// </div> -// ); -// }); -// return ( -// <div> -// Document ({this.props.field.Id}) -// <div style={{ paddingLeft: "25px" }}> -// {fields} -// </div> -// </div> -// ); -// } -// } - -// @observer -// class DebugViewer extends React.Component<{ fieldId: string }> { -// @observable -// private field?: Field; - -// @observable -// private error?: string; - -// constructor(props: { fieldId: string }) { -// super(props); -// this.update(); -// } - -// update() { -// Server.GetField(this.props.fieldId, action((field: Opt<Field>) => { -// this.field = field; -// if (!field) { -// this.error = `Field with id ${this.props.fieldId} not found`; -// } -// })); - -// } - -// render() { -// let content; -// if (this.field) { -// // content = this.field.ToJson(); -// if (this.field instanceof ListField) { -// content = (<ListViewer field={this.field} />); -// } else if (this.field instanceof Document) { -// content = (<DocumentViewer field={this.field} />); -// } else if (this.field instanceof BasicField) { -// content = (<FieldViewer field={this.field} />); -// } else if (this.field instanceof Key) { -// content = (<KeyViewer field={this.field} />); -// } else { -// content = (<span>Unrecognized field type</span>); -// } -// } else if (this.error) { -// content = <span>Field <b>{this.props.fieldId}</b> not found <button onClick={() => this.update()}>Refresh</button></span>; -// } else { -// content = <span>Field loading: {this.props.fieldId}</span>; -// } -// return content; -// } -// } - -// @observer -// class Viewer extends React.Component { -// @observable -// private idToAdd: string = ''; - -// @observable -// private ids: string[] = []; - -// @action -// inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { -// this.idToAdd = e.target.value; -// } - -// @action -// onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => { -// if (e.key === "Enter") { -// this.ids.push(this.idToAdd); -// this.idToAdd = ""; -// } -// } - -// render() { -// return ( -// <> -// <input value={this.idToAdd} -// onChange={this.inputOnChange} -// onKeyDown={this.onKeyPress} /> -// <div> -// {this.ids.map(id => <DebugViewer fieldId={id} key={id}></DebugViewer>)} -// </div> -// </> -// ); -// } -// } - -// ReactDOM.render(( -// <div style={{ position: "absolute", width: "100%", height: "100%" }}> -// <Viewer /> -// </div>), -// document.getElementById('root') -// );
\ No newline at end of file +import { Doc, Field, FieldResult } from '../new_fields/Doc'; +import { DocServer } from '../client/DocServer'; +import { Id } from '../new_fields/RefField'; +import { List } from '../new_fields/List'; +import { URLField } from '../new_fields/URLField'; + +configure({ + enforceActions: "observed" +}); + +@observer +class ListViewer extends React.Component<{ field: List<Field> }>{ + @observable + expanded = false; + + render() { + let content; + if (this.expanded) { + content = ( + <div> + {this.props.field.map((field, index) => <DebugViewer field={field} key={index} />)} + </div> + ); + } else { + content = <>[...]</>; + } + return ( + <div> + <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button> + {content} + </div > + ); + } +} + +@observer +class DocumentViewer extends React.Component<{ field: Doc }> { + @observable + expanded = false; + render() { + let content; + if (this.expanded) { + const keys = Object.keys(this.props.field); + let fields = keys.map(key => { + return ( + <div key={key}> + <b>({key}): </b> + <DebugViewer field={this.props.field[key]}></DebugViewer> + </div> + ); + }); + content = ( + <div> + Document ({this.props.field[Id]}) + <div style={{ paddingLeft: "25px" }}> + {fields} + </div> + </div> + ); + } else { + content = <>[...] ({this.props.field[Id]})</>; + } + return ( + <div> + <button onClick={action(() => this.expanded = !this.expanded)}>Toggle</button> + {content} + </div > + ); + } +} + +@observer +class DebugViewer extends React.Component<{ field: FieldResult }> { + + render() { + let content; + const field = this.props.field; + if (field instanceof List) { + content = (<ListViewer field={field} />); + } else if (field instanceof Doc) { + content = (<DocumentViewer field={field} />); + } else if (typeof field === "string") { + content = <p>"{field}"</p>; + } else if (typeof field === "number" || typeof field === "boolean") { + content = <p>{field}</p>; + } else if (field instanceof URLField) { + content = <p>{field.url.href}</p>; + } else { + content = <p>Unrecognized field type</p>; + } + return content; + } +} + +@observer +class Viewer extends React.Component { + @observable + private idToAdd: string = ''; + + @observable + private fields: Field[] = []; + + @action + inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => { + this.idToAdd = e.target.value; + } + + @action + onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => { + if (e.key === "Enter") { + DocServer.GetRefField(this.idToAdd).then(action((field: any) => { + if (field !== undefined) { + this.fields.push(field); + } + })); + this.idToAdd = ""; + } + } + + render() { + return ( + <> + <input value={this.idToAdd} + onChange={this.inputOnChange} + onKeyDown={this.onKeyPress} /> + <div> + {this.fields.map((field, index) => <DebugViewer field={field} key={index}></DebugViewer>)} + </div> + </> + ); + } +} + +ReactDOM.render(( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <Viewer /> + </div>), + document.getElementById('root') +);
\ No newline at end of file diff --git a/src/new_fields/CursorField.ts b/src/new_fields/CursorField.ts index 7fd326a5f..fc144222c 100644 --- a/src/new_fields/CursorField.ts +++ b/src/new_fields/CursorField.ts @@ -6,17 +6,17 @@ import { serializable, createSimpleSchema, object } from "serializr"; export type CursorPosition = { x: number, y: number -} +}; export type CursorMetadata = { id: string, identifier: string -} +}; export type CursorData = { metadata: CursorMetadata, position: CursorPosition -} +}; const PositionSchema = createSimpleSchema({ x: true, diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index 46ccb3e90..02dd34cb4 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -4,12 +4,9 @@ import { autoObject, SerializationHelper, Deserializable } from "../client/util/ import { DocServer } from "../client/DocServer"; import { setter, getter, getField, updateFunction, deleteProperty } from "./util"; import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types"; -import { UndoManager, undoBatch } from "../client/util/UndoManager"; import { listSpec } from "./Schema"; -import { List } from "./List"; import { ObjectField, Parent, OnUpdate } from "./ObjectField"; import { RefField, FieldId, Id, HandleUpdate } from "./RefField"; -import { Docs } from "../client/documents/Documents"; export function IsField(field: any): field is Field { return (typeof field === "string") @@ -29,15 +26,21 @@ export const SelfProxy = Symbol("SelfProxy"); export const WidthSym = Symbol("Width"); export const HeightSym = Symbol("Height"); +/** + * Cast any field to either a List of Docs or undefined if the given field isn't a List of Docs. + * If a default value is given, that will be returned instead of undefined. + * If a default value is given, the returned value should not be modified as it might be a temporary value. + * If no default value is given, and the returned value is not undefined, it can be safely modified. + */ export function DocListCastAsync(field: FieldResult): Promise<Doc[] | undefined>; export function DocListCastAsync(field: FieldResult, defaultValue: Doc[]): Promise<Doc[]>; export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) { const list = Cast(field, listSpec(Doc)); - return list ? Promise.all(list) : Promise.resolve(defaultValue); + return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue); } -export function DocListCast(field: FieldResult) { - return Cast(field, listSpec(Doc), []).filter(d => d && d instanceof Doc).map(d => d as Doc) +export function DocListCast(field: FieldResult): Doc[] { + return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; } @Deserializable("doc").withFields(["id"]) @@ -130,11 +133,12 @@ export namespace Doc { const self = doc[Self]; return getField(self, key, ignoreProto); } - export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): T | null | undefined { - return Cast(Get(doc, key, ignoreProto), ctor) as T | null | undefined; + export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> { + return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>; } export async function SetOnPrototype(doc: Doc, key: string, value: Field) { - const proto = doc.proto; + const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc; + if (proto) { proto[key] = value; } @@ -152,22 +156,43 @@ export namespace Doc { for (const key in fields) { if (fields.hasOwnProperty(key)) { const value = fields[key]; - if (value !== undefined) { - doc[key] = value; - } + // Do we want to filter out undefineds? + // if (value !== undefined) { + doc[key] = value; + // } } } return doc; } + // compare whether documents or their protos match + export function AreProtosEqual(doc: Doc, other: Doc) { + let r = (doc[Id] === other[Id]); + let r2 = (doc.proto && doc.proto.Id === other[Id]); + let r3 = (other.proto && other.proto.Id === doc[Id]); + let r4 = (doc.proto && other.proto && doc.proto[Id] === other.proto[Id]); + return r || r2 || r3 || r4 ? true : false; + } + + // gets the document's prototype or returns the document if it is a prototype + export function GetProto(doc: Doc) { + return Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto! : doc; + } + + export function MakeAlias(doc: Doc) { + const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : undefined; const alias = new Doc; - PromiseValue(Cast(doc.proto, Doc)).then(proto => { - if (proto) { - alias.proto = proto; - } - }); + if (!proto) { + alias.proto = doc; + } else { + PromiseValue(Cast(doc.proto, Doc)).then(proto => { + if (proto) { + alias.proto = proto; + } + }); + } return alias; } @@ -193,39 +218,13 @@ export namespace Doc { return copy; } - export function MakeLink(source: Doc, target: Doc) { - UndoManager.RunInBatch(() => { - let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 }); - //let linkDoc = new Doc; - linkDoc.proto!.title = "-link name-"; - linkDoc.linkDescription = ""; - linkDoc.linkTags = "Default"; - - linkDoc.linkedTo = target; - linkDoc.linkedFrom = source; - - let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc)); - if (!linkedFrom) { - target.linkedFromDocs = linkedFrom = new List<Doc>(); - } - linkedFrom.push(linkDoc); - - let linkedTo = Cast(source.linkedToDocs, listSpec(Doc)); - if (!linkedTo) { - source.linkedToDocs = linkedTo = new List<Doc>(); - } - linkedTo.push(linkDoc); - return linkDoc; - }, "make link"); - } - - export function MakeDelegate(doc: Doc): Doc; - export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>; - export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> { + export function MakeDelegate(doc: Doc, id?: string): Doc; + export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc>; + export function MakeDelegate(doc: Opt<Doc>, id?: string): Opt<Doc> { if (!doc) { return undefined; } - const delegate = new Doc(); + const delegate = new Doc(id, true); delegate.proto = doc; return delegate; } diff --git a/src/new_fields/List.ts b/src/new_fields/List.ts index 88a65eba4..70e36f911 100644 --- a/src/new_fields/List.ts +++ b/src/new_fields/List.ts @@ -230,6 +230,16 @@ class ListImpl<T extends Field> extends ObjectField { const list = new Proxy<this>(this, { set: setter, get: listGetter, + ownKeys: target => Object.keys(target.__fields), + getOwnPropertyDescriptor: (target, prop) => { + if (prop in target.__fields) { + return { + configurable: true,//TODO Should configurable be true? + enumerable: true, + }; + } + return Reflect.getOwnPropertyDescriptor(target, prop); + }, deleteProperty: deleteProperty, defineProperty: () => { throw new Error("Currently properties can't be defined on documents using Object.defineProperty"); }, }); diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts index f276bfa67..51768c6db 100644 --- a/src/new_fields/ObjectField.ts +++ b/src/new_fields/ObjectField.ts @@ -6,7 +6,7 @@ export const Parent = Symbol("Parent"); export const Copy = Symbol("Copy"); export abstract class ObjectField { - protected [OnUpdate](diff?: any) { }; + protected [OnUpdate](diff?: any) { } private [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; } diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts index a5f5e368b..d94994a07 100644 --- a/src/new_fields/util.ts +++ b/src/new_fields/util.ts @@ -55,6 +55,17 @@ export function getter(target: any, prop: string | symbol | number, receiver: an } return getField(target, prop); } +function getProtoField(protoField: Doc | undefined, prop: string | number, cb?: (field: Field | undefined) => void) { + if (!protoField) return undefined; + let field = protoField[prop]; + if (field instanceof Promise) { + cb && field.then(cb); + return field; + } else { + cb && cb(field); + return field; + } +} //TODO The callback parameter is never being passed in currently, so we should be able to get rid of it. export function getField(target: any, prop: string | number, ignoreProto: boolean = false, callback?: (field: Field | undefined) => void): any { @@ -62,17 +73,12 @@ export function getField(target: any, prop: string | number, ignoreProto: boolea if (field instanceof ProxyField) { return field.value(callback); } - if (field === undefined && !ignoreProto) { + if (field === undefined && !ignoreProto && prop !== "proto") { const proto = getField(target, "proto", true); if (proto instanceof Doc) { - let field = proto[prop]; - if (field instanceof Promise) { - callback && field.then(callback); - return undefined; - } else { - callback && callback(field); - return field; - } + return getProtoField(proto, prop, callback); + } else if (proto instanceof Promise) { + return proto.then(async proto => getProtoField(proto, prop, callback)); } } callback && callback(field); diff --git a/src/server/Search.ts b/src/server/Search.ts index 0f7004bdf..5ca5578a7 100644 --- a/src/server/Search.ts +++ b/src/server/Search.ts @@ -22,7 +22,8 @@ export class Search { try { const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { qs: { - q: query + q: query, + fl: "id" } })); const fields = searchResults.response.docs; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index 5f45d7bcc..aef2d3f4a 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,15 +1,21 @@ import { computed, observable, action, runInAction } from "mobx"; import * as rp from 'request-promise'; import { Docs } from "../../../client/documents/Documents"; -import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea"; +import { Attribute, AttributeGroup, Catalog, Schema, AggregateFunction } from "../../../client/northstar/model/idea/idea"; import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; import { RouteStore } from "../../RouteStore"; import { DocServer } from "../../../client/DocServer"; -import { Doc } from "../../../new_fields/Doc"; +import { Doc, Opt, Field } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; import { CollectionTreeView } from "../../../client/views/collections/CollectionTreeView"; import { CollectionView } from "../../../client/views/collections/CollectionView"; +import { NorthstarSettings, Gateway } from "../../../client/northstar/manager/Gateway"; +import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel"; +import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel"; +import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation"; +import { Cast, PromiseValue } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; export class CurrentUserUtils { private static curr_email: string; @@ -31,13 +37,13 @@ export class CurrentUserUtils { doc.title = this.email; doc.data = new List<Doc>(); doc.excludeFromLibrary = true; - doc.optionalRightCollection = Docs.SchemaDocument([], { title: "Pending documents" }); + doc.optionalRightCollection = Docs.SchemaDocument(["title"], [], { title: "Pending documents" }); // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); // (doc.library as Doc).excludeFromLibrary = true; return doc; } - public static loadCurrentUser(): Promise<any> { + public static async loadCurrentUser(): Promise<any> { let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => { if (response) { let obj = JSON.parse(response); @@ -47,7 +53,7 @@ export class CurrentUserUtils { throw new Error("There should be a user! Why does Dash think there isn't one?"); } }); - let userDocPromise = rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { + let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { if (id) { return DocServer.GetRefField(id).then(field => runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id))); @@ -55,14 +61,63 @@ export class CurrentUserUtils { throw new Error("There should be a user id! Why does Dash think there isn't one?"); } }); + try { + const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); + NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); + await Gateway.Instance.ClearCatalog(); + const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); + let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc))); + let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras); + if (catprom) await Promise.all(catprom); + } catch (e) { + + } return Promise.all([userPromise, userDocPromise]); } /* Northstar catalog ... really just for testing so this should eventually go away */ + // --------------- Northstar hooks ------------- / + static _northstarSchemas: Doc[] = []; @observable private static _northstarCatalog?: Catalog; @computed public static get NorthstarDBCatalog() { return this._northstarCatalog; } + + @action static SetNorthstarCatalog(ctlog: Catalog, extras: Catalog[]) { + CurrentUserUtils.NorthstarDBCatalog = ctlog; + if (ctlog && ctlog.schemas) { + extras.map(ex => ctlog.schemas!.push(ex)); + return ctlog.schemas.map(async schema => { + let schemaDocuments: Doc[] = []; + let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema); + await Promise.all(attributesToBecomeDocs.reduce((promises, attr) => { + promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { + if (field instanceof Doc) { + 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(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! })); + } + }))); + return promises; + }, [] as Promise<void>[])); + return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })); + }); + } + } public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { this._northstarCatalog = ctlog; } + public static AddNorthstarSchema(schema: Schema, schemaDoc: Doc) { + if (this._northstarCatalog && CurrentUserUtils._northstarSchemas) { + this._northstarCatalog.schemas!.push(schema); + CurrentUserUtils._northstarSchemas.push(schemaDoc); + let schemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); + schemas.push(schema.displayName!); + CurrentUserUtils.UserDocument.DBSchemas = new List<string>(schemas); + } + } public static GetNorthstarSchema(name: string): Schema | undefined { return !this._northstarCatalog || !this._northstarCatalog.schemas ? undefined : ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas, (s: Schema) => s.displayName === name); diff --git a/src/server/index.ts b/src/server/index.ts index da6bc0165..d19c65e0a 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -362,8 +362,8 @@ function UpdateField(socket: Socket, diff: Diff) { Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null }); let term = ToSearchTerm(val); if (term !== undefined) { - let { suffix, value } = term; - update[key + suffix] = { set: value }; + // let { suffix, value } = term; + // update[key + suffix] = { set: value }; } } if (dynfield) { |