aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorEleanor Eng <eleanor_eng@brown.edu>2019-06-04 10:29:02 -0400
committerEleanor Eng <eleanor_eng@brown.edu>2019-06-04 10:29:02 -0400
commit376ebd44a16dfa04aacd3582e87767aed1a01f36 (patch)
tree3a9e623cf6689e1ea6975954596bf5bda6303249 /src/client/views/collections
parent8f14e688220096ccecfd1aa0dd54b00e48f92270 (diff)
parent6f49d067b58caf6297f7ae7687cf05b627c27a1d (diff)
merge with master
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx43
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx221
-rw-r--r--src/client/views/collections/CollectionPDFView.scss39
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx4
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss90
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx327
-rw-r--r--src/client/views/collections/CollectionStackingView.scss51
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx179
-rw-r--r--src/client/views/collections/CollectionSubView.tsx38
-rw-r--r--src/client/views/collections/CollectionTreeView.scss65
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx142
-rw-r--r--src/client/views/collections/CollectionVideoView.scss4
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx98
-rw-r--r--src/client/views/collections/CollectionView.tsx45
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss22
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx94
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss8
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx124
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx67
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss16
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx173
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx235
24 files changed, 1519 insertions, 584 deletions
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 76adfcdcd..734669893 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -1,14 +1,14 @@
-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';
import { FieldViewProps } from '../nodes/FieldView';
import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types';
-import { Doc, FieldResult, Opt } from '../../../new_fields/Doc';
+import { Doc, FieldResult, Opt, DocListCast } from '../../../new_fields/Doc';
import { listSpec } from '../../../new_fields/Schema';
import { List } from '../../../new_fields/List';
-import { Id } from '../../../new_fields/RefField';
import { SelectionManager } from '../../util/SelectionManager';
+import { Id } from '../../../new_fields/FieldSymbols';
export enum CollectionViewType {
Invalid,
@@ -16,6 +16,7 @@ export enum CollectionViewType {
Schema,
Docking,
Tree,
+ Stacking
}
export interface CollectionRenderProps {
@@ -36,9 +37,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 {
@@ -63,13 +75,13 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!(documentToAdd instanceof Doc)) {
return false;
}
- let data = Cast(documentToAdd.data, listSpec(Doc), []);
- for (const doc of data.filter(d => d instanceof Document)) {
+ let data = DocListCast(documentToAdd.data);
+ for (const doc of data) {
if (this.createsCycle(doc, containerDocument)) {
return true;
}
}
- let annots = Cast(documentToAdd.annotations, listSpec(Doc), []);
+ let annots = DocListCast(documentToAdd.annotations);
for (const annot of annots) {
if (this.createsCycle(annot, containerDocument)) {
return true;
@@ -82,12 +94,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);
@@ -95,17 +107,20 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
if (!this.createsCycle(doc, props.Document)) {
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
+ let alreadyAdded = true;
if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v[Id] === doc[Id])) {
+ if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
value.push(doc);
}
} else {
- this.props.Document[this.props.fieldKey] = new List([doc]);
+ alreadyAdded = false;
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
}
// set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
- if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) {
+ if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
let zoom = NumCast(this.props.Document.scale, 1);
- doc.zoomBasis = zoom;
+ // Doc.GetProto(doc).zoomBasis = zoom;
}
}
return true;
@@ -113,12 +128,14 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
@action.bound
removeDocument(doc: Doc): boolean {
+ SelectionManager.DeselectAll();
const props = this.props;
//TODO This won't create the field if it doesn't already exist
const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
let index = -1;
for (let i = 0; i < value.length; i++) {
- if (value[i][Id] === doc[Id]) {
+ let v = value[i];
+ if (v instanceof Doc && v[Id] === doc[Id]) {
index = i;
break;
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index cfb1aef7d..dcc1bd95d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,25 +1,28 @@
-import * as GoldenLayout from "golden-layout";
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, observable, reaction, trace, runInAction } 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 { Utils, returnTrue, emptyFunction, returnOne, returnZero } from "../../../Utils";
+import * as GoldenLayout from "../../../client/goldenLayout";
+import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { FieldId } from "../../../new_fields/RefField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
-import React = require("react");
import { SubCollectionViewProps } from "./CollectionSubView";
-import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
-import { Transform } from '../../util/Transform';
-import { Doc, Opt, Field } from "../../../new_fields/Doc";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { DocServer } from "../../DocServer";
-import { listSpec } from "../../../new_fields/Schema";
-import { Id, FieldId } from "../../../new_fields/RefField";
-import { faSignInAlt } from "@fortawesome/free-solid-svg-icons";
+import React = require("react");
+import { ParentDocSelector } from './ParentDocumentSelector';
+import { DocumentManager } from '../../util/DocumentManager';
+import { CollectionViewType } from './CollectionBaseView';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -72,11 +75,52 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this.stateChanged();
}
+ @undoBatch
+ @action
+ public CloseRightSplit = (document: Doc): boolean => {
+ let retVal = false;
+ if (this._goldenLayout.root.contentItems[0].isRow) {
+ retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+ if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+ Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
+ child.contentItems[0].remove();
+ this.layoutChanged(document);
+ 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);
+ 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('stateChanged');
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
+ if (removed) CollectionDockingView.Instance._removedDocs.push(removed);
+ this.stateChanged();
+ }
+
//
// Creates a vertical split on the right side of the docking view, and then adds the Document to that split
//
@action
- public AddRightSplit(document: Doc, minimize: boolean = false) {
+ public AddRightSplit = (document: Doc, minimize: boolean = false) => {
let docs = Cast(this.props.Document.data, listSpec(Doc));
if (docs) {
docs.push(document);
@@ -103,17 +147,26 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
newContentItem.config.width = 50;
}
if (minimize) {
- newContentItem.config.width = 10;
- newContentItem.config.height = 10;
+ // bcz: this makes the drag image show up better, but it also messes with fixed layout sizes
+ // newContentItem.config.width = 10;
+ // newContentItem.config.height = 10;
}
newContentItem.callDownwards('_$init');
- this._goldenLayout.root.callDownwards('setSize', [this._goldenLayout.width, this._goldenLayout.height]);
- this._goldenLayout.emit('stateChanged');
- this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
- this.stateChanged();
+ this.layoutChanged();
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);
@@ -128,6 +181,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();
@@ -135,6 +189,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;
@@ -148,12 +203,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 = "";
@@ -167,12 +225,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) => {
@@ -209,6 +272,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,
@@ -231,6 +298,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
stateChanged = () => {
+ let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
+ CollectionDockingView.Instance._removedDocs.map(theDoc =>
+ docs && docs.indexOf(theDoc) !== -1 &&
+ docs.splice(docs.indexOf(theDoc), 1));
+ CollectionDockingView.Instance._removedDocs.length = 0;
var json = JSON.stringify(this._goldenLayout.toConfig());
this.props.Document.dockingConfig = json;
if (this.undohack && !this.hack) {
@@ -251,44 +323,54 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
return template.content.firstChild;
}
- tabCreated = (tab: any) => {
+ tabCreated = async (tab: any) => {
if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
- DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async f => {
- if (f instanceof Doc) {
- const title = Cast(f.title, "string");
- if (title !== undefined) {
- tab.titleElement[0].textContent = title;
- }
- const lf = await Cast(f.linkedFromDocs, listSpec(Doc));
- const lt = await Cast(f.linkedToDocs, listSpec(Doc));
- let count = (lf ? lf.length : 0) + (lt ? lt.length : 0);
- let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`);
+ if (tab.contentItem.config.fixed) {
+ tab.contentItem.parent.config.fixed = true;
+ }
+ DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
+ if (doc instanceof Doc) {
+ let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
tab.element.append(counter);
+ let upDiv = document.createElement("span");
+ const stack = tab.contentItem.parent;
+ ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
+ tab.reactComponents = [upDiv];
+ tab.element.append(upDiv);
counter.DashDocId = tab.contentItem.config.props.documentId;
- tab.reactionDisposer = reaction((): [List<Field> | null | undefined, List<Field> | null | undefined] => [lf, lt],
- ([linkedFrom, linkedTo]) => {
- let count = (linkedFrom ? linkedFrom.length : 0) + (linkedTo ? linkedTo.length : 0);
- counter.innerHTML = count;
- });
+ tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ () => {
+ 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(function () {
+ .click(async function () {
if (tab.reactionDisposer) {
tab.reactionDisposer();
}
- DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async f => runInAction(() => {
- if (f instanceof Doc) {
- let docs = Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc));
- docs && docs.indexOf(f) !== -1 && docs.splice(docs.indexOf(f), 1);
- }
- }));
+ let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
tab.contentItem.remove();
});
}
+ tabDestroyed = (tab: any) => {
+ if (tab.reactComponents) {
+ for (const ele of tab.reactComponents) {
+ ReactDOM.unmountComponentAtNode(ele);
+ }
+ }
+ }
+ _removedDocs: Doc[] = [];
+
stackCreated = (stack: any) => {
//stack.header.controlsContainer.find('.lm_popout').hide();
stack.header.controlsContainer.find('.lm_close') //get the close icon
@@ -296,13 +378,21 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.click(action(function () {
//if (confirm('really close this?')) {
stack.remove();
+ stack.contentItems.map(async (contentItem: any) => {
+ let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
+ if (doc instanceof Doc) {
+ let theDoc = doc;
+ CollectionDockingView.Instance._removedDocs.push(theDoc);
+ }
+ });
//}
}));
stack.header.controlsContainer.find('.lm_popout') //get the close icon
.off('click') //unbind the current click handler
.click(action(function () {
- var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
- let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
+ stack.config.fixed = !stack.config.fixed;
+ // var url = DocServer.prepend("/doc/" + stack.contentItems[0].tab.contentItem.config.props.documentId);
+ // let win = window.open(url, stack.contentItems[0].tab.title, "width=300,height=400");
}));
}
@@ -312,6 +402,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
);
}
+
}
interface DockedFrameProps {
@@ -324,7 +415,12 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
-
+ get _stack(): any {
+ let parent = (this.props as any).glContainer.parent.parent;
+ if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1)
+ return parent.parent.contentItems[1];
+ return parent;
+ }
constructor(props: any) {
super(props);
DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
@@ -343,20 +439,38 @@ 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 / this.contentScaling() / 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);
+ 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)
+ if (!this._document) {
return (null);
+ }
return (
<div className="collectionDockingView-content" ref={this._mainCont}
- style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView key={this._document![Id]} Document={this._document!}
- toggleMinimized={emptyFunction}
+ style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
+ <DocumentView key={this._document[Id]} Document={this._document}
+ bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={this.contentScaling}
@@ -368,6 +482,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
+ addDocTab={this.addDocTab}
ContainingCollectionView={undefined} />
</div >);
}
@@ -375,7 +490,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
render() {
let theContent = this.content;
return !this._document ? (null) :
- <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}>
+ <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
{({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
</Measure>;
}
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index f6fb79582..50201bae8 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -1,49 +1,56 @@
.collectionPdfView-buttonTray {
- top : 15px;
- left : 20px;
- position: relative;
+ top: 15px;
+ left: 20px;
+ position: relative;
transform-origin: left top;
position: absolute;
}
+
.collectionPdfView-thumb {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: darkgray;
}
+
.collectionPdfView-slider {
- width:25px;
- height:25px;
+ width: 25px;
+ height: 25px;
transform-origin: left top;
position: absolute;
background: lightgray;
}
-.collectionPdfView-cont{
+
+.collectionPdfView-cont {
width: 100%;
height: 100%;
- position: absolute;
+ position: absolute;
top: 0;
- left:0;
+ left: 0;
+ z-index: -1;
}
+
.collectionPdfView-cont-dragging {
span {
user-select: none;
}
}
+
.collectionPdfView-backward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 0px;
+ top: 0px;
+ left: 0px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
}
+
.collectionPdfView-forward {
- color : white;
+ color: white;
font-size: 24px;
- top :0px;
- left : 45px;
+ top: 0px;
+ left: 45px;
position: absolute;
background-color: rgba(50, 50, 50, 0.2);
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index b3762206a..5e51437a4 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -8,7 +8,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView";
import { CollectionRenderProps, CollectionBaseView, CollectionViewType } from "./CollectionBaseView";
import { emptyFunction } from "../../../Utils";
import { NumCast } from "../../../new_fields/Types";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
@observer
@@ -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..186e006f3 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -4,52 +4,63 @@
.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;
position: absolute;
width: 100%;
height: 100%;
+ overflow: hidden;
.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 +68,7 @@
top: 0;
background: $main-accent;
}
+
.collectionSchemaView-columnsHandle {
position: absolute;
height: 37px;
@@ -66,6 +78,7 @@
bottom: 0;
background: $main-accent;
}
+
.collectionSchemaView-colDividerDragger {
position: relative;
box-sizing: border-box;
@@ -74,6 +87,7 @@
float: top;
width: 100%;
}
+
.collectionSchemaView-dividerDragger {
position: relative;
box-sizing: border-box;
@@ -82,11 +96,13 @@
float: left;
height: 100%;
}
+
.collectionSchemaView-tableContainer {
position: relative;
float: left;
height: 100%;
}
+
.ReactTable {
// position: absolute; // display: inline-block;
// overflow: auto;
@@ -95,6 +111,7 @@
background: $light-color;
box-sizing: border-box;
border: none !important;
+
.rt-table {
overflow-y: auto;
overflow-x: auto;
@@ -103,42 +120,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 +171,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 +214,12 @@ ul {
padding: 0px;
margin: 0px;
}
+
.schema-options-subHeader {
color: $intermediate-color;
margin-bottom: 5px;
}
+
#schemaOptionsMenuBtn:hover {
transform: scale(1.15);
}
@@ -198,15 +229,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 +245,8 @@ ul {
max-height: 175px;
font-family: $sans-serif;
font-size: 12px;
- }
-
+}
+
.Resizer {
box-sizing: border-box;
@@ -223,6 +254,7 @@ ul {
opacity: 0.5;
z-index: 1;
background-clip: padding-box;
+
&.horizontal {
height: 11px;
margin: -5px 0;
@@ -230,22 +262,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 +302,12 @@ ul {
-ms-flex-direction: column;
flex-direction: column;
}
+
header {
padding: 1rem;
background: #eee;
}
+
footer {
padding: 1rem;
background: #eee;
@@ -283,10 +321,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 67784fa81..11d71d023 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -2,7 +2,7 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCog, faPlus } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked, runInAction } from "mobx";
+import { action, computed, observable, untracked, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
@@ -19,13 +19,23 @@ import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { Opt, Field, Doc } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Opt, Field, Doc, DocListCastAsync, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
import { listSpec } from "../../../new_fields/Schema";
import { List } from "../../../new_fields/List";
-import { Id } from "../../../new_fields/RefField";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Gateway } from "../../northstar/manager/Gateway";
+import { Docs } from "../../documents/Documents";
+import { ContextMenu } from "../ContextMenu";
+import { CollectionView } from "./CollectionView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+library.add(faCog);
+library.add(faPlus);
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@@ -48,7 +58,7 @@ class KeyToggle extends React.Component<{ keyName: string, checked: boolean, tog
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
private _mainCont?: HTMLDivElement;
- private _startSplitPercent = 0;
+ private _startPreviewWidth = 0;
private DIVIDER_WIDTH = 4;
@observable _columns: Array<string> = ["title", "data", "author"];
@@ -56,15 +66,41 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@observable _columnsPercentage = 0;
@observable _keys: string[] = [];
@observable _newKeyName: string = "";
+ @observable previewScript: string = "";
- @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
+ @computed get previewHeight() { return () => this.props.PanelHeight() - 2 * this.borderWidth; }
+ @computed get tableWidth() { return this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH - this.previewWidth(); }
@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), undefined, "copy")}>{col}</p>,
+ accessor: (doc: Doc) => doc ? doc[col] : 0,
+ id: col
+ };
+ });
+ }
+
+ onHeaderDrag = (columnName: string) => {
+ let schemaDoc = Cast(this.props.Document.schemaDoc, Doc);
+ if (schemaDoc instanceof Doc) {
+ let columnDocs = DocListCast(schemaDoc.data);
+ if (columnDocs) {
+ let ddoc = columnDocs.find(doc => doc.title === columnName);
+ if (ddoc)
+ return ddoc;
+ }
+ }
+ 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,52 +112,48 @@ 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, this.props.Document.schemaDoc ? "copy" : undefined)(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];
- if (field) {
- //TODO Types
- // return field.ToScriptString();
- return String(field);
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return false;
}
return applyToDoc(props.Document, script.run);
}}
- OnFillDown={(value: string) => {
- let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { addReturn: true, params: { this: Doc.name } });
if (!script.compiled) {
return;
}
const run = script.run;
//TODO This should be able to be refactored to compile the script once
- const val = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc));
- if (val) {
- val.forEach(doc => applyToDoc(doc, run));
- }
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
</div >
@@ -171,30 +203,31 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
//toggles preview side-panel of schema
@action
- toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ toggleExpander = () => {
+ this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
}
+ onDividerDown = (e: React.PointerEvent) => {
+ this._startPreviewWidth = this.previewWidth();
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ }
@action
onDividerMove = (e: PointerEvent): void => {
let nativeWidth = this._mainCont!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
+ this.props.Document.schemaPreviewWidth = Math.min(nativeWidth.right - nativeWidth.left - 40,
+ this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]);
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent === this.splitPercentage) {
- this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ if (this._startPreviewWidth === this.previewWidth()) {
+ this.toggleExpander();
}
}
- onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this.splitPercentage;
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
- }
onPointerDown = (e: React.PointerEvent): void => {
if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
@@ -209,6 +242,33 @@ 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() : "0") + ",", "");
+ 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 }, { dbDoc: self.props.Document });
+ if (schemaDoc) {
+ //self.props.CollectionView.props.addDocument(schemaDoc, false);
+ self.props.Document.schemaDoc = schemaDoc;
+ }
+ }
+ }
+
@action
addColumn = () => {
this.columns.push(this._newKeyName);
@@ -220,64 +280,19 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
this._newKeyName = e.currentTarget.value;
}
- @observable previewScript: string = "";
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- 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;
- }
- get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); }
- get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; }
- get previewRegionWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * this.splitPercentage / 100; }
-
- private previewDocNativeWidth = () => Cast(this.previewDocument!.nativeWidth, "number", this.previewRegionWidth);
- private previewDocNativeHeight = () => Cast(this.previewDocument!.nativeHeight, "number", this.previewRegionHeight);
- private previewContentScaling = () => {
- let wscale = this.previewRegionWidth / (this.previewDocNativeWidth() ? this.previewDocNativeWidth() : this.previewRegionWidth);
- if (wscale * this.previewDocNativeHeight() > this.previewRegionHeight) {
- return this.previewRegionHeight / (this.previewDocNativeHeight() ? this.previewDocNativeHeight() : this.previewRegionHeight);
- }
- return wscale;
+ return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
}
- private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
- private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling();
- get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
+
getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
- - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
- - this.borderWidth).scale(1 / this.previewContentScaling())
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
- @computed
- get previewPanel() {
- // let doc = CompileScript(this.previewScript, { this: selected }, true)();
- const previewDoc = this.previewDocument;
- return !previewDoc ? (null) : (
- <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}>
- <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
- <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
- toggleMinimized={emptyFunction}
- addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
- ScreenToLocalTransform={this.getPreviewTransform}
- ContentScaling={this.previewContentScaling}
- PanelWidth={this.previewPanelWidth} PanelHeight={this.previewPanelHeight}
- ContainingCollectionView={this.props.CollectionView}
- focus={emptyFunction}
- parentActive={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- />
- </div>
- <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, this.previewPanelWidth() / 2)}px)` }} />
- </div>
- );
- }
get documentKeysCheckList() {
- const docs = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const docs = DocListCast(this.props.Document[this.props.fieldKey]);
let keys: { [key: string]: boolean } = {};
// bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
// then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
@@ -300,7 +315,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
<div id="schema-options-header"><h5><b>Options</b></h5></div>
<div id="options-flyout-div">
<h6 className="schema-options-subHeader">Preview Window</h6>
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div>
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} /> Show Preview </div>
<h6 className="schema-options-subHeader" >Displayed Columns</h6>
<ul id="schema-col-checklist" >
{this.documentKeysCheckList}
@@ -315,34 +330,130 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
@computed
+ get reactTable() {
+ trace();
+ let previewWidth = this.previewWidth() + 2 * this.borderWidth + this.DIVIDER_WIDTH + 1;
+ return <ReactTable style={{ position: "relative", float: "left", width: `calc(100% - ${previewWidth}px` }} data={this.childDocs} page={0} pageSize={this.childDocs.length} showPagination={false}
+ columns={this.tableColumns}
+ column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
+ getTrProps={this.getTrProps}
+ />
+ }
+
+ @computed
get dividerDragger() {
- return this.splitPercentage === 0 ? (null) :
+ return this.previewWidth() === 0 ? (null) :
<div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
}
+ @computed
+ get previewPanel() {
+ trace();
+ return <CollectionSchemaPreview
+ Document={this.previewDocument}
+ width={this.previewWidth}
+ height={this.previewHeight}
+ getTransform={this.getPreviewTransform}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={this.setPreviewScript}
+ previewScript={this.previewScript}
+ />
+ }
+ @action
+ setPreviewScript = (script: string) => {
+ this.previewScript = script;
+ }
+
render() {
- library.add(faCog);
- library.add(faPlus);
- //This can't just pass FieldValue to filter because filter passes other arguments to the passed in function, which end up as default values in FieldValue
- const children = (this.children || []).filter(doc => FieldValue(doc));
+ trace();
return (
<div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
- onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
- <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}>
- <ReactTable data={children} page={0} pageSize={children.length} showPagination={false}
- columns={this.columns.map(col => ({
- Header: col,
- accessor: (doc: Doc) => [doc, col],
- id: col
- }))}
- column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
- getTrProps={this.getTrProps}
- />
- </div>
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
+ {this.reactTable}
{this.dividerDragger}
- {this.previewPanel}
+ {!this.previewWidth() ? (null) : this.previewPanel}
{this.tableOptionsPanel}
</div>
);
}
+}
+interface CollectionSchemaPreviewProps {
+ Document?: Doc;
+ width: () => number;
+ height: () => number;
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+ getTransform: () => Transform;
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ addDocTab: (document: Doc, where: string) => void;
+ setPreviewScript: (script: string) => void;
+ previewScript?: string;
+}
+
+@observer
+export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
+ private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
+ private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
+ private contentScaling = () => {
+ let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width());
+ if (wscale * this.nativeHeight > this.props.height()) {
+ return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height());
+ }
+ return wscale;
+ }
+ private PanelWidth = () => this.nativeWidth * this.contentScaling();
+ private PanelHeight = () => this.nativeHeight * this.contentScaling();
+ private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling())
+ get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.setPreviewScript(e.currentTarget.value);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+ render() {
+ trace();
+ console.log(this.props.Document);
+ let input = this.props.previewScript === undefined ? (null) :
+ <input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} />;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width() }}>
+ {!this.props.Document || !this.props.width ? (null) : (
+ <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)` }}>
+ <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
+ addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={this.getTransform}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+ ContainingCollectionView={this.props.CollectionView}
+ focus={emptyFunction}
+ parentActive={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction}
+ addDocTab={this.props.addDocTab}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </div>)}
+ {input}
+ </div>);
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
new file mode 100644
index 000000000..4d84aaaa9
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -0,0 +1,51 @@
+@import "../globalCssVariables";
+
+.collectionStackingView {
+ top: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ position: absolute;
+ overflow-y: auto;
+ border-width: 0;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border-color: $light-color-secondary;
+ border-style: solid;
+ border-radius: 0 0 $border-radius $border-radius;
+ box-sizing: border-box;
+
+ .collectionStackingView-docView-container {
+ width: 45%;
+ margin: 5% 2.5%;
+ padding-left: 2.5%;
+ height: auto;
+ }
+
+ .collectionStackingView-flexCont {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: center;
+ }
+
+ .collectionStackingView-masonrySingle, .collectionStackingView-masonryGrid{
+ width:100%;
+ height:100%;
+ position: absolute;
+ }
+ .collectionStackingView-masonryGrid {
+ display:grid;
+ }
+
+ .collectionStackingView-description {
+ font-size: 100%;
+ margin-bottom: 1vw;
+ padding: 10px;
+ height: 2vw;
+ width: 100%;
+ font-family: $sans-serif;
+ background: $dark-color;
+ color: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
new file mode 100644
index 000000000..da7ea50c6
--- /dev/null
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -0,0 +1,179 @@
+import React = require("react");
+import { action, computed, IReactionDisposer, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { BoolCast, NumCast } from "../../../new_fields/Types";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { CollectionSchemaPreview } from "./CollectionSchemaView";
+import "./CollectionStackingView.scss";
+import { CollectionSubView } from "./CollectionSubView";
+
+@observer
+export class CollectionStackingView extends CollectionSubView(doc => doc) {
+ _masonryGridRef: HTMLDivElement | null = null;
+ _heightDisposer?: IReactionDisposer;
+ get gridGap() { return 10; }
+ get gridSize() { return 20; }
+ get singleColumn() { return BoolCast(this.props.Document.singleColumn, true); }
+ get columnWidth() { return this.singleColumn ? this.props.PanelWidth() - 4 * this.gridGap : NumCast(this.props.Document.columnWidth, 250); }
+
+ componentDidMount() {
+ this._heightDisposer = reaction(() => [this.childDocs.map(d => [d.height, d.width, d.zoomBasis, d.nativeHeight, d.nativeWidth, d.isMinimized]), this.columnWidth, this.props.PanelHeight()],
+ () => {
+ if (this.singleColumn) {
+ this.props.Document.height = this.childDocs.filter(d => !d.isMinimized).reduce((height, d) => {
+ let hgt = d[HeightSym]();
+ let wid = d[WidthSym]();
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ if (nw && nh) hgt = nh / nw * Math.min(this.columnWidth, wid);
+ return height + hgt + 2 * this.gridGap;
+ }, this.gridGap * 2);
+ }
+ }, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ if (this._heightDisposer) this._heightDisposer();
+ }
+
+ @action
+ moveDocument = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean): boolean => {
+ this.props.removeDocument(doc);
+ addDocument(doc);
+ return true;
+ }
+ getDocTransform(doc: Doc, dref: HTMLDivElement) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(dref);
+ let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
+ let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ return this.props.ScreenToLocalTransform().translate(offset[0], offset[1]).scale(NumCast(doc.width, 1) / this.columnWidth);
+ }
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDropTarget(ele!);
+ }
+ @undoBatch
+ @action
+ public collapseToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ maximizedDoc.isMinimized = !isMinimized;
+ });
+ }
+ }
+
+ @computed
+ get singleColumnChildren() {
+ return this.childDocs.filter(d => !d.isMinimized).map((d, i) => {
+ let dref = React.createRef<HTMLDivElement>();
+ let script = undefined;
+ let colWidth = () => d.nativeWidth ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth;
+ let margin = colWidth() < this.columnWidth ? "auto" : undefined;
+ let rowHeight = () => {
+ let hgt = d[HeightSym]();
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ if (nw && nh) hgt = nh / nw * colWidth();
+ return hgt;
+ }
+ let dxf = () => this.getDocTransform(d, dref.current!).scale(this.columnWidth / d[WidthSym]());
+ return <div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{ marginTop: `${i ? 2 * this.gridGap : 0}px`, width: colWidth(), height: rowHeight(), marginLeft: margin, marginRight: margin }} >
+ <CollectionSchemaPreview
+ Document={d}
+ width={colWidth}
+ height={rowHeight}
+ getTransform={dxf}
+ CollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ active={this.props.active}
+ whenActiveChanged={this.props.whenActiveChanged}
+ addDocTab={this.props.addDocTab}
+ setPreviewScript={emptyFunction}
+ previewScript={script}>
+ </CollectionSchemaPreview>
+ </div>;
+ });
+ }
+ @computed
+ get children() {
+ return this.childDocs.filter(d => !d.isMinimized).map(d => {
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(d, dref.current!);
+ let colSpan = Math.ceil(Math.min(d[WidthSym](), this.columnWidth + this.gridGap) / (this.gridSize + this.gridGap));
+ let rowSpan = Math.ceil((this.columnWidth / d[WidthSym]() * d[HeightSym]() + this.gridGap) / (this.gridSize + this.gridGap));
+ let childFocus = (doc: Doc) => {
+ doc.libraryBrush = true;
+ this.props.focus(this.props.Document); // just focus on this collection, not the underlying document because the API doesn't support adding an offset to focus on and we can't pan zoom our contents to be centered.
+ }
+ return (<div className="collectionStackingView-masonryDoc"
+ key={d[Id]}
+ ref={dref}
+ style={{
+ width: NumCast(d.nativeWidth, d[WidthSym]()),
+ height: NumCast(d.nativeHeight, d[HeightSym]()),
+ transformOrigin: "top left",
+ gridRowEnd: `span ${rowSpan}`,
+ gridColumnEnd: `span ${colSpan}`,
+ transform: `scale(${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())}, ${this.columnWidth / NumCast(d.nativeWidth, d[WidthSym]())})`
+ }} >
+ <DocumentView key={d[Id]} Document={d}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.moveDocument}
+ ContainingCollectionView={this.props.CollectionView}
+ isTopMost={false}
+ ScreenToLocalTransform={dxf}
+ focus={childFocus}
+ ContentScaling={returnOne}
+ PanelWidth={d[WidthSym]}
+ PanelHeight={d[HeightSym]}
+ selectOnLoad={false}
+ parentActive={this.props.active}
+ addDocTab={this.props.addDocTab}
+ bringToFront={emptyFunction}
+ whenActiveChanged={this.props.whenActiveChanged}
+ collapseToPoint={this.collapseToPoint}
+ />
+ </div>);
+ })
+ }
+ render() {
+ let leftMargin = 2 * this.gridGap;
+ let topMargin = 2 * this.gridGap;
+ let itemCols = Math.ceil(this.columnWidth / (this.gridSize + this.gridGap));
+ let cells = Math.floor((this.props.PanelWidth() - leftMargin) / (itemCols * (this.gridSize + this.gridGap)));
+ return (
+ <div className="collectionStackingView" style={{ height: "100%" }}
+ ref={this.createRef} onWheel={(e: React.WheelEvent) => e.stopPropagation()}>
+ <div className={`collectionStackingView-masonry${this.singleColumn ? "Single" : "Grid"}`}
+ style={{
+ padding: `${topMargin}px 0px 0px ${leftMargin}px`,
+ width: this.singleColumn ? "100%" : `${cells * itemCols * (this.gridSize + this.gridGap) + leftMargin}`,
+ height: "100%",
+ overflow: "hidden",
+ marginRight: "auto",
+ position: "relative",
+ gridGap: this.gridGap,
+ gridTemplateColumns: this.singleColumn ? undefined : `repeat(auto-fill, minmax(${this.gridSize}px,1fr))`,
+ gridAutoRows: this.singleColumn ? undefined : `${this.gridSize}px`
+ }}
+ >
+ {this.singleColumn ? this.singleColumnChildren : this.children}
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 828ac880a..fe9e12640 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -10,13 +10,13 @@ import * as rp from 'request-promise';
import { CollectionView } from "./CollectionView";
import { CollectionPDFView } from "./CollectionPDFView";
import { CollectionVideoView } from "./CollectionVideoView";
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
import { DocComponent } from "../DocComponent";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue, FieldValue } from "../../../new_fields/Types";
+import { Cast, PromiseValue, FieldValue, ListSpec } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
import { DocServer } from "../../DocServer";
-import { ObjectField } from "../../../new_fields/ObjectField";
+import CursorField from "../../../new_fields/CursorField";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -30,8 +30,6 @@ export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
}
-export type CursorEntry = TupleField<[string, string], [number, number]>;
-
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
@@ -47,33 +45,32 @@ 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 Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => FieldValue(doc));
+ return DocListCast(this.props.Document[this.props.fieldKey]);
}
@action
protected async setCursorPosition(position: [number, number]) {
- return;
let ind;
let doc = this.props.Document;
let id = CurrentUserUtils.id;
let email = CurrentUserUtils.email;
+ let pos = { x: position[0], y: position[1] };
if (id && email) {
- let textInfo: [string, string] = [id, email];
const proto = await doc.proto;
if (!proto) {
return;
}
- let cursors = await Cast(proto.cursors, listSpec(ObjectField));
+ let cursors = Cast(proto.cursors, listSpec(CursorField));
if (!cursors) {
- proto.cursors = cursors = new List<ObjectField>();
+ proto.cursors = cursors = new List<CursorField>();
}
- if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
- cursors[ind].Data[1] = position;
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
+ cursors[ind].setPosition(pos);
} else {
- let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
+ let entry = new CursorField({ metadata: { id: id, identifier: email, timestamp: Date.now() }, position: pos });
cursors.push(entry);
}
}
@@ -132,7 +129,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
options.dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
- if (path.includes('localhost')) {
+ if (path.includes(window.location.hostname)) {
let s = path.split('/');
let id = s[s.length - 1];
DocServer.GetRefField(id).then(field => {
@@ -156,6 +153,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ if (e.ctrlKey) {
+ e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
+ return;
+ }
let html = e.dataTransfer.getData("text/html");
let text = e.dataTransfer.getData("text/plain");
@@ -170,6 +171,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>[] = [];
@@ -206,7 +212,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let path = window.location.origin + file;
- let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName });
+ let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 300, width: 300, title: dropFileName });
docPromise.then(doc => doc && this.props.addDocument(doc));
}));
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 19d4abc05..bb3be0a73 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -23,43 +23,44 @@
margin: 5px 0;
}
- .collection-child {
- margin-top: 10px;
- margin-bottom: 10px;
- }
.no-indent {
padding-left: 0;
}
.bullet {
- position: absolute;
- width: 1.5em;
- display: inline-block;
+ float: left;
+ position: relative;
+ width: 15px;
+ display: block;
color: $intermediate-color;
margin-top: 3px;
- transform: scale(1.3,1.3);
- }
-
- .coll-title {
- width:max-content;
- display: block;
- font-size: 24px;
+ transform: scale(1.3, 1.3);
}
.docContainer {
margin-left: 10px;
display: block;
- width: max-content;
+ // width:100%;//width: max-content;
}
.docContainer:hover {
- .delete-button {
- display: inline;
- // width: auto;
+ .treeViewItem-openRight {
+ display: inline-block;
+ // display: inline;
+ svg {
+ display:block;
+ padding:0px;
+ margin: 0px;
+ }
}
}
+
+ .editableView-container {
+ font-weight: bold;
+ }
+
.delete-button {
color: $intermediate-color;
// float: right;
@@ -67,4 +68,32 @@
// margin-top: 3px;
display: inline;
}
+
+ .treeViewItem-openRight {
+ margin-left: 5px;
+ display: none;
+ }
+
+ .docContainer:hover {
+ .delete-button {
+ display: inline;
+ // width: auto;
+ }
+ }
+
+ .coll-title {
+ width: max-content;
+ display: block;
+ font-size: 24px;
+ }
+
+ .collection-child {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ }
+
+ .collectionTreeView-keyHeader {
+ font-style: italic;
+ font-size: 8pt;
+ }
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b67d6f965..2814c0502 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,5 +1,5 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretDown, faCaretRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import { faCaretDown, faCaretRight, faTrashAlt, faAngleRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
@@ -9,15 +9,17 @@ import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
import { Document, listSpec } from '../../../new_fields/Schema';
-import { Cast, StrCast, BoolCast, FieldValue } from '../../../new_fields/Types';
-import { Doc } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/RefField';
+import { Cast, StrCast, BoolCast, FieldValue, NumCast } from '../../../new_fields/Types';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/FieldSymbols';
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 { Docs } from '../../documents/Documents';
+import { MainView } from '../MainView';
+import { CollectionViewType } from './CollectionBaseView';
export interface TreeViewProps {
@@ -25,6 +27,7 @@ export interface TreeViewProps {
deleteDoc: (doc: Doc) => void;
moveDocument: DragManager.MoveFunction;
dropAction: "alias" | "copy" | undefined;
+ addDocTab: (doc: Doc, where: string) => void;
}
export enum BulletType {
@@ -34,6 +37,7 @@ export enum BulletType {
}
library.add(faTrashAlt);
+library.add(faAngleRight);
library.add(faCaretDown);
library.add(faCaretRight);
@@ -45,15 +49,27 @@ class TreeView extends React.Component<TreeViewProps> {
@observable _collapsed: boolean = true;
- delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+
+ @undoBatch openRight = async () => {
+ if (this.props.document.dockingConfig) {
+ MainView.Instance.openWorkspace(this.props.document);
+ } else {
+ this.props.addDocTab(this.props.document, "openRight");
+ }
+ }
get children() {
return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
}
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ }
+
@action
- remove = (document: Document) => {
- let children = Cast(this.props.document.data, listSpec(Doc), []);
+ remove = (document: Document, key: string) => {
+ let children = Cast(this.props.document[key], listSpec(Doc), []);
if (children) {
children.splice(children.indexOf(document), 1);
}
@@ -65,7 +81,7 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}
//TODO This should check if it was removed
- this.remove(document);
+ this.remove(document, "data");
return addDoc(document);
}
@@ -79,6 +95,15 @@ class TreeView extends React.Component<TreeViewProps> {
return <div className="bullet" onClick={onClicked}>{bullet ? <FontAwesomeIcon icon={bullet} /> : ""} </div>;
}
+ @action
+ onMouseEnter = () => {
+ this._isOver = true;
+ }
+ @observable _isOver: boolean = false;
+ @action
+ onMouseLeave = () => {
+ this._isOver = false;
+ }
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -87,7 +112,8 @@ class TreeView extends React.Component<TreeViewProps> {
let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
let editableView = (titleString: string) =>
(<EditableView
- display={"inline"}
+ oneLine={!this._isOver ? true : false}
+ display={"inline-block"}
contents={titleString}
height={36}
GetValue={() => StrCast(this.props.document.title)}
@@ -97,46 +123,73 @@ class TreeView extends React.Component<TreeViewProps> {
return true;
}}
/>);
+ let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
+ let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+ <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
+ <FontAwesomeIcon icon="angle-right" size="lg" />
+ {/* <FontAwesomeIcon icon="angle-right" size="lg" /> */}
+ </div>);
return (
- <div className="docContainer" ref={reference} onPointerDown={onItemDown}>
+ <div className="docContainer" ref={reference} onPointerDown={onItemDown} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}
+ 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))}
- {/* <div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div> */}
+ {openRight}
+ {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
</div >);
}
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 Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.document) });
- if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+ if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
+ if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+ }
+ ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+ } else {
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
}
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
e.stopPropagation();
}
}
- onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; }
- onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; }
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.document.libraryBrush = false; };
render() {
let bulletType = BulletType.List;
- let contentElement: JSX.Element | null = (null);
- var children = Cast(this.props.document.data, listSpec(Doc));
- if (children) { // add children for a collection
- if (!this._collapsed) {
- bulletType = BulletType.Collapsible;
- contentElement = <ul>
- {TreeView.GetChildElements(children, this.remove, this.move, this.props.dropAction)}
- </ul >;
- }
- else bulletType = BulletType.Collapsed;
+ let contentElement: (JSX.Element | null)[] = [];
+ let keys = Array.from(Object.keys(this.props.document));
+ if (this.props.document.proto instanceof Doc) {
+ keys.push(...Array.from(Object.keys(this.props.document.proto)));
+ while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
}
+ keys.map(key => {
+ let docList = DocListCast(this.props.document[key]);
+ let doc = Cast(this.props.document[key], Doc);
+ if (doc instanceof Doc || docList.length) {
+ if (!this._collapsed) {
+ bulletType = BulletType.Collapsible;
+ let spacing = (key === "data") ? 0 : -10;
+ contentElement.push(<ul key={key + "more"}>
+ {(key === "data") ? (null) :
+ <span className="collectionTreeView-keyHeader" style={{ display: "block", marginTop: "7px" }} key={key}>{key}</span>}
+ <div style={{ display: "block", marginTop: `${spacing}px` }}>
+ {TreeView.GetChildElements(doc instanceof Doc ? [doc] : docList, key !== "data", (doc: Doc) => this.remove(doc, key), this.move, this.props.dropAction, this.props.addDocTab)}
+ </div>
+ </ul >);
+ } else {
+ bulletType = BulletType.Collapsed;
+ }
+ }
+ });
return <div className="treeViewItem-container"
- style={{ background: BoolCast(this.props.document.libraryBrush, false) ? "#06121212" : "0" }}
- onContextMenu={this.onWorkspaceContextMenu}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ onContextMenu={this.onWorkspaceContextMenu}>
<li className="collection-child">
{this.renderBullet(bulletType)}
{this.renderTitle()}
@@ -144,9 +197,9 @@ class TreeView extends React.Component<TreeViewProps> {
</li>
</div>;
}
- public static GetChildElements(docs: Doc[], remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
- return docs.filter(child => !child.excludeFromLibrary).filter(doc => FieldValue(doc)).map(child =>
- <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
+ public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => void) {
+ return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
+ <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} addDocTab={addDocTab} />);
}
}
@@ -160,27 +213,24 @@ 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()) });
- }
- if (!ContextMenu.Instance.getItems().some(item => item.description === "Delete")) {
- ContextMenu.Instance.addItem({ description: "Delete", event: undoBatch(() => this.remove(this.props.Document)) });
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped() && this.props.Document.excludeFromLibrary) { // excludeFromLibrary means this is the user document
+ ContextMenu.Instance.addItem({ description: "Create Workspace", event: undoBatch(() => MainView.Instance.createNewWorkspace()) });
+ ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.remove(this.props.Document)) });
}
}
render() {
- trace();
- const children = this.children;
let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
- if (!children) {
+ if (!this.childDocs) {
return (null);
}
- let childElements = TreeView.GetChildElements(children, this.remove, this.props.moveDocument, dropAction);
+ let childElements = TreeView.GetChildElements(this.childDocs, false, this.remove, this.props.moveDocument, dropAction, this.props.addDocTab);
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..9d2c23d3e 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -2,10 +2,10 @@
.collectionVideoView-cont{
width: 100%;
height: 100%;
- position: absolute;
+ position: inherit;
top: 0;
left:0;
-
+ z-index: -1;
}
.collectionVideoView-time{
color : white;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index 9dee217cb..7853544d5 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,4 +1,5 @@
import { action, observable, trace } from "mobx";
+import * as htmlToImage from "html-to-image";
import { observer } from "mobx-react";
import { ContextMenu } from "../ContextMenu";
import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
@@ -6,30 +7,33 @@ import React = require("react");
import "./CollectionVideoView.scss";
import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { emptyFunction } from "../../../Utils";
-import { Id } from "../../../new_fields/RefField";
+import { emptyFunction, Utils } from "../../../Utils";
+import { Id } from "../../../new_fields/FieldSymbols";
import { VideoBox } from "../nodes/VideoBox";
+import { NumCast, Cast, StrCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import { SearchBox } from "../SearchBox";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
@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 +42,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 +63,55 @@ 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 });
}
+
+ let field = Cast(this.props.Document[this.props.fieldKey], VideoField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt"
+ });
+ }
+ let width = NumCast(this.props.Document.width);
+ let height = NumCast(this.props.Document.height);
+ ContextMenu.Instance.addItem({
+ description: "Take Snapshot", event: async () => {
+ var canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ ctx && ctx.drawImage(this._videoBox!.player!, 0, 0, canvas.width, canvas.height);
+
+ //convert to desired file format
+ var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+
+ let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ SearchBox.convertDataUri(dataUrl, filename).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.ImageDocument(url, {
+ x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
+ width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
+ });
+ this.props.addDocument && this.props.addDocument(imageSummary, false);
+ DocUtils.MakeLink(imageSummary, this.props.Document);
+ }
+ });
+ },
+ icon: "expand-arrows-alt"
+ });
}
- 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 +122,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..68eefab4c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,17 +1,27 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faProjectDiagram, faSignature, faSquare, faTh, faThList, 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/FieldSymbols';
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 { ContextMenuProps } from '../ContextMenuItem';
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
+import { CollectionDockingView } from "./CollectionDockingView";
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { CollectionStackingView } from './CollectionStackingView';
+import { CollectionTreeView } from "./CollectionTreeView";
+export const COLLECTION_BORDER_WIDTH = 2;
+
+library.add(faTh);
+library.add(faTree);
+library.add(faSquare);
+library.add(faProjectDiagram);
+library.add(faSignature);
+library.add(faThList);
@observer
export class CollectionView extends React.Component<FieldViewProps> {
@@ -23,6 +33,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
+ case CollectionViewType.Stacking: return (<CollectionStackingView {...props} CollectionView={this} />);
case CollectionViewType.Freeform:
default:
return (<CollectionFreeFormView {...props} CollectionView={this} />);
@@ -34,9 +45,15 @@ 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) });
+ let subItems: ContextMenuProps[] = [];
+ subItems.push({ description: "Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Freeform), icon: "signature" });
+ if (CollectionBaseView.InSafeMode()) {
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Invalid), icon: "project-diagram" });
+ }
+ subItems.push({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "th-list" });
+ subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
+ subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
+ ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
}
}
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
new file mode 100644
index 000000000..2dd3e49f2
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -0,0 +1,22 @@
+.PDS-flyout {
+ position: absolute;
+ z-index: 9999;
+ background-color: #eeeeee;
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
+ min-width: 150px;
+ color: black;
+ top: 12px;
+
+ padding: 10px;
+ border-radius: 3px;
+
+ hr {
+ height: 1px;
+ margin: 0px;
+ background-color: gray;
+ border-top: 0px;
+ border-bottom: 0px;
+ border-right: 0px;
+ border-left: 0px;
+ }
+} \ 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..f11af04a3
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -0,0 +1,94 @@
+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/FieldSymbols";
+import { SearchUtil } from "../../util/SearchUtil";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { NumCast } from "../../../new_fields/Types";
+import { CollectionViewType } from "./CollectionBaseView";
+
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
+@observer
+export class SelectorContextMenu extends React.Component<SelectorProps> {
+ @observable private _docs: { col: Doc, target: Doc }[] = [];
+ @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
+
+ constructor(props: SelectorProps) {
+ super(props);
+
+ this.fetchDocuments();
+ }
+
+ async fetchDocuments() {
+ let aliases = (await SearchUtil.GetAliasesOfDocument(this.props.Document)).filter(doc => doc !== this.props.Document);
+ const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
+ const map: Map<Doc, Doc> = new Map;
+ const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search(`data_l:"${doc[Id]}"`, true)));
+ allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
+ docs.forEach(doc => map.delete(doc));
+ runInAction(() => {
+ this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: this.props.Document }));
+ this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+ });
+ }
+
+ getOnClick({ col, target }: { col: Doc, target: Doc }) {
+ return () => {
+ col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
+ if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+ const newPanX = NumCast(target.x) + NumCast(target.width) / NumCast(target.zoomBasis, 1) / 2;
+ const newPanY = NumCast(target.y) + NumCast(target.height) / NumCast(target.zoomBasis, 1) / 2;
+ col.panX = newPanX;
+ col.panY = newPanY;
+ }
+ this.props.addDocTab(col, "inTab");
+ };
+ }
+
+ render() {
+ return (
+ <>
+ <p>Contexts:</p>
+ {this._docs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ {this._otherDocs.length ? <hr></hr> : null}
+ {this._otherDocs.map(doc => <p><a onClick={this.getOnClick(doc)}>{doc.col.title}</a></p>)}
+ </>
+ );
+ }
+}
+
+@observer
+export class ParentDocSelector extends React.Component<SelectorProps> {
+ @observable hover = false;
+
+ @action
+ onMouseLeave = () => {
+ this.hover = false;
+ }
+
+ @action
+ onMouseEnter = () => {
+ this.hover = true;
+ }
+
+ render() {
+ let flyout;
+ if (this.hover) {
+ flyout = (
+ <div className="PDS-flyout" title=" ">
+ <SelectorContextMenu {...this.props} />
+ </div>
+ );
+ }
+ return (
+ <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ <p>^</p>
+ {flyout}
+ </span>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 3e8a8a442..737ffba7d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,12 +1,12 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- stroke-width: 3;
transform: translate(10000px,10000px);
+ opacity: 0.5;
pointer-events: all;
}
.collectionfreeformlinkview-linkCircle {
- stroke: black;
- stroke-width: 3;
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
transform: translate(10000px,10000px);
pointer-events: all;
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 3b700b053..61de83f57 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -40,18 +40,18 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
let l = this.props.LinkDocs;
let a = this.props.A;
let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
return (
<>
- <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- style={{ strokeWidth: `${l.length * 5}` }}
+ <line key={"linkLine"} className="collectionfreeformlinkview-linkLine"
+ style={{ strokeWidth: `${35 * l.length / 2}` }}
x1={`${x1}`} y1={`${y1}`}
x2={`${x2}`} y2={`${y2}`} />
- <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={10} onPointerDown={this.onPointerDown} />
+ <circle key={"linkCircle"} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} />
</>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b34e0856e..a43c5f241 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Utils } from "../../../../Utils";
import { DocumentManager } from "../../../util/DocumentManager";
@@ -7,70 +7,68 @@ import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Doc } from "../../../../new_fields/Doc";
+import { Doc, DocListCastAsync, DocListCast } from "../../../../new_fields/Doc";
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 { Id } from "../../../../new_fields/FieldSymbols";
@observer
export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
- this._brushReactionDisposer = reaction(() => Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).map(doc => NumCast(doc.x)),
+ this._brushReactionDisposer = reaction(
() => {
- let views = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).filter(doc => StrCast(doc.backgroundLayout, "").indexOf("istogram") !== -1);
- for (let i = 0; i < views.length; i++) {
- for (let j = 0; j < views.length; j++) {
- let srcDoc = views[j];
- let dstDoc = views[i];
- let x1 = NumCast(srcDoc.x);
- let x1w = NumCast(srcDoc.width, -1);
- let x2 = NumCast(dstDoc.x);
- let x2w = NumCast(dstDoc.width, -1);
- if (x1w < 0 || x2w < 0 || i === j) {
- continue;
- }
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ },
+ () => {
+ let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ views.forEach((dstDoc, i) => {
+ views.forEach((srcDoc, j) => {
let dstTarg = dstDoc;
let srcTarg = srcDoc;
- let findBrush = (field: List<Doc>) => field.findIndex(brush => {
- let bdocs = brush ? Cast(brush.brushingDocs, listSpec(Doc), []) : [];
- return (bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false);
- });
- let brushAction = (field: List<Doc>) => {
- let found = findBrush(field);
- if (found !== -1) {
- console.log("REMOVE BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- field.splice(found, 1);
- }
- };
- if (Math.abs(x1 + x1w - x2) < 20) {
- let linkDoc: Doc = new Doc();
- linkDoc.title = "Histogram Brush";
- linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
- linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
-
- brushAction = (field: List<Doc>) => {
- if (findBrush(field) === -1) {
- console.log("ADD BRUSH " + srcTarg.Title + " " + dstTarg.Title);
- (findBrush(field) === -1) && field.push(linkDoc);
+ let x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ let x2w = NumCast(dstDoc.width, -1) / NumCast(srcDoc.zoomBasis, 1);
+ if (x1w < 0 || x2w < 0 || i === j) { }
+ else {
+ let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ });
+ let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ let found = findBrush(field);
+ if (found !== -1) {
+ console.log("REMOVE BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.splice(found, 1);
}
};
- }
- let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc));
- if (dstBrushDocs === undefined) {
- dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
- }
- let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc));
- if (srcBrushDocs === undefined) {
- srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
- }
- brushAction(dstBrushDocs);
- brushAction(srcBrushDocs);
+ if (Math.abs(x1 + x1w - x2) < 20) {
+ let linkDoc: Doc = new Doc();
+ linkDoc.title = "Histogram Brush";
+ linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
- }
- }
+ brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ if (findBrush(field) === -1) {
+ console.log("ADD BRUSH " + srcTarg.title + " " + dstTarg.title);
+ field.push(linkDoc);
+ }
+ };
+ }
+ if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ brushAction(dstBrushDocs);
+ brushAction(srcBrushDocs);
+ }
+ });
+ });
});
}
componentWillUnmount() {
@@ -86,7 +84,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
}
if (view.props.ContainingCollectionView) {
let collid = view.props.ContainingCollectionView.props.Document[Id];
- Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []).
+ DocListCast(this.props.Document[this.props.fieldKey]).
filter(child =>
child[Id] === collid).map(view =>
DocumentManager.Instance.getDocumentViews(view).map(view =>
@@ -102,21 +100,27 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
let targetViews = this.documentAnchors(connection.b);
let possiblePairs: { a: Doc, b: Doc, }[] = [];
srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
- possiblePairs.map(possiblePair =>
- drawnPairs.reduce((found, drawnPair) => {
- let match = (possiblePair.a === drawnPair.a && possiblePair.b === drawnPair.b);
+ possiblePairs.map(possiblePair => {
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
+ let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
+ let match = match1 || match2;
if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
drawnPair.l.push(connection.l);
}
return match || found;
- }, false)
- ||
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
- );
+ }, false)) {
+ console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
+ drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+ }
+ });
return drawnPairs;
}, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ return connections.map(c => {
+ let x = c.l.reduce((p, l) => p + l[Id], "");
+ return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
+ removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+ });
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 036745eca..2838b7905 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -1,26 +1,38 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
-import { CollectionViewProps, CursorEntry } from "../CollectionSubView";
+import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import CursorField from "../../../../new_fields/CursorField";
+import { List } from "../../../../new_fields/List";
+import { Cast } from "../../../../new_fields/Types";
+import { listSpec } from "../../../../new_fields/Schema";
+import * as mobxUtils from 'mobx-utils';
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
- protected getCursors(): CursorEntry[] {
+
+ protected getCursors(): CursorField[] {
let doc = this.props.Document;
+
let id = CurrentUserUtils.id;
- let cursors = doc.GetList(KeyStore.Cursors, [] as CursorEntry[]);
- let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
- return id ? notMe : [];
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ const now = mobxUtils.now();
+ // const now = Date.now();
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000);
}
private crosshairs?: HTMLCanvasElement;
drawCrosshairs = (backgroundColor: string) => {
if (this.crosshairs) {
- let c = this.crosshairs;
- let ctx = c.getContext('2d');
+ let ctx = this.crosshairs.getContext('2d');
if (ctx) {
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, 20, 20);
@@ -49,29 +61,26 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
}
}
}
- @computed
+
get sharedCursors() {
- return this.getCursors().map(entry => {
- if (entry.Data.length > 0) {
- let id = entry.Data[0][0];
- let email = entry.Data[0][1];
- let point = entry.Data[1];
- this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22");
- return (
- <div key={id} className="collectionFreeFormRemoteCursors-cont"
- style={{ transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)` }}
- >
- <canvas className="collectionFreeFormRemoteCursors-canvas"
- ref={(el) => { if (el) this.crosshairs = el; }}
- width={20}
- height={20}
- />
- <p className="collectionFreeFormRemoteCursors-symbol">
- {email[0].toUpperCase()}
- </p>
- </div>
- );
- }
+ return this.getCursors().map(c => {
+ let m = c.data.metadata;
+ let l = c.data.position;
+ this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
+ return (
+ <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => { if (el) this.crosshairs = el; }}
+ width={20}
+ height={20}
+ />
+ <p className="collectionFreeFormRemoteCursors-symbol">
+ {m.identifier[0].toUpperCase()}
+ </p>
+ </div>
+ );
});
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index cb849b325..e10ba9d7e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -25,6 +25,9 @@
height: 100%;
width: 100%;
}
+ >.jsx-parser {
+ z-index:0;
+ }
//nested freeform views
// .collectionfreeformview-container {
@@ -37,7 +40,9 @@
border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
- overflow: hidden;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
@@ -50,6 +55,10 @@
position: inherit;
height: 100%;
}
+
+ >.jsx-parser {
+ z-index:0;
+ }
.formattedTextBox-cont {
background: $light-color-secondary;
@@ -61,7 +70,10 @@
border-radius: $border-radius;
box-sizing: border-box;
position:absolute;
- overflow: hidden;
+ z-index: -1;
+ .marqueeView {
+ overflow: hidden;
+ }
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 69b880d20..e741953d6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -22,9 +22,11 @@ import v5 = require("uuid/v5");
import { Timeline } from "../../nodes/Timeline";
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";
+import { Id } from "../../../../new_fields/FieldSymbols";
export const panZoomSchema = createSchema({
panX: "number",
@@ -37,23 +39,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 FieldValue(this.Document.nativeWidth, 0); }
- @computed get nativeHeight() { return FieldValue(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) => {
@@ -66,13 +67,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 FieldValue(this.children, [] as Doc[]).filter((doc) => {
+ return this.childDocs.filter(doc => {
var page = NumCast(doc.page, -1);
return page === curPage || page === -1;
});
@@ -90,7 +91,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)) {
@@ -112,15 +113,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
- let childSelected = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), [] as Doc[]).filter(doc => doc).reduce((childSelected, doc) => {
- var dv = DocumentManager.Instance.getDocumentView(doc);
- return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false);
- }, false);
- if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
- (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
- (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) ||
- (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
- ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || 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);
@@ -138,29 +131,40 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
if (!e.cancelBubble) {
- let x = Cast(this.props.Document.panX, "number", 0);
- let y = Cast(this.props.Document.panY, "number", 0);
- 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 ? Cast(docs[0].x, "number", 0) : 0;
- let maxx = docs.length ? Cast(docs[0].width, "number", 0) + minx : minx;
- let miny = docs.length ? Cast(docs[0].y, "number", 0) : 0;
- let maxy = docs.length ? Cast(docs[0].height, "number", 0) + miny : miny;
+ let minx = docs.length ? NumCast(docs[0].x) : 0;
+ let maxx = docs.length ? NumCast(docs[0].width) / NumCast(docs[0].zoomBasis, 1) + minx : minx;
+ let miny = docs.length ? NumCast(docs[0].y) : 0;
+ let maxy = docs.length ? NumCast(docs[0].height) / NumCast(docs[0].zoomBasis, 1) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
- let x = Cast(doc.x, "number", 0);
- let xe = x + Cast(doc.width, "number", 0);
- let y = Cast(doc.y, "number", 0);
- let ye = y + Cast(doc.height, "number", 0);
+ let x = NumCast(doc.x);
+ let xe = x + NumCast(doc.width) / NumCast(doc.zoomBasis, 1);
+ let y = NumCast(doc.y);
+ let ye = y + NumCast(doc.height) / NumCast(doc.zoomBasis, 1);
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
- let panelwidth = this._pwidth / this.zoomScaling() / 2;
- let panelheight = this._pheight / this.zoomScaling() / 2;
- if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx;
- if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx;
- if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy;
- if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;
+ let ink = Cast(this.props.Document.ink, InkField);
+ if (ink && ink.inkData) {
+ ink.inkData.forEach((value: StrokeData, key: string) => {
+ let bounds = InkingCanvas.StrokeRect(value);
+ ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+ ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+ });
+ }
+
+ let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(),
+ this._pheight / this.zoomScaling());
+ let panelwidth = panelDim[0];
+ let panelheight = panelDim[1];
+ if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
+ if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
+ if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
+ if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
}
this.setPan(x - dx, y - dy);
this._lastX = e.pageX;
@@ -175,7 +179,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// if (!this.props.active()) {
// return;
// }
- let childSelected = (this.children || []).filter(doc => doc).some(doc => {
+ let childSelected = this.childDocs.some(doc => {
var dv = DocumentManager.Instance.getDocumentView(doc);
return dv && SelectionManager.IsSelected(dv) ? true : false;
});
@@ -187,8 +191,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (e.ctrlKey) {
let deltaScale = (1 - (e.deltaY / coefficient));
- this.props.Document.nativeWidth = this.nativeWidth * deltaScale;
- this.props.Document.nativeHeight = this.nativeHeight * deltaScale;
+ let nw = this.nativeWidth * deltaScale;
+ let nh = this.nativeHeight * deltaScale;
+ if (nw && nh) {
+ this.props.Document.nativeWidth = nw;
+ this.props.Document.nativeHeight = nh;
+ }
e.stopPropagation();
e.preventDefault();
} else {
@@ -202,7 +210,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();
@@ -211,6 +219,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
setPan(panX: number, panY: number) {
+ 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));
@@ -228,33 +237,50 @@ 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;
return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
}).forEach((doc, index) => doc.zIndex = index + 1);
doc.zIndex = docs.length + 1;
- return doc;
}
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) / NumCast(doc.zoomBasis, 1) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / NumCast(doc.zoomBasis, 1) / 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
- }
}
getDocumentViewProps(document: Doc): DocumentViewProps {
return {
Document: document,
- toggleMinimized: emptyFunction,
addDocument: this.props.addDocument,
removeDocument: this.props.removeDocument,
moveDocument: this.props.moveDocument,
@@ -267,19 +293,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContainingCollectionView: this.props.CollectionView,
focus: this.focusDocument,
parentActive: this.props.active,
- whenActiveChanged: 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 || []).filter(doc => doc).reduce((prev, doc) => {
- if (!FieldValue(doc)) return prev;
+ 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");
+ if (Math.round(page) === Math.round(curPage) || page === -1) {
+ let minim = BoolCast(doc.isMinimized, false);
if (minim === undefined || !minim) {
prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
}
@@ -297,7 +324,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
}
- private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
+ private childViews = () => [
+ <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
+ ...this.views
+ ];
render() {
const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
const easing = () => this.props.Document.panTransformType === "Ease";
@@ -316,23 +346,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
{this.childViews}
</InkingCanvas>
</CollectionFreeFormLinksView>
- {/* <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" /> */}
+ <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
- <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
</MarqueeView>
- <Timeline {...this.props} />
- </div>
+<<<<<<< HEAD
+ <Timeline {...this.props} />
+=======
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+>>>>>>> 6f49d067b58caf6297f7ae7687cf05b627c27a1d
+ </div >
);
}
}
@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get overlayView() {
- let overlayLayout = Cast(this.props.Document.overlayLayout, "string", "");
- return !overlayLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- isTopMost={this.props.isTopMost} isSelected={returnFalse} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
}
render() {
return this.overlayView;
@@ -342,13 +373,11 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps> {
@observer
class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
@computed get backgroundView() {
- let backgroundLayout = Cast(this.props.Document.backgroundLayout, "string", "");
- return !backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ 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.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index ae0a9fd48..6e8ec8662 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -21,6 +21,6 @@
white-space:nowrap;
}
.marquee-legend::after {
- content: "Press: C (collection), or Delete"
+ content: "Press: c (collection), s (summary), r (replace) or Delete"
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8c81f6990..29734fa19 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,9 +1,10 @@
-import { action, computed, observable } from "mobx";
+import * as htmlToImage from "html-to-image";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Docs } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
@@ -13,8 +14,14 @@ import { Utils } from "../../../../Utils";
import { Doc } from "../../../../new_fields/Doc";
import { NumCast, Cast } from "../../../../new_fields/Types";
import { InkField, StrokeData } from "../../../../new_fields/InkField";
-import { Templates } from "../../Templates";
import { List } from "../../../../new_fields/List";
+import { ImageField } from "../../../../new_fields/URLField";
+import { Template, Templates } from "../../Templates";
+import { SearchBox } from "../../SearchBox";
+import { DocServer } from "../../../DocServer";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { CollectionView } from "../CollectionView";
+import { CollectionViewType } from "../CollectionBaseView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -31,6 +38,7 @@ interface MarqueeViewProps {
@observer
export class MarqueeView extends React.Component<MarqueeViewProps>
{
+ private _mainCont = React.createRef<HTMLDivElement>();
@observable _lastX: number = 0;
@observable _lastY: number = 0;
@observable _downX: number = 0;
@@ -48,28 +56,108 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._visible = false;
}
+ @undoBatch
@action
onKeyPress = (e: KeyboardEvent) => {
//make textbox and add it to this collection
let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
- let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
- this.props.addLiveTextDocument(newBox);
+ if (e.key === "q" && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ let text: string = await navigator.clipboard.readText();
+ 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(";") ||
+ ns[i].endsWith(".\r") || ns[i].endsWith(".") ||
+ ns[i].endsWith(":\r") || ns[i].endsWith(":")) && i < ns.length - 1) {
+ let sub = ns[i].endsWith("\r") ? 1 : 0;
+ let br = ns[i + 1].trim() === "";
+ ns.splice(i, 2, ns[i].substr(0, ns[i].length - sub) + ns[i + 1].trimLeft());
+ if (br) break;
+ }
+ }
+ ns.map(line => {
+ let indent = line.search(/\S|$/);
+ let newBox = Docs.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
+ this.props.addDocument(newBox, false);
+ y += 40 * this.props.getTransform().Scale;
+ });
+ })();
+ } else if (e.key === "b" && e.ctrlKey) {
+ e.preventDefault();
+ navigator.clipboard.readText().then(text => {
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ if (ns.length === 1 && text.startsWith("http")) {
+ this.props.addDocument(Docs.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ } else {
+ this.pasteTable(ns, x, y);
+ }
+ });
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
e.stopPropagation();
}
+ //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
+ // assumes that 1st row has header entry for each column
+ // assumes subsequent rows have entries for each column header OR
+ // any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
+ // assumes each cell is a string or a number
+ pasteTable(ns: string[], x: number, y: number) {
+ 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[] = [];
+ let groupAttr: string | number = "";
+ let rowProto = new Doc();
+ rowProto.title = rowProto.Id;
+ rowProto.width = 200;
+ rowProto.isPrototype = true;
+ for (let i = 1; i < ns.length - 1; i++) {
+ let values = ns[i].split("\t");
+ if (values.length === 1 && columns.length > 1) {
+ groupAttr = values[0];
+ continue;
+ }
+ let docDataProto = Doc.MakeDelegate(rowProto);
+ docDataProto.isPrototype = true;
+ columns.forEach((col, i) => docDataProto[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
+ if (groupAttr) {
+ docDataProto._group = groupAttr;
+ }
+ docDataProto.title = i.toString();
+ let doc = Doc.MakeDelegate(docDataProto);
+ doc.width = 200;
+ docList.push(doc);
+ }
+ let newCol = Docs.SchemaDocument([...(groupAttr ? ["_group"] : []), ...columns.filter(c => c)], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
+
+ this.props.addDocument(newCol, false);
+ }
+ }
@action
onPointerDown = (e: React.PointerEvent): void => {
this._downX = this._lastX = e.pageX;
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);
- }
- if (e.altKey) {
- e.preventDefault();
+ if (e.altKey) {
+ //e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
+ e.preventDefault();
+ }
+ // bcz: do we need this? it kills the context menu on the main collection if !altKey
+ // e.stopPropagation();
}
}
@@ -135,32 +223,39 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@undoBatch
@action
- marqueeCommand = (e: KeyboardEvent) => {
- if (this._commandExecuted) {
+ marqueeCommand = async (e: KeyboardEvent) => {
+ if (this._commandExecuted || (e as any).propagationIsStopped) {
return;
}
- if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") {
+ 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) {
this.marqueeInkDelete(ink.inkData);
}
+ SelectionManager.DeselectAll();
this.cleanupInteractions(false);
e.stopPropagation();
}
- if (e.key === "c" || e.key === "r" || e.key === "e") {
+ if (e.key === "c" || e.key === "s" || e.key === "S" || e.key === "e" || e.key === "p") {
this._commandExecuted = true;
e.stopPropagation();
+ e.preventDefault();
+ (e as any).propagationIsStopped = true;
let bounds = this.Bounds;
- let selected = this.marqueeSelect().map(d => {
- 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;
- });
+ 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 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);
@@ -170,51 +265,73 @@ 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" || e.key === "S" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
});
-
+ newCollection.zoomBasis = zoomBasis;
this.marqueeInkDelete(inkData);
- // SelectionManager.DeselectAll();
- if (e.key === "r") {
- let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
- summary.maximizedDocs = new List<Doc>(selected);
- // summary.doc1 = selected[0];
- // if (selected.length > 1)
- // summary.doc2 = selected[1];
- // summary.templates = new List<string>([Templates.Summary.Layout]);
- this.props.addLiveTextDocument(summary);
- e.preventDefault();
- let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
- selected.map(maximizedDoc => {
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0])
+
+ if (e.key === "s") {
+ 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: "#e2ad32" /* yellow */, title: "-summary-" });
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ summary.proto!.subBulletDocs = new List<Doc>(selected);
+ //summary.proto!.maximizeLocation = "inTab"; // or "inPlace", or "onRight"
+ summary.templates = new List<string>([Templates.Bullet.Layout]);
+ let container = Docs.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, title: "-summary-" });
+ container.viewType = CollectionViewType.Stacking;
+ this.props.addLiveTextDocument(container);
+ // });
+ } else if (e.key === "S") {
+ await htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 0.2 }).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: "#e2ad32" /* yellow */, title: "-summary-" });
+ SearchBox.convertDataUri(dataUrl, "icon" + summary[Id] + "_image").then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ let imageSummary = Docs.ImageDocument(url, {
+ x: bounds.left, y: bounds.top + 100 / zoomBasis,
+ width: 150, height: bounds.height / bounds.width * 150, title: "-summary image-"
+ });
+ summary.imageSummary = imageSummary;
+ this.props.addDocument(imageSummary, false);
+ }
+ })
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ 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);
});
}
else {
this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
}
- 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>) {
@@ -268,17 +385,21 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@computed
get marqueeDiv() {
- let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} >
+ return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
<span className="marquee-legend" />
</div>;
}
render() {
+ let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
- {this.props.children}
- {!this._visible ? (null) : this.marqueeDiv}
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
+ {this._visible ? this.marqueeDiv : null}
+ <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
+ {this.props.children}
+ </div>
+ </div>
</div>;
}
} \ No newline at end of file