aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/.DS_Storebin6148 -> 6148 bytes
-rw-r--r--src/client/views/ContextMenu.scss33
-rw-r--r--src/client/views/ContextMenu.tsx47
-rw-r--r--src/client/views/ContextMenuItem.tsx7
-rw-r--r--src/client/views/DocComponent.tsx14
-rw-r--r--src/client/views/DocumentDecorations.scss205
-rw-r--r--src/client/views/DocumentDecorations.tsx565
-rw-r--r--src/client/views/EditableView.scss20
-rw-r--r--src/client/views/EditableView.tsx34
-rw-r--r--src/client/views/InkingCanvas.scss63
-rw-r--r--src/client/views/InkingCanvas.tsx241
-rw-r--r--src/client/views/InkingControl.scss135
-rw-r--r--src/client/views/InkingControl.tsx79
-rw-r--r--src/client/views/InkingStroke.scss3
-rw-r--r--src/client/views/InkingStroke.tsx34
-rw-r--r--src/client/views/Main.scss191
-rw-r--r--src/client/views/Main.tsx358
-rw-r--r--src/client/views/MainOverlayTextBox.scss20
-rw-r--r--src/client/views/MainOverlayTextBox.tsx99
-rw-r--r--src/client/views/PresentationView.scss79
-rw-r--r--src/client/views/PresentationView.tsx150
-rw-r--r--src/client/views/PreviewCursor.scss9
-rw-r--r--src/client/views/PreviewCursor.tsx64
-rw-r--r--src/client/views/SearchBox.scss102
-rw-r--r--src/client/views/SearchBox.tsx178
-rw-r--r--src/client/views/SearchItem.tsx73
-rw-r--r--src/client/views/TemplateMenu.tsx90
-rw-r--r--src/client/views/Templates.tsx92
-rw-r--r--src/client/views/_nodeModuleOverrides.scss23
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx176
-rw-r--r--src/client/views/collections/CollectionDockingView.scss23
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx360
-rw-r--r--src/client/views/collections/CollectionFreeFormView.scss75
-rw-r--r--src/client/views/collections/CollectionFreeFormView.tsx394
-rw-r--r--src/client/views/collections/CollectionPDFView.scss49
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx102
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss200
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx454
-rw-r--r--src/client/views/collections/CollectionSubView.tsx231
-rw-r--r--src/client/views/collections/CollectionTreeView.scss107
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx308
-rw-r--r--src/client/views/collections/CollectionVideoView.scss42
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx89
-rw-r--r--src/client/views/collections/CollectionView.tsx146
-rw-r--r--src/client/views/collections/CollectionViewBase.tsx152
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss8
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx66
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx58
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx130
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss24
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx87
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss107
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx404
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss26
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx367
-rw-r--r--src/client/views/globalCssVariables.scss35
-rw-r--r--src/client/views/globalCssVariables.scss.d.ts10
-rw-r--r--src/client/views/nodes/Annotation.tsx128
-rw-r--r--src/client/views/nodes/AudioBox.tsx43
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx304
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx111
-rw-r--r--src/client/views/nodes/DocumentView.scss54
-rw-r--r--src/client/views/nodes/DocumentView.tsx517
-rw-r--r--src/client/views/nodes/FieldTextBox.scss14
-rw-r--r--src/client/views/nodes/FieldView.tsx127
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss80
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx374
-rw-r--r--src/client/views/nodes/IconBox.scss22
-rw-r--r--src/client/views/nodes/IconBox.tsx84
-rw-r--r--src/client/views/nodes/ImageBox.scss47
-rw-r--r--src/client/views/nodes/ImageBox.tsx149
-rw-r--r--src/client/views/nodes/KeyValueBox.scss116
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx114
-rw-r--r--src/client/views/nodes/KeyValuePair.scss37
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx106
-rw-r--r--src/client/views/nodes/LinkBox.scss43
-rw-r--r--src/client/views/nodes/LinkBox.tsx85
-rw-r--r--src/client/views/nodes/LinkEditor.scss23
-rw-r--r--src/client/views/nodes/LinkEditor.tsx21
-rw-r--r--src/client/views/nodes/LinkMenu.scss1
-rw-r--r--src/client/views/nodes/LinkMenu.tsx36
-rw-r--r--src/client/views/nodes/PDFBox.scss24
-rw-r--r--src/client/views/nodes/PDFBox.tsx407
-rw-r--r--src/client/views/nodes/Sticky.tsx83
-rw-r--r--src/client/views/nodes/VideoBox.scss8
-rw-r--r--src/client/views/nodes/VideoBox.tsx176
-rw-r--r--src/client/views/nodes/WebBox.scss25
-rw-r--r--src/client/views/nodes/WebBox.tsx67
90 files changed, 7844 insertions, 2844 deletions
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 6bd614c8b..5008ddfcf 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 8bc7708f7..3b1b18f88 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -1,9 +1,24 @@
+@import "globalCssVariables";
.contextMenu-cont {
- position: absolute;
- display: flex;
- z-index: 1000;
- box-shadow: #AAAAAA .2vw .2vw .4vw;
- flex-direction: column;
+ position: absolute;
+ display: flex;
+ z-index: $contextMenu-zindex;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+ flex-direction: column;
+}
+
+.contextMenu-item:first-child {
+ background: $intermediate-color;
+ color: $light-color;
+}
+
+.contextMenu-item:first-child::placeholder {
+ color: $light-color;
+}
+
+.contextMenu-item:first-child:hover {
+ background: $intermediate-color;
+ color: $light-color;
}
.subMenu-cont {
@@ -30,7 +45,7 @@
transition: all .1s;
border-width: .11px;
border-style: none;
- border-color: rgb(187, 186, 186);
+ border-color: $intermediate-color; // rgb(187, 186, 186);
border-bottom-style: solid;
padding: 10px;
white-space: nowrap;
@@ -38,8 +53,8 @@
}
.contextMenu-item:hover {
- transition: all .1s;
- background: #B0E0E6;
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
}
.contextMenu-description {
@@ -51,4 +66,4 @@
.icon-background {
background-color: #DDDDDD;
-} \ No newline at end of file
+}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 39ccb55b0..30ac23adf 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -11,13 +11,15 @@ library.add(faSearch);
@observer
export class ContextMenu extends React.Component {
- static Instance: ContextMenu
+ static Instance: ContextMenu;
@observable private _items: Array<ContextMenuProps> = [{ description: "test", event: (e: React.MouseEvent) => e.preventDefault(), icon: "smile" }];
@observable private _pageX: number = 0;
@observable private _pageY: number = 0;
@observable private _display: string = "none";
@observable private _searchString: string = "";
+ // afaik displaymenu can be called before all the items are added to the menu, so can't determine in displayMenu what the height of the menu will be
+ @observable private _yRelativeToTop: boolean = true;
private ref: React.RefObject<HTMLDivElement>;
@@ -25,15 +27,15 @@ export class ContextMenu extends React.Component {
constructor(props: Readonly<{}>) {
super(props);
- this.ref = React.createRef()
+ this.ref = React.createRef();
ContextMenu.Instance = this;
}
@action
clearItems() {
- this._items = []
- this._display = "none"
+ this._items = [];
+ this._display = "none";
}
@action
@@ -50,18 +52,28 @@ export class ContextMenu extends React.Component {
@action
displayMenu(x: number, y: number) {
- this._pageX = x
- this._pageY = y
+ //maxX and maxY will change if the UI/font size changes, but will work for any amount
+ //of items added to the menu
+ let maxX = window.innerWidth - 150;
+ let maxY = window.innerHeight - ((this._items.length + 1/*for search box*/) * 34 + 30);
+
+ this._pageX = x > maxX ? maxX : x;
+ this._pageY = y > maxY ? maxY : y;
this._searchString = "";
- this._display = "flex"
+ this._display = "flex";
}
intersects = (x: number, y: number): boolean => {
if (this.ref.current && this._display !== "none") {
- if (x >= this._pageX && x <= this._pageX + this.ref.current.getBoundingClientRect().width) {
- if (y >= this._pageY && y <= this._pageY + this.ref.current.getBoundingClientRect().height) {
+ let menuSize = { width: this.ref.current.getBoundingClientRect().width, height: this.ref.current.getBoundingClientRect().height };
+
+ let upperLeft = { x: this._pageX, y: this._yRelativeToTop ? this._pageY : window.innerHeight - (this._pageY + menuSize.height) };
+ let bottomRight = { x: this._pageX + menuSize.width, y: this._yRelativeToTop ? this._pageY + menuSize.height : window.innerHeight - this._pageY };
+
+ if (x >= upperLeft.x && x <= bottomRight.x) {
+ if (y >= upperLeft.y && y <= bottomRight.y) {
return true;
}
}
@@ -70,22 +82,23 @@ export class ContextMenu extends React.Component {
}
render() {
+ let style = this._yRelativeToTop ? { left: this._pageX, top: this._pageY, display: this._display } :
+ { left: this._pageX, bottom: this._pageY, display: this._display };
+
+
return (
- <div className="contextMenu-cont" style={{ left: this._pageX, top: this._pageY, display: this._display }} ref={this.ref}>
+ <div className="contextMenu-cont" style={style} ref={this.ref}>
<span>
<span className="icon-background">
<FontAwesomeIcon icon="circle" size="lg" />
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} ></input>
+ <input className="contextMenu-item" type="text" placeholder="Search . . ." value={this._searchString} onChange={this.onChange} />
</span>
- {this._items.filter(prop => {
- return prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1;
- }).map(prop => {
- return <ContextMenuItem {...prop} key={prop.description} />
- })}
+ {this._items.filter(prop => prop.description.toLowerCase().indexOf(this._searchString.toLowerCase()) !== -1).
+ map(prop => <ContextMenuItem {...prop} key={prop.description} />)}
</div>
- )
+ );
}
@action
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index fac7342aa..aefc633bc 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -15,6 +15,9 @@ export interface SubmenuProps {
subitems: ContextMenuProps[];
}
+export interface ContextMenuItemProps {
+ type: ContextMenuProps | SubmenuProps;
+}
export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
@observer
@@ -22,7 +25,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
@observable private _items: Array<ContextMenuProps> = [];
@observable private overItem = false;
- constructor(props: ContextMenuProps) {
+ constructor(props: ContextMenuProps | SubmenuProps) {
super(props);
if ("subitems" in this.props) {
this.props.subitems.forEach(i => this._items.push(i));
@@ -50,7 +53,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps> {
</div>)
}
return (
- <div className="contextMenu-item" onMouseEnter={action(() => {
+ <div className="contextMenu-item" onClick={this.props.event} onMouseEnter={action(() => {
this.overItem = true
})} onMouseLeave={action(() => this.overItem = false)}>
<div className="contextMenu-description"> {this.props.description}</div>
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
new file mode 100644
index 000000000..d6562492f
--- /dev/null
+++ b/src/client/views/DocComponent.tsx
@@ -0,0 +1,14 @@
+import * as React from 'react';
+import { Doc } from '../../new_fields/Doc';
+import { computed } from 'mobx';
+
+export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<P> {
+ //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+ @computed
+ get Document(): T {
+ return schemaCtor(this.props.Document);
+ }
+ }
+ return Component;
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index fb9091dfc..ba9f32d7d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,51 +1,226 @@
-#documentDecorations-container {
+@import "globalCssVariables";
+
+$linkGap : 3px;
+.documentDecorations {
position: absolute;
+}
+
+.documentDecorations-container {
+ z-index: $docDecorations-zindex;
+ position: absolute;
+ top: 0;
+ left: 0;
display: grid;
- z-index: 1000;
- grid-template-rows: 20px 1fr 20px 0px;
- grid-template-columns: 20px 1fr 20px;
+ grid-template-rows: 20px 8px 1fr 8px;
+ grid-template-columns: 8px 16px 1fr 8px 8px;
pointer-events: none;
+
#documentDecorations-centerCont {
+ grid-column: 3;
background: none;
}
+
.documentDecorations-resizer {
pointer-events: auto;
- background: lightblue;
- opacity: 0.4;
+ background: $alt-accent;
+ opacity: 0.8;
}
+
+ #documentDecorations-topLeftResizer,
+ #documentDecorations-leftResizer,
+ #documentDecorations-bottomLeftResizer {
+ grid-column: 1
+ }
+
+ #documentDecorations-topResizer,
+ #documentDecorations-bottomResizer {
+ grid-column-start: 2;
+ grid-column-end: 5;
+ }
+
+ #documentDecorations-bottomRightResizer,
+ #documentDecorations-topRightResizer,
+ #documentDecorations-rightResizer {
+ grid-column-start: 5;
+ grid-column-end: 7;
+ }
+
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
}
+
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
}
+
#documentDecorations-topResizer,
#documentDecorations-bottomResizer {
cursor: ns-resize;
}
+
#documentDecorations-leftResizer,
#documentDecorations-rightResizer {
cursor: ew-resize;
}
-
+ .title{
+ background: $alt-accent;
+ grid-column-start: 3;
+ grid-column-end: 4;
+ pointer-events: auto;
+ overflow: hidden;
+ }
+}
+
+
+.documentDecorations-closeButton {
+ background: $alt-accent;
+ opacity: 0.8;
+ grid-column-start: 4;
+ grid-column-end: 6;
+ pointer-events: all;
+ text-align: center;
+ cursor: pointer;
+}
+
+.documentDecorations-minimizeButton {
+ background: $alt-accent;
+ opacity: 0.8;
+ grid-column-start: 1;
+ grid-column-end: 3;
+ pointer-events: all;
+ text-align: center;
+ cursor: pointer;
+ position: absolute;
+ left: 0px;
+ top: 0px;
+ width: $MINIMIZED_ICON_SIZE;
+ height: $MINIMIZED_ICON_SIZE;
+}
+
+.documentDecorations-background {
+ background: lightblue;
+ position: absolute;
+ opacity: 0.1;
}
-.linkButton-empty {
+
+.linkFlyout {
+ grid-column: 2/4;
+ margin-top: $linkGap;
+}
+
+.linkButton-empty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.linkButton-nonempty:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.link-button-container {
+ grid-column: 1/4;
+ width: auto;
+ height: auto;
+ display: flex;
+ flex-direction: row;
+}
+
+.linkButton-linker {
+ margin-left: 5px;
+ margin-top: $linkGap;
height: 20px;
width: 20px;
- margin-top: 10px;
+ text-align: center;
border-radius: 50%;
- opacity: 0.6;
pointer-events: auto;
- background-color: #2B6091;
+ color: $dark-color;
+ border: $dark-color 1px solid;
+}
+
+.linkButton-linker:hover {
+ cursor: pointer;
+ transform: scale(1.05);
}
-.linkButton-nonempty {
+
+.linkButton-empty, .linkButton-nonempty {
height: 20px;
width: 20px;
- margin-top: 10px;
border-radius: 50%;
- opacity: 0.6;
+ opacity: 0.9;
pointer-events: auto;
- background-color: rgb(35, 165, 42);
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
+}
+
+.templating-menu {
+ position: absolute;
+ bottom: 0;
+ left: 50px;
+ pointer-events: auto;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.fa-icon-link {
+ margin-top: 3px;
+}
+.templating-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ font-size:14;
+ background-color: $dark-color;
+ color: $light-color;
+ text-align: center;
+ cursor: pointer;
+
+ &:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ }
+}
+
+#template-list {
+ position: absolute;
+ top: 0;
+ left: 30px;
+ width: max-content;
+ font-family: $sans-serif;
+ font-size: 12px;
+ background-color: $light-color-secondary;
+ padding: 2px 12px;
+ list-style: none;
+ .templateToggle {
+ text-align: left;
+ }
+
+ input {
+ margin-right: 10px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index dc62f97cf..7083b1003 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,30 +1,121 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../util/SelectionManager";
+import { action, computed, observable, runInAction, untracked, reaction } from "mobx";
import { observer } from "mobx-react";
-import './DocumentDecorations.scss'
-import { KeyStore } from '../../fields/KeyStore'
-import { NumberField } from "../../fields/NumberField";
-import { props } from "bluebird";
-import { DragManager } from "../util/DragManager";
+import { emptyFunction, Utils } from "../../Utils";
+import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { undoBatch } from "../util/UndoManager";
+import './DocumentDecorations.scss';
+import { DocumentView, PositionDocument } from "./nodes/DocumentView";
import { LinkMenu } from "./nodes/LinkMenu";
-import { ListField } from "../../fields/ListField";
+import { TemplateMenu } from "./TemplateMenu";
+import React = require("react");
+import { Template, Templates } from "./Templates";
+import { CompileScript } from "../util/Scripting";
+import { IconBox } from "./nodes/IconBox";
+import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types";
+import { Doc, FieldResult } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Docs } from "../documents/Documents";
+import { List } from "../../new_fields/List";
const higflyout = require("@hig/flyout");
-const { anchorPoints } = higflyout;
-const Flyout = higflyout.default;
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+import { faLink } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
+import { CollectionView } from "./collections/CollectionView";
+import { DocumentManager } from "../util/DocumentManager";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FieldView } from "./nodes/FieldView";
+
+library.add(faLink);
@observer
-export class DocumentDecorations extends React.Component {
- static Instance: DocumentDecorations
- private _resizer = ""
+export class DocumentDecorations extends React.Component<{}, { value: string }> {
+ static Instance: DocumentDecorations;
private _isPointerDown = false;
+ private _resizing = "";
+ private keyinput: React.RefObject<HTMLInputElement>;
+ private _resizeBorderWidth = 16;
+ private _linkBoxHeight = 20 + 3; // link button height + margin
+ private _titleHeight = 20;
private _linkButton = React.createRef<HTMLDivElement>();
+ private _linkerButton = React.createRef<HTMLDivElement>();
+ private _downX = 0;
+ private _downY = 0;
+ private _iconDoc?: Doc = undefined;
+ @observable private _minimizedX = 0;
+ @observable private _minimizedY = 0;
+ @observable private _title: string = "";
+ @observable private _edtingTitle = false;
+ @observable private _fieldKey = "title";
@observable private _hidden = false;
+ @observable private _opacity = 1;
+ @observable private _removeIcon = false;
+ @observable public Interacting = false;
constructor(props: Readonly<{}>) {
- super(props)
+ super(props);
+ DocumentDecorations.Instance = this;
+ this.keyinput = React.createRef();
+ reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false);
+ }
- DocumentDecorations.Instance = this
+ @action titleChanged = (event: any) => { this._title = event.target.value; };
+ @action titleBlur = () => { this._edtingTitle = false; };
+ @action titleEntered = (e: any) => {
+ var key = e.keyCode || e.which;
+ // enter pressed
+ if (key === 13) {
+ var text = e.target.value;
+ if (text[0] === '#') {
+ this._fieldKey = text.slice(1, text.length);
+ this._title = this.selectionTitle;
+ }
+ else {
+ if (SelectionManager.SelectedDocuments().length > 0) {
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "number") {
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = +this._title;
+ });
+ } else {
+ SelectionManager.SelectedDocuments().forEach(d => {
+ let doc = d.props.Document.proto ? d.props.Document.proto : d.props.Document;
+ doc[this._fieldKey] = this._title;
+ });
+ }
+ }
+ }
+ e.target.blur();
+ }
+ }
+ @action onTitleDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onTitleMove);
+ document.removeEventListener("pointerup", this.onTitleUp);
+ document.addEventListener("pointermove", this.onTitleMove);
+ document.addEventListener("pointerup", this.onTitleUp);
+ }
+ @action onTitleMove = (e: PointerEvent): void => {
+ if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
+ this.Interacting = true;
+ }
+ if (this.Interacting) this.onBackgroundMove(e);
+ e.stopPropagation();
+ }
+ @action onTitleUp = (e: PointerEvent): void => {
+ if (Math.abs(e.clientX - this._downX) < 4 || Math.abs(e.clientY - this._downY) < 4) {
+ this._title = this.selectionTitle;
+ this._edtingTitle = true;
+ }
+ document.removeEventListener("pointermove", this.onTitleMove);
+ document.removeEventListener("pointerup", this.onTitleUp);
+ this.onBackgroundUp(e);
}
@computed
@@ -39,20 +130,192 @@ export class DocumentDecorations extends React.Component {
return {
x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- }
+ };
}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
}
+ onBackgroundDown = (e: React.PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ document.addEventListener("pointermove", this.onBackgroundMove);
+ document.addEventListener("pointerup", this.onBackgroundUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ @action
+ onBackgroundMove = (e: PointerEvent): void => {
+ let dragDocView = SelectionManager.SelectedDocuments()[0];
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
+ const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
+ let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+ dragData.xOffset = xoff;
+ dragData.yOffset = yoff;
+ dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
+ this.Interacting = true;
+ this._hidden = true;
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ document.removeEventListener("pointermove", this.onTitleMove);
+ document.removeEventListener("pointerup", this.onTitleUp);
+ DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, {
+ handlers: { dragComplete: action(() => this._hidden = this.Interacting = false) },
+ hideSource: true
+ });
+ e.stopPropagation();
+ }
- @computed
- public get Hidden() { return this._hidden; }
- public set Hidden(value: boolean) { this._hidden = value; }
+ @action
+ onBackgroundUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onBackgroundMove);
+ document.removeEventListener("pointerup", this.onBackgroundUp);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ onCloseDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ document.removeEventListener("pointermove", this.onCloseMove);
+ document.addEventListener("pointermove", this.onCloseMove);
+ document.removeEventListener("pointerup", this.onCloseUp);
+ document.addEventListener("pointerup", this.onCloseUp);
+ }
+ }
+ onCloseMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ }
+ }
+ @undoBatch
+ @action
+ onCloseUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document));
+ SelectionManager.DeselectAll();
+ document.removeEventListener("pointermove", this.onCloseMove);
+ document.removeEventListener("pointerup", this.onCloseUp);
+ }
+ }
+ @action
+ onMinimizeDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ this._iconDoc = undefined;
+ if (e.button === 0) {
+ this._downX = e.pageX;
+ this._downY = e.pageY;
+ this._removeIcon = false;
+ let selDoc = SelectionManager.SelectedDocuments()[0];
+ let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
+ this._minimizedX = selDocPos[0] + 12;
+ this._minimizedY = selDocPos[1] + 12;
+ document.removeEventListener("pointermove", this.onMinimizeMove);
+ document.addEventListener("pointermove", this.onMinimizeMove);
+ document.removeEventListener("pointerup", this.onMinimizeUp);
+ document.addEventListener("pointerup", this.onMinimizeUp);
+ }
+ }
+
+ @action
+ onMinimizeMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD ||
+ Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) {
+ let selDoc = SelectionManager.SelectedDocuments()[0];
+ let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0);
+ let snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20;
+ this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX;
+ this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY;
+ let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
+
+ if (selectedDocs.length > 1) {
+ this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+ this.moveIconDoc(this._iconDoc);
+ } else {
+ this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
+ }
+ this._removeIcon = snapped;
+ }
+ }
+ @undoBatch
+ @action
+ onMinimizeUp = (e: PointerEvent): void => {
+ e.stopPropagation();
+ if (e.button === 0) {
+ document.removeEventListener("pointermove", this.onMinimizeMove);
+ document.removeEventListener("pointerup", this.onMinimizeUp);
+ let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
+ if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
+ selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
+ }
+ if (!this._removeIcon) {
+ if (selectedDocs.length === 1) {
+ this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].props.toggleMinimized());
+ } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
+ let docViews = SelectionManager.ViewsSortedVertically();
+ let topDocView = docViews[0];
+ let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
+ if (ind !== -1) {
+ topDocView.templates.splice(ind, 1);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ topDocView.addTemplate(Templates.Bullet);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ }
+ }
+ }
+ this._removeIcon = false;
+ }
+ runInAction(() => this._minimizedX = this._minimizedY = 0);
+ }
+
+ @undoBatch
+ @action createIcon = (selected: DocumentView[], layoutString: string): Doc => {
+ let doc = selected[0].props.Document;
+ let iconDoc = Docs.IconDocument(layoutString);
+ iconDoc.isButton = true;
+ iconDoc.proto!.title = selected.length > 1 ? "ICONset" : "ICON" + StrCast(doc.title);
+ iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
+ iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
+ iconDoc.isMinimized = false;
+ iconDoc.width = Number(MINIMIZED_ICON_SIZE);
+ iconDoc.height = Number(MINIMIZED_ICON_SIZE);
+ iconDoc.x = NumCast(doc.x);
+ iconDoc.y = NumCast(doc.y) - 24;
+ iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document.proto!));
+ doc.minimizedDoc = iconDoc;
+ selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
+ return iconDoc;
+ }
+ @action
+ public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => {
+ let doc = docView.props.Document;
+ let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
+
+ if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
+ const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
+ iconDoc = this.createIcon([docView], layout);
+ }
+ return iconDoc;
+ }
+ moveIconDoc(iconDoc: Doc) {
+ let selView = SelectionManager.SelectedDocuments()[0];
+ let zoom = NumCast(selView.props.Document.zoomBasis, 1);
+ let where = (selView.props.ScreenToLocalTransform()).scale(selView.props.ContentScaling()).scale(1 / zoom).
+ transformPoint(this._minimizedX - 12, this._minimizedY - 12);
+ iconDoc.x = where[0] + NumCast(selView.props.Document.x);
+ iconDoc.y = where[1] + NumCast(selView.props.Document.y);
+ }
+
+ @action
onPointerDown = (e: React.PointerEvent): void => {
e.stopPropagation();
if (e.button === 0) {
this._isPointerDown = true;
- this._resizer = e.currentTarget.id;
+ this._resizing = e.currentTarget.id;
+ this.Interacting = true;
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -60,46 +323,62 @@ export class DocumentDecorations extends React.Component {
}
}
- onLinkButtonDown = (e: React.PointerEvent): void => {
- // if ()
- // let linkMenu = new LinkMenu(SelectionManager.SelectedDocuments()[0]);
- // linkMenu.Hidden = false;
- console.log("down");
+ onLinkerButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.addEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ document.addEventListener("pointerup", this.onLinkerButtonUp);
+ }
+ onLinkerButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ e.stopPropagation();
+ }
+ @action
+ onLinkerButtonMoved = (e: PointerEvent): void => {
+ if (this._linkerButton.current !== null) {
+ document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkerButtonUp);
+ let selDoc = SelectionManager.SelectedDocuments()[0];
+ let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined;
+ let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
+ FormattedTextBox.InputBoxOverlay = undefined;
+ DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
e.stopPropagation();
- document.removeEventListener("pointermove", this.onLinkButtonMoved)
+ }
+
+ onLinkButtonDown = (e: React.PointerEvent): void => {
+ e.stopPropagation();
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
document.addEventListener("pointermove", this.onLinkButtonMoved);
- document.removeEventListener("pointerup", this.onLinkButtonUp)
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
document.addEventListener("pointerup", this.onLinkButtonUp);
-
}
onLinkButtonUp = (e: PointerEvent): void => {
- console.log("up");
- document.removeEventListener("pointermove", this.onLinkButtonMoved)
- document.removeEventListener("pointerup", this.onLinkButtonUp)
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
e.stopPropagation();
}
+ onLinkButtonMoved = async (e: PointerEvent) => {
+ if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+ document.removeEventListener("pointermove", this.onLinkButtonMoved);
+ document.removeEventListener("pointerup", this.onLinkButtonUp);
- onLinkButtonMoved = (e: PointerEvent): void => {
- console.log("moved");
- let dragData: { [id: string]: any } = {};
- dragData["linkSourceDoc"] = SelectionManager.SelectedDocuments()[0];
- if (this._linkButton.current != null) {
- DragManager.StartDrag(this._linkButton.current, dragData, {
- handlers: {
- dragComplete: action(() => { }),
- },
- hideSource: false
- })
+ DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);
}
- document.removeEventListener("pointermove", this.onLinkButtonMoved)
- document.removeEventListener("pointerup", this.onLinkButtonUp)
e.stopPropagation();
}
-
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
@@ -109,75 +388,84 @@ export class DocumentDecorations extends React.Component {
let dX = 0, dY = 0, dW = 0, dH = 0;
- switch (this._resizer) {
+ switch (this._resizing) {
case "":
break;
case "documentDecorations-topLeftResizer":
- dX = -1
- dY = -1
- dW = -(e.movementX)
- dH = -(e.movementY)
+ dX = -1;
+ dY = -1;
+ dW = -(e.movementX);
+ dH = -(e.movementY);
break;
case "documentDecorations-topRightResizer":
- dW = e.movementX
- dY = -1
- dH = -(e.movementY)
+ dW = e.movementX;
+ dY = -1;
+ dH = -(e.movementY);
break;
case "documentDecorations-topResizer":
- dY = -1
- dH = -(e.movementY)
+ dY = -1;
+ dH = -(e.movementY);
break;
case "documentDecorations-bottomLeftResizer":
- dX = -1
- dW = -(e.movementX)
- dH = e.movementY
+ dX = -1;
+ dW = -(e.movementX);
+ dH = e.movementY;
break;
case "documentDecorations-bottomRightResizer":
- dW = e.movementX
- dH = e.movementY
+ dW = e.movementX;
+ dH = e.movementY;
break;
case "documentDecorations-bottomResizer":
- dH = e.movementY
+ dH = e.movementY;
break;
case "documentDecorations-leftResizer":
- dX = -1
- dW = -(e.movementX)
+ dX = -1;
+ dW = -(e.movementX);
break;
case "documentDecorations-rightResizer":
- dW = e.movementX
+ dW = e.movementX;
break;
}
+ runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
- const rect = element.screenRect();
+ const rect = element.ContentDiv ? element.ContentDiv.getBoundingClientRect() : new DOMRect();
+
if (rect.width !== 0) {
- let doc = element.props.Document;
- let width = doc.GetNumber(KeyStore.Width, 0);
- let nwidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- let nheight = doc.GetNumber(KeyStore.NativeHeight, 0);
- let height = doc.GetNumber(KeyStore.Height, nwidth ? nheight / nwidth * width : 0);
- let x = doc.GetOrCreate(KeyStore.X, NumberField);
- let y = doc.GetOrCreate(KeyStore.Y, NumberField);
+ let doc = PositionDocument(element.props.Document);
+ let width = FieldValue(doc.width, 0);
+ let nwidth = FieldValue(doc.nativeWidth, 0);
+ let nheight = FieldValue(doc.nativeHeight, 0);
+ let height = FieldValue(doc.height, nwidth ? nheight / nwidth * width : 0);
+ let x = FieldValue(doc.x, 0);
+ let y = FieldValue(doc.y, 0);
let scale = width / rect.width;
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
- x.Data += dX * (actualdW - width);
- y.Data += dY * (actualdH - height);
- var nativeWidth = doc.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = doc.GetNumber(KeyStore.NativeHeight, 0);
+ x += dX * (actualdW - width);
+ y += dY * (actualdH - height);
+ doc.x = x;
+ doc.y = y;
+ var nativeWidth = FieldValue(doc.nativeWidth, 0);
+ var nativeHeight = FieldValue(doc.nativeHeight, 0);
if (nativeWidth > 0 && nativeHeight > 0) {
- if (Math.abs(dW) > Math.abs(dH))
+ if (Math.abs(dW) > Math.abs(dH)) {
actualdH = nativeHeight / nativeWidth * actualdW;
+ }
else actualdW = nativeWidth / nativeHeight * actualdH;
}
- doc.SetNumber(KeyStore.Width, actualdW);
- doc.SetNumber(KeyStore.Height, actualdH);
+ doc.width = actualdW;
+ doc.height = actualdH;
}
- })
+ });
}
+ @action
onPointerUp = (e: PointerEvent): void => {
e.stopPropagation();
+ this._resizing = "";
+ this.Interacting = false;
+ SelectionManager.ReselectAll();
if (e.button === 0) {
e.preventDefault();
this._isPointerDown = false;
@@ -186,6 +474,22 @@ export class DocumentDecorations extends React.Component {
}
}
+ @computed
+ get selectionTitle(): string {
+ if (SelectionManager.SelectedDocuments().length === 1) {
+ let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
+ if (typeof field === "string") {
+ return field;
+ }
+ else if (typeof field === "number") {
+ return field.toString();
+ }
+ } else if (SelectionManager.SelectedDocuments().length > 1) {
+ return "-multiple-";
+ }
+ return "-unset-";
+ }
+
changeFlyoutContent = (): void => {
}
@@ -195,33 +499,79 @@ export class DocumentDecorations extends React.Component {
render() {
var bounds = this.Bounds;
- if (this.Hidden) {
- return (null);
- }
- if (isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
- console.log("DocumentDecorations: Bounds Error")
+ let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
+ if (bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
+ let minimizeIcon = (
+ <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
+ {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
+ </div>);
let linkButton = null;
if (SelectionManager.SelectedDocuments().length > 0) {
let selFirst = SelectionManager.SelectedDocuments()[0];
+ let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
+ let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
+ let linkCount = linkToSize + linkFromSize;
linkButton = (<Flyout
anchorPoint={anchorPoints.RIGHT_TOP}
- content={
- <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}>
- </LinkMenu>
- }>
- <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} ref={this._linkButton} />
- </Flyout>);
- }
- return (
- <div id="documentDecorations-container" style={{
- width: (bounds.r - bounds.x + 40) + "px",
- height: (bounds.b - bounds.y + 40) + "px",
- left: bounds.x - 20,
- top: bounds.y - 20,
+ content={<LinkMenu docView={selFirst}
+ changeFlyout={this.changeFlyoutContent} />}>
+ <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
+ </Flyout >);
+ }
+
+ let templates: Map<Template, boolean> = new Map();
+ Array.from(Object.values(Templates.TemplateList)).map(template => {
+ let sorted = SelectionManager.ViewsSortedVertically().slice().sort((doc1, doc2) => {
+ if (NumCast(doc1.props.Document.x) > NumCast(doc2.props.Document.x)) return 1;
+ if (NumCast(doc1.props.Document.x) < NumCast(doc2.props.Document.x)) return -1;
+ return 0;
+ });
+ let docTemps = sorted.reduce((res: string[], doc: DocumentView, i) => {
+ let temps = doc.props.Document.templates;
+ if (temps instanceof List) {
+ temps.map(temp => {
+ if (temp !== Templates.Bullet.Layout || i === 0) {
+ res.push(temp);
+ }
+ });
+ }
+ return res;
+ }, [] as string[]);
+ let checked = false;
+ docTemps.forEach(temp => {
+ if (template.Layout === temp) {
+ checked = true;
+ }
+ });
+ templates.set(template, checked);
+ });
+
+ return (<div className="documentDecorations">
+ <div className="documentDecorations-background" style={{
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2,
+ pointerEvents: this.Interacting ? "none" : "all",
+ zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+ }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
+ </div>
+ <div className="documentDecorations-container" style={{
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
+ opacity: this._opacity
}}>
+ {minimizeIcon}
+
+ {this._edtingTitle ?
+ <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
+ <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
+ <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>
<div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
@@ -231,10 +581,19 @@ export class DocumentDecorations extends React.Component {
<div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
- {linkButton}
-
+ <div className="link-button-container">
+ <div className="linkButtonWrapper">
+ <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton} </div>
+ </div>
+ <div className="linkButtonWrapper">
+ <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
+ <FontAwesomeIcon className="fa-icon-link" icon="link" size="sm" />
+ </div>
+ </div>
+ <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
+ </div>
</div >
- )
+ </div>
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
new file mode 100644
index 000000000..dfa110f8d
--- /dev/null
+++ b/src/client/views/EditableView.scss
@@ -0,0 +1,20 @@
+.editableView-container-editing, .editableView-container-editing-oneLine {
+ overflow-wrap: break-word;
+ word-wrap: break-word;
+ hyphens: auto;
+ overflow: hidden;
+}
+.editableView-container-editing-oneLine {
+ span {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display:block;
+ }
+ input {
+ display:block;
+ }
+}
+.editableView-input {
+ width: 100%;
+} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 84b1b91c3..78143ccda 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,6 +1,7 @@
-import React = require('react')
+import React = require('react');
import { observer } from 'mobx-react';
-import { observable, action } from 'mobx';
+import { observable, action, trace } from 'mobx';
+import "./EditableView.scss";
export interface EditableProps {
/**
@@ -15,11 +16,15 @@ export interface EditableProps {
* */
SetValue(value: string): boolean;
+ OnFillDown?(value: string): void;
+
/**
* The contents to render when not editing
*/
contents: any;
- height: number
+ height: number;
+ display?: string;
+ oneLine?: boolean;
}
/**
@@ -34,26 +39,31 @@ export class EditableView extends React.Component<EditableProps> {
@action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key == "Enter" && !e.ctrlKey) {
- if (this.props.SetValue(e.currentTarget.value)) {
+ if (e.key === "Enter") {
+ if (!e.ctrlKey) {
+ if (this.props.SetValue(e.currentTarget.value)) {
+ this.editing = false;
+ }
+ } else if (this.props.OnFillDown) {
+ this.props.OnFillDown(e.currentTarget.value);
this.editing = false;
}
- } else if (e.key == "Escape") {
+ } else if (e.key === "Escape") {
this.editing = false;
}
}
render() {
if (this.editing) {
- return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
- style={{ display: "inline" }}></input>
+ return <input className="editableView-input" defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)}
+ style={{ display: this.props.display }} />;
} else {
return (
- <div className="editableView-container-editing" style={{ display: "inline", height: "100%", maxHeight: `${this.props.height}` }}
- onClick={action(() => this.editing = true)}>
- {this.props.contents}
+ <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} style={{ display: this.props.display, height: "auto", maxHeight: `${this.props.height}` }}
+ onClick={action(() => this.editing = true)} >
+ <span>{this.props.contents}</span>
</div>
- )
+ );
}
}
} \ No newline at end of file
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index f654b194b..2c550051c 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,32 +1,35 @@
-.inking-canvas {
- position: fixed;
- top: -50000px;
- left: -50000px; // z-index: 99; //overlays ink on top of everything
- svg {
- width: 100000px;
- height: 100000px;
- .highlight {
- mix-blend-mode: multiply;
- }
- }
-}
+@import "globalCssVariables";
-.inking-control {
+.inkingCanvas {
+ opacity:0.99;
+}
+.inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect {
position: absolute;
- right: 0;
- bottom: 75px;
- text-align: right;
- .ink-panel {
- margin-top: 12px;
- &:first {
- margin-top: 0;
- }
- }
- .ink-size {
- display: flex;
- justify-content: space-between;
- input {
- width: 85%;
- }
- }
-} \ No newline at end of file
+ top: 0;
+ left:0;
+ width: 8192px;
+ height: 8192px;
+ cursor:"crosshair";
+ pointer-events: auto;
+
+}
+.inkingCanvas-canSelect,
+.inkingCanvas-noSelect {
+ top:-50000px;
+ left:-50000px;
+ width: 100000px;
+ height: 100000px;
+}
+.inkingCanvas-noSelect {
+ pointer-events: none;
+ cursor: "arrow";
+}
+.inkingCanvas-paths-ink, .inkingCanvas-paths-markers {
+ pointer-events: none;
+ z-index: 10000; // overlays ink on top of everything
+ cursor: "arrow";
+}
+.inkingCanvas-paths-markers {
+ mix-blend-mode: multiply;
+}
+
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 0d87c1239..afe3e3ecb 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,120 +1,140 @@
+import { action, computed, trace, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { action, computed } from "mobx";
-import { InkingControl } from "./InkingControl";
-import React = require("react");
+import { Utils } from "../../Utils";
import { Transform } from "../util/Transform";
-import { Document } from "../../fields/Document";
-import { KeyStore } from "../../fields/KeyStore";
-import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { JsxArgs } from "./nodes/DocumentView";
+import "./InkingCanvas.scss";
+import { InkingControl } from "./InkingControl";
import { InkingStroke } from "./InkingStroke";
-import "./InkingCanvas.scss"
-import { CollectionDockingView } from "./collections/CollectionDockingView";
-import { Utils } from "../../Utils";
-import { FieldWaiting } from "../../fields/Field";
-import { getMapLikeKeys } from "mobx/lib/internal";
-
+import React = require("react");
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { StrokeData, InkField, InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+import { Cast, PromiseValue, NumCast } from "../../new_fields/Types";
interface InkCanvasProps {
getScreenTransform: () => Transform;
- Document: Document;
+ Document: Doc;
+ children: () => JSX.Element[];
}
@observer
export class InkingCanvas extends React.Component<InkCanvasProps> {
-
- private _isDrawing: boolean = false;
- private _idGenerator: string = "";
-
- constructor(props: Readonly<InkCanvasProps>) {
- super(props);
+ maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome
+ @observable inkMidX: number = 0;
+ @observable inkMidY: number = 0;
+ private previousState?: Map<string, StrokeData>;
+ private _currentStrokeId: string = "";
+ public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean {
+ return stroke.pathData.reduce((inside: boolean, val) => inside ||
+ (selRect.left < val.x && selRect.left + selRect.width > val.x &&
+ selRect.top < val.y && selRect.top + selRect.height > val.y)
+ , false);
}
-
- @computed
- get inkData(): StrokeMap {
- let map = this.props.Document.GetT(KeyStore.Ink, InkField);
- if (!map || map === FieldWaiting) {
- return new Map;
- }
- return new Map(map.Data);
- }
-
- set inkData(value: StrokeMap) {
- this.props.Document.SetData(KeyStore.Ink, value, InkField);
+ public static StrokeRect(stroke: StrokeData): { left: number, top: number, right: number, bottom: number } {
+ return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) =>
+ ({
+ left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y),
+ right: Math.max(bounds.right, val.x), bottom: Math.max(bounds.bottom, val.y)
+ })
+ , { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: -Number.MAX_VALUE, bottom: -Number.MAX_VALUE });
}
componentDidMount() {
- document.addEventListener("mouseup", this.handleMouseUp);
+ PromiseValue(Cast(this.props.Document.ink, InkField)).then(ink => runInAction(() => {
+ if (ink) {
+ let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) =>
+ strokeData.pathData.reduce(([mix, max, miy, may], p) =>
+ [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)],
+ [mix, max, miy, may]),
+ [Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]);
+ this.inkMidX = (bounds[0] + bounds[1]) / 2;
+ this.inkMidY = (bounds[2] + bounds[3]) / 2;
+ }
+ }));
}
- componentWillUnmount() {
- document.removeEventListener("mouseup", this.handleMouseUp);
+ @computed
+ get inkData(): Map<string, StrokeData> {
+ let map = Cast(this.props.Document.ink, InkField);
+ return !map ? new Map : new Map(map.inkData);
}
+ set inkData(value: Map<string, StrokeData>) {
+ Doc.SetOnPrototype(this.props.Document, "ink", new InkField(value));
+ }
@action
- handleMouseDown = (e: React.PointerEvent): void => {
- if (e.button != 0 ||
- InkingControl.Instance.selectedTool === InkTool.None) {
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button !== 0 || e.altKey || e.ctrlKey || InkingControl.Instance.selectedTool === InkTool.None) {
return;
}
- e.stopPropagation()
- if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
- return
- }
- e.stopPropagation()
- const point = this.relativeCoordinatesForEvent(e);
- // start the new line, saves a uuid to represent the field of the stroke
- this._idGenerator = Utils.GenerateGuid();
- let data = this.inkData;
- data.set(this._idGenerator,
- {
- pathData: [point],
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ e.stopPropagation();
+ e.preventDefault();
+
+ this.previousState = this.inkData;
+
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ // start the new line, saves a uuid to represent the field of the stroke
+ this._currentStrokeId = Utils.GenerateGuid();
+ const data = this.inkData;
+ data.set(this._currentStrokeId, {
+ pathData: [this.relativeCoordinatesForEvent(e.clientX, e.clientY)],
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: this.props.Document.GetNumber(KeyStore.CurPage, 0)
+ page: NumCast(this.props.Document.curPage, -1)
});
- this.inkData = data;
- this._isDrawing = true;
+ this.inkData = data;
+ }
}
@action
- handleMouseMove = (e: React.PointerEvent): void => {
- if (!this._isDrawing ||
- InkingControl.Instance.selectedTool === InkTool.None) {
- return;
- }
- e.stopPropagation()
- if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
- return
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ let coord = this.relativeCoordinatesForEvent(e.clientX, e.clientY);
+ if (Math.abs(coord.x - this.inkMidX) > 500 || Math.abs(coord.y - this.inkMidY) > 500) {
+ this.inkMidX = coord.x;
+ this.inkMidY = coord.y;
}
- const point = this.relativeCoordinatesForEvent(e);
-
- // add points to new line as it is being drawn
- let data = this.inkData;
- let strokeData = data.get(this._idGenerator);
- if (strokeData) {
- strokeData.pathData.push(point);
- data.set(this._idGenerator, strokeData);
- }
-
- this.inkData = data;
+ e.stopPropagation();
+ e.preventDefault();
+
+ const batch = UndoManager.StartBatch("One ink stroke");
+ const oldState = this.previousState || new Map;
+ this.previousState = undefined;
+ const newState = this.inkData;
+ UndoManager.AddEvent({
+ undo: () => this.inkData = oldState,
+ redo: () => this.inkData = newState,
+ });
+ batch.end();
}
@action
- handleMouseUp = (e: MouseEvent): void => {
- this._isDrawing = false;
+ onPointerMove = (e: PointerEvent): void => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ let data = this.inkData; // add points to new line as it is being drawn
+ let strokeData = data.get(this._currentStrokeId);
+ if (strokeData) {
+ strokeData.pathData.push(this.relativeCoordinatesForEvent(e.clientX, e.clientY));
+ data.set(this._currentStrokeId, strokeData);
+ }
+ this.inkData = data;
+ }
}
- relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => {
- let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY);
- x += 50000
- y += 50000
+ relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => {
+ let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey);
return { x, y };
}
+ @undoBatch
@action
removeLine = (id: string): void => {
let data = this.inkData;
@@ -122,50 +142,41 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
this.inkData = data;
}
- render() {
- // styling for cursor
- let canvasStyle = {};
- if (InkingControl.Instance.selectedTool === InkTool.None) {
- canvasStyle = { pointerEvents: "none" };
- } else {
- canvasStyle = { pointerEvents: "auto", cursor: "crosshair" };
- }
-
- // get data from server
- // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField);
- // if (!inkField || inkField == "<Waiting>") {
- // return (<div className="inking-canvas" style={canvasStyle}
- // onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} >
- // <svg>
- // </svg>
- // </div >)
- // }
-
- let lines = this.inkData;
-
- // parse data from server
- let paths: Array<JSX.Element> = []
- let curPage = this.props.Document.GetNumber(KeyStore.CurPage, 0)
- Array.from(lines).map(item => {
- let id = item[0];
- let strokeData = item[1];
- if (strokeData.page == 0 || strokeData.page == curPage)
+ @computed
+ get drawnPaths() {
+ let curPage = NumCast(this.props.Document.curPage, -1);
+ let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
+ if (strokeData.page === -1 || strokeData.page === curPage) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
+ count={strokeData.pathData.length}
+ offsetX={this.maxCanvasDim - this.inkMidX}
+ offsetY={this.maxCanvasDim - this.inkMidY}
color={strokeData.color}
width={strokeData.width}
tool={strokeData.tool}
- deleteCallback={this.removeLine} />)
- })
+ deleteCallback={this.removeLine} />);
+ }
+ return paths;
+ }, [] as JSX.Element[]);
+ return [<svg className={`inkingCanvas-paths-ink`} key="Pens"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }} >
+ {paths.filter(path => path.props.tool !== InkTool.Highlighter)}
+ </svg>,
+ <svg className={`inkingCanvas-paths-markers`} key="Markers"
+ style={{ left: `${this.inkMidX - this.maxCanvasDim}px`, top: `${this.inkMidY - this.maxCanvasDim}px` }}>
+ {paths.filter(path => path.props.tool === InkTool.Highlighter)}
+ </svg>];
+ }
+ render() {
+ let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None ? "canSelect" : "noSelect";
return (
-
- <div className="inking-canvas" style={canvasStyle}
- onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
- <svg>
- {paths}
- </svg>
+ <div className="inkingCanvas" >
+ <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
+ {this.props.children()}
+ {this.drawnPaths}
</div >
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
new file mode 100644
index 000000000..ba4ec41af
--- /dev/null
+++ b/src/client/views/InkingControl.scss
@@ -0,0 +1,135 @@
+@import "globalCssVariables";
+.inking-control {
+ position: absolute;
+ left: 70px;
+ bottom: 70px;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ label,
+ input,
+ option {
+ font-size: 12px;
+ }
+ input[type="range"] {
+ -webkit-appearance: none;
+ background-color: transparent;
+ vertical-align: middle;
+ margin-top: 8px;
+ &:focus {
+ outline: none;
+ }
+ &::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 3px;
+ border-radius: 1.5px;
+ cursor: pointer;
+ background: $intermediate-color;
+ }
+ &::-webkit-slider-thumb {
+ height: 12px;
+ width: 12px;
+ border: 1px solid $intermediate-color;
+ border-radius: 6px;
+ background: $light-color;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -4px;
+ }
+ &::-moz-range-track {
+ width: 100%;
+ height: 3px;
+ border-radius: 1.5px;
+ cursor: pointer;
+ background: $light-color;
+ }
+ &::-moz-range-thumb {
+ height: 12px;
+ width: 12px;
+ border: 1px solid $intermediate-color;
+ border-radius: 6px;
+ background: $light-color;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -4px;
+ }
+ }
+ input[type="text"] {
+ border: none;
+ padding: 0 0px;
+ background: transparent;
+ color: $dark-color;
+ font-size: 12px;
+ margin-top: 4px;
+ }
+ .ink-panel {
+ margin: 6px 12px 6px 0;
+ height: 30px;
+ vertical-align: middle;
+ line-height: 36px;
+ padding: 0 10px;
+ color: $intermediate-color;
+ &:first {
+ margin-top: 0;
+ }
+ }
+ .ink-tools {
+ display: flex;
+ background-color: transparent;
+ border-radius: 0;
+ padding: 0;
+ button {
+ height: 36px;
+ padding: 0px;
+ padding-bottom: 3px;
+ margin-left: 10px;
+ background-color: transparent;
+ color: $intermediate-color;
+ }
+ button:hover {
+ transform: scale(1.15);
+ }
+ }
+ .ink-size {
+ display: flex;
+ justify-content: space-between;
+ input[type="text"] {
+ width: 42px;
+ }
+ >* {
+ margin-right: 6px;
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+ .ink-color {
+ display: flex;
+ position: relative;
+ padding-right: 0;
+ label {
+ margin-right: 6px;
+ }
+ .ink-color-display {
+ border-radius: 11px;
+ width: 22px;
+ height: 22px;
+ margin-top: 6px;
+ cursor: pointer;
+ text-align: center; // span {
+ // color: $light-color;
+ // font-size: 8px;
+ // user-select: none;
+ // }
+ }
+ .ink-color-picker {
+ background-color: $light-color;
+ border-radius: 5px;
+ padding: 12px;
+ position: absolute;
+ bottom: 36px;
+ left: -3px;
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 955831fb6..17d4a1e49 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,20 +1,30 @@
import { observable, action, computed } from "mobx";
+
import { CirclePicker, ColorResult } from 'react-color';
import React = require("react");
-import "./InkingCanvas.scss"
-import { InkTool } from "../../fields/InkField";
import { observer } from "mobx-react";
+import "./InkingControl.scss";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
+import { SelectionManager } from "../util/SelectionManager";
+import { InkTool } from "../../new_fields/InkField";
+import { Doc } from "../../new_fields/Doc";
+
+library.add(faPen, faHighlighter, faEraser, faBan);
@observer
export class InkingControl extends React.Component {
static Instance: InkingControl = new InkingControl({});
@observable private _selectedTool: InkTool = InkTool.None;
- @observable private _selectedColor: string = "#f44336";
+ @observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "25";
+ @observable private _open: boolean = false;
+ @observable private _colorPickerDisplay = false;
constructor(props: Readonly<{}>) {
super(props);
- InkingControl.Instance = this
+ InkingControl.Instance = this;
}
@action
@@ -25,6 +35,9 @@ export class InkingControl extends React.Component {
@action
switchColor = (color: ColorResult): void => {
this._selectedColor = color.hex;
+ SelectionManager.SelectedDocuments().forEach(doc =>
+ doc.props.ContainingCollectionView && Doc.SetOnPrototype(doc.props.Document, "backgroundColor", color.hex)
+ );
}
@action
@@ -49,29 +62,51 @@ export class InkingControl extends React.Component {
selected = (tool: InkTool) => {
if (this._selectedTool === tool) {
- return { backgroundColor: "black", color: "white" }
+ return { color: "#61aaa3" };
}
- return {}
+ return {};
+ }
+
+ @action
+ toggleDisplay = () => {
+ this._open = !this._open;
+ }
+
+
+ @action
+ toggleColorPicker = () => {
+ this._colorPickerDisplay = !this._colorPickerDisplay;
}
render() {
return (
- <div className="inking-control">
- <div className="ink-tools ink-panel">
- <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}>Pen</button>
- <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}>Highlighter</button>
- <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}>Eraser</button>
- <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}> None</button>
- </div>
- <div className="ink-size ink-panel">
- <label htmlFor="stroke-width">Size</label>
- <input type="range" min="1" max="100" defaultValue="25" name="stroke-width"
+ <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <li className="ink-tools ink-panel">
+ <div className="ink-tool-buttons">
+ <button onClick={() => this.switchTool(InkTool.Pen)} style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" title="Pen" /></button>
+ <button onClick={() => this.switchTool(InkTool.Highlighter)} style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" title="Highlighter" /></button>
+ <button onClick={() => this.switchTool(InkTool.Eraser)} style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" title="Eraser" /></button>
+ <button onClick={() => this.switchTool(InkTool.None)} style={this.selected(InkTool.None)}><FontAwesomeIcon icon="ban" size="lg" title="Pointer" /></button>
+ </div>
+ </li>
+ <li className="ink-color ink-panel">
+ <label>COLOR: </label>
+ <div className="ink-color-display" style={{ backgroundColor: this._selectedColor }}
+ onClick={() => this.toggleColorPicker()}>
+ {/* {this._colorPickerDisplay ? <span>&#9660;</span> : <span>&#9650;</span>} */}
+ </div>
+ <div className="ink-color-picker" style={this._colorPickerDisplay ? { display: "block" } : { display: "none" }}>
+ <CirclePicker onChange={this.switchColor} circleSize={22} width={"220"} />
+ </div>
+ </li>
+ <li className="ink-size ink-panel">
+ <label htmlFor="stroke-width">SIZE: </label>
+ <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
+ <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- </div>
- <div className="ink-color ink-panel">
- <CirclePicker onChange={this.switchColor} />
- </div>
- </div>
- )
+ </li>
+ </ul >
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss
new file mode 100644
index 000000000..cdbfdcff3
--- /dev/null
+++ b/src/client/views/InkingStroke.scss
@@ -0,0 +1,3 @@
+.inkingStroke-marker {
+ mix-blend-mode: multiply
+} \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index d724421d3..37b1f5899 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,12 +1,16 @@
import { observer } from "mobx-react";
-import { observable } from "mobx";
+import { observable, trace } from "mobx";
import { InkingControl } from "./InkingControl";
-import { InkTool } from "../../fields/InkField";
import React = require("react");
+import { InkTool } from "../../new_fields/InkField";
+import "./InkingStroke.scss";
interface StrokeProps {
+ offsetX: number;
+ offsetY: number;
id: string;
+ count: number;
line: Array<{ x: number, y: number }>;
color: string;
width: string;
@@ -21,23 +25,14 @@ export class InkingStroke extends React.Component<StrokeProps> {
@observable private _strokeColor: string = this.props.color;
@observable private _strokeWidth: string = this.props.width;
- private _canvasColor: string = "#cdcdcd";
-
- deleteStroke = (e: React.MouseEvent): void => {
+ deleteStroke = (e: React.PointerEvent): void => {
if (InkingControl.Instance.selectedTool === InkTool.Eraser && e.buttons === 1) {
this.props.deleteCallback(this.props.id);
}
}
parseData = (line: Array<{ x: number, y: number }>): string => {
- if (line.length === 0) {
- return "";
- }
- const pathData = "M " +
- line.map(p => {
- return p.x + " " + p.y;
- }).join(" L ");
- return pathData;
+ return !line.length ? "" : "M " + line.map(p => (p.x + this.props.offsetX) + " " + (p.y + this.props.offsetY)).join(" L ");
}
createStyle() {
@@ -48,19 +43,20 @@ export class InkingStroke extends React.Component<StrokeProps> {
fill: "none",
stroke: this._strokeColor,
strokeWidth: this._strokeWidth + "px",
- }
+ };
}
}
-
render() {
let pathStyle = this.createStyle();
let pathData = this.parseData(this.props.line);
+ let pathlength = this.props.count; // bcz: this is needed to force reactions to the line data changes
+ let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
+ let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
return (
- <path className={(this._strokeTool === InkTool.Highlighter) ? "highlight" : ""}
- d={pathData} style={pathStyle} strokeLinejoin="round" strokeLinecap="round"
- onMouseOver={this.deleteStroke} onMouseDown={this.deleteStroke} />
- )
+ <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
+ onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 4334ed299..d63b0147b 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -1,51 +1,196 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
html,
body {
width: 100%;
height: 100%;
- overflow: hidden;
- font-family: 'Hind Siliguri', sans-serif;
+ overflow: auto;
+ font-family: $sans-serif;
margin: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
}
-h1 {
- font-size: 50px;
- position: fixed;
- top: 30px;
- left: 50%;
- transform: translateX(-50%);
- color: black;
- text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff;
- z-index: 9999;
- font-family: 'Fjalla One', sans-serif;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
+div {
user-select: none;
}
+#dash-title {
+ position: absolute;
+ right: 46.5%;
+ letter-spacing: 3px;
+ top: 9px;
+ font-size: 12px;
+ color: $alt-accent;
+ z-index: 9999;
+}
+
+.jsx-parser {
+ width: 100%;
+ pointer-events: none;
+ border-radius: inherit;
+}
+
p {
margin: 0px;
padding: 0px;
}
+
::-webkit-scrollbar {
-webkit-appearance: none;
- height:5px;
- width:5px;
+ height: 5px;
+ width: 10px;
}
+
::-webkit-scrollbar-thumb {
border-radius: 2px;
- background-color: rgba(0,0,0,.5);
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+// button stuff
+button {
+ background: $dark-color;
+ outline: none;
+ border: 0px;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ padding: 10px;
+ transition: transform 0.2s;
}
-.main-buttonDiv {
+button:hover {
+ background: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+}
+
+.clear-db-button {
position: absolute;
- width: 150px;
- left: 0px;
+ right: 45%;
+ bottom: 3%;
+ font-size: 50%;
+}
+
+.round-button {
+ width: 36px;
+ height: 36px;
+ border-radius: 18px;
+ font-size: 15px;
+}
+
+.round-button:hover {
+ transform: scale(1.15);
}
+
+.add-button {
+ position: relative;
+ margin-right: 10px;
+}
+
.main-undoButtons {
position: absolute;
width: 150px;
right: 0px;
}
+
+//toolbar stuff
+#toolbar {
+ position: absolute;
+ bottom: 62px;
+ left: 24px;
+
+ .toolbar-button {
+ display: block;
+ margin-bottom: 10px;
+ }
+}
+
+// add nodes menu. Note that the + button is actually an input label, not an actual button.
+#add-nodes-menu {
+ position: absolute;
+ bottom: 22px;
+ left: 24px;
+
+ label {
+ background: $dark-color;
+ color: $light-color;
+ display: inline-block;
+ border-radius: 18px;
+ font-size: 25px;
+ width: 36px;
+ height: 36px;
+ margin-right: 10px;
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+
+ label p {
+ padding-left: 10.5px;
+ }
+
+ label:hover {
+ background: $main-accent;
+ transform: scale(1.15);
+ }
+
+ input {
+ display: none;
+ }
+
+ input:not(:checked)~#add-options-content {
+ display: none;
+ }
+
+ input:checked~label {
+ transform: rotate(45deg);
+ transition: transform 0.5s;
+ cursor: pointer;
+ }
+}
+
+#root {
+ overflow: visible;
+}
+
+#main-div {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: scroll;
+ z-index: 1;
+}
+
+#mainContent-div {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+#add-options-content {
+ display: table;
+ opacity: 1;
+ margin: 0;
+ padding: 0;
+ position: relative;
+ float: right;
+ bottom: 0.3em;
+ margin-bottom: -1.68em;
+}
+
+ul#add-options-list {
+ list-style: none;
+ padding: 5 0 0 0;
+
+ li {
+ display: inline-block;
+ padding: 0;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b78f59681..177047a9a 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,117 +1,281 @@
-import { action, configure } from 'mobx';
+import { IconName, library } from '@fortawesome/fontawesome-svg-core';
+import { faFilePdf, faFilm, faFont, faGlobeAsia, faImage, faMusic, faObjectGroup, faPenNib, faRedoAlt, faTable, faTree, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, configure, observable, runInAction, trace } from 'mobx';
+import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
-import { Document } from '../../fields/Document';
-import { KeyStore } from '../../fields/KeyStore';
-import "./Main.scss";
-import { MessageStore } from '../../server/Message';
-import { Utils } from '../../Utils';
-import { Documents } from '../documents/Documents';
-import { Server } from '../Server';
-import { setupDrag } from '../util/DragManager';
+import Measure from 'react-measure';
+import * as request from 'request';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { RouteStore } from '../../server/RouteStore';
+import { emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils';
+import { Docs } from '../documents/Documents';
+import { SetupDrag, DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
+import { PresentationView } from './PresentationView';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { ContextMenu } from './ContextMenu';
import { DocumentDecorations } from './DocumentDecorations';
-import { DocumentView } from './nodes/DocumentView';
-import "./Main.scss";
import { InkingControl } from './InkingControl';
+import "./Main.scss";
+import { MainOverlayTextBox } from './MainOverlayTextBox';
+import { DocumentView } from './nodes/DocumentView';
+import { PreviewCursor } from './PreviewCursor';
+import { SearchBox } from './SearchBox';
+import { SelectionManager } from '../util/SelectionManager';
+import { FieldResult, Field, Doc, Opt } from '../../new_fields/Doc';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { DocServer } from '../DocServer';
+import { listSpec } from '../../new_fields/Schema';
+import { Id } from '../../new_fields/RefField';
+import { HistoryUtil } from '../util/History';
+
+@observer
+export class Main extends React.Component {
+ public static Instance: Main;
+ @observable private _workspacesShown: boolean = false;
+ @observable public pwidth: number = 0;
+ @observable public pheight: number = 0;
-configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action
-window.addEventListener("drop", (e) => e.preventDefault(), false)
-window.addEventListener("dragover", (e) => e.preventDefault(), false)
-document.addEventListener("pointerdown", action(function (e: PointerEvent) {
- if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
- ContextMenu.Instance.clearItems()
+ @computed private get mainContainer(): Opt<Doc> {
+ return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
+ }
+ private set mainContainer(doc: Opt<Doc>) {
+ if (doc) {
+ if (!("presentationView" in doc)) {
+ doc.presentationView = new Doc();
+ }
+ CurrentUserUtils.UserDocument.activeWorkspace = doc;
+ }
}
-}), true)
+ constructor(props: Readonly<{}>) {
+ super(props);
+ Main.Instance = this;
+ // causes errors to be generated when modifying an observable outside of an action
+ configure({ enforceActions: "observed" });
+ if (window.location.pathname !== RouteStore.home) {
+ let pathname = window.location.pathname.split("/");
+ if (pathname.length > 1 && pathname[pathname.length - 2] === 'doc') {
+ CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
+ }
+ }
-const mainDocId = "mainDoc";
-let mainContainer: Document;
-let mainfreeform: Document;
-Documents.initProtos(mainDocId, (res?: Document) => {
- if (res instanceof Document) {
- mainContainer = res;
- mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document);
+ library.add(faFont);
+ library.add(faImage);
+ library.add(faFilePdf);
+ library.add(faObjectGroup);
+ library.add(faTable);
+ library.add(faGlobeAsia);
+ library.add(faUndoAlt);
+ library.add(faRedoAlt);
+ library.add(faPenNib);
+ library.add(faFilm);
+ library.add(faMusic);
+ library.add(faTree);
+ this.initEventListeners();
+ this.initAuthenticationRouters();
}
- else {
- mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId);
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
+ initEventListeners = () => {
+ // window.addEventListener("pointermove", (e) => this.reportLocation(e))
+ window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
+ window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
+ window.addEventListener("keydown", (e) => {
+ if (e.key === "Escape") {
+ DragManager.AbortDrag();
+ SelectionManager.DeselectAll();
+ }
+ }, false); // drag event handler
+ // click interactions for the context menu
+ document.addEventListener("pointerdown", action(function (e: PointerEvent) {
+ if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) {
+ ContextMenu.Instance.clearItems();
+ }
+ }), true);
+ }
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] };
- mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout));
- mainContainer.Set(KeyStore.ActiveFrame, mainfreeform);
- }, 0);
+ initAuthenticationRouters = async () => {
+ // Load the user's active workspace, or create a new one if initial session after signup
+ if (!CurrentUserUtils.MainDocId) {
+ const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ if (doc) {
+ this.openWorkspace(doc);
+ } else {
+ this.createNewWorkspace();
+ }
+ } else {
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+ field instanceof Doc ? this.openWorkspace(field) :
+ this.createNewWorkspace(CurrentUserUtils.MainDocId));
+ }
}
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
- let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf"
- let weburl = "https://cs.brown.edu/courses/cs166/";
- let audiourl = "http://techslides.com/demos/samples/sample.mp3";
- let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
- let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {}))
- let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" }))
- let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
- let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" }));
- let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" }));
- let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" }));
- let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" }));
- let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
- let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }))
- let addClick = (creator: () => Document) => action(() =>
- mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator())
- );
-
- let imgRef = React.createRef<HTMLDivElement>();
- let pdfRef = React.createRef<HTMLDivElement>();
- let webRef = React.createRef<HTMLDivElement>();
- let textRef = React.createRef<HTMLDivElement>();
- let schemaRef = React.createRef<HTMLDivElement>();
- let videoRef = React.createRef<HTMLDivElement>();
- let audioRef = React.createRef<HTMLDivElement>();
- let colRef = React.createRef<HTMLDivElement>();
-
- ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
- <DocumentView Document={mainContainer}
- AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity}
- ContentScaling={() => 1}
- PanelWidth={() => 0}
- PanelHeight={() => 0}
+ @action
+ createNewWorkspace = async (id?: string) => {
+ const list = Cast(CurrentUserUtils.UserDocument.data, listSpec(Doc));
+ if (list) {
+ let freeformDoc = Docs.FreeformDocument([], { x: 0, y: 400, title: `WS collection ${list.length + 1}` });
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
+ let mainDoc = Docs.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` });
+ list.push(mainDoc);
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => {
+ this.openWorkspace(mainDoc);
+ let pendingDocument = Docs.SchemaDocument(["title"], [], { title: "New Mobile Uploads" });
+ mainDoc.optionalRightCollection = pendingDocument;
+ }, 0);
+ }
+ }
+
+ @action
+ openWorkspace = async (doc: Doc, fromHistory = false) => {
+ CurrentUserUtils.MainDocId = doc[Id];
+ this.mainContainer = doc;
+ fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], initializers: {} });
+ const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
+ // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
+ setTimeout(async () => {
+ if (col) {
+ const l = Cast(col.data, listSpec(Doc));
+ if (l && l.length > 0) {
+ CollectionDockingView.Instance.AddRightSplit(col);
+ }
+ }
+ }, 100);
+ }
+ @action
+ onResize = (r: any) => {
+ this.pwidth = r.offset.width;
+ this.pheight = r.offset.height;
+ }
+ getPWidth = () => {
+ return this.pwidth;
+ }
+ getPHeight = () => {
+ return this.pheight;
+ }
+ @computed
+ get mainContent() {
+ let mainCont = this.mainContainer;
+ let content = !mainCont ? (null) :
+ <DocumentView Document={mainCont}
+ toggleMinimized={emptyFunction}
+ addDocument={undefined}
+ addDocTab={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.getPWidth}
+ PanelHeight={this.getPHeight}
isTopMost={true}
- SelectOnLoad={false}
- focus={() => { }}
- ContainingCollectionView={undefined} />
- <DocumentDecorations />
- <ContextMenu />
- <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} >
- <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div>
- <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} >
- <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div>
- <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}>
- <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div>
- <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}>
- <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div>
- <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}>
- <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div>
- <div className="main-buttonDiv" style={{ bottom: '125px' }} >
- <button onClick={clearDatabase}>Clear Database</button></div>
- <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}>
- <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div>
- <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}>
- <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div>
- <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}>
- <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div>
- <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button>
- <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button>
- <InkingControl />
- </div>),
- document.getElementById('root'));
-})
+ selectOnLoad={false}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined} />;
+ const pres = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
+ return <Measure offset onResize={this.onResize}>
+ {({ measureRef }) =>
+ <div ref={measureRef} id="mainContent-div">
+ {content}
+ {pres ? <PresentationView Document={pres} key="presentation" /> : null}
+ </div>
+ }
+ </Measure>;
+ }
+
+ /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
+ nodesMenu() {
+
+ let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+ let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c27211_sample_explain.pdf";
+ let weburl = "https://cs.brown.edu/courses/cs166/";
+ let audiourl = "http://techslides.com/demos/samples/sample.mp3";
+ let videourl = "http://techslides.com/demos/sample-videos/small.mp4";
+
+ let addTextNode = action(() => Docs.TextDocument({ borderRounding: -1, width: 200, height: 200, title: "a text note" }));
+ let addColNode = action(() => Docs.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" }));
+ let addSchemaNode = action(() => Docs.SchemaDocument(["title"], [], { width: 200, height: 200, title: "a schema collection" }));
+ let addTreeNode = action(() => Docs.TreeDocument([CurrentUserUtils.UserDocument], { width: 250, height: 400, title: "Library:" + CurrentUserUtils.email, dropAction: "alias" }));
+ // let addTreeNode = action(() => Docs.TreeDocument(this._northstarSchemas, { width: 250, height: 400, title: "northstar schemas", dropAction: "copy" }));
+ let addVideoNode = action(() => Docs.VideoDocument(videourl, { width: 200, title: "video node" }));
+ let addPDFNode = action(() => Docs.PdfDocument(pdfurl, { width: 200, height: 200, title: "a pdf doc" }));
+ let addImageNode = action(() => Docs.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
+ let addWebNode = action(() => Docs.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" }));
+ let addAudioNode = action(() => Docs.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" }));
+
+ let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
+ [React.createRef<HTMLDivElement>(), "font", "Add Textbox", addTextNode],
+ [React.createRef<HTMLDivElement>(), "image", "Add Image", addImageNode],
+ [React.createRef<HTMLDivElement>(), "file-pdf", "Add PDF", addPDFNode],
+ [React.createRef<HTMLDivElement>(), "film", "Add Video", addVideoNode],
+ [React.createRef<HTMLDivElement>(), "music", "Add Audio", addAudioNode],
+ [React.createRef<HTMLDivElement>(), "globe-asia", "Add Web Clipping", addWebNode],
+ [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
+ [React.createRef<HTMLDivElement>(), "tree", "Add Tree", addTreeNode],
+ [React.createRef<HTMLDivElement>(), "table", "Add Schema", addSchemaNode],
+ ];
+
+ return < div id="add-nodes-menu" >
+ <input type="checkbox" id="add-menu-toggle" />
+ <label htmlFor="add-menu-toggle" title="Add Node"><p>+</p></label>
+
+ <div id="add-options-content">
+ <ul id="add-options-list">
+ {btns.map(btn =>
+ <li key={btn[1]} ><div ref={btn[0]}>
+ <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
+ <FontAwesomeIcon icon={btn[1]} size="sm" />
+ </button>
+ </div></li>)}
+ </ul>
+ </div>
+ </div >;
+ }
+
+ /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
+ @computed
+ get miscButtons() {
+ let logoutRef = React.createRef<HTMLDivElement>();
+
+ return [
+ <button className="clear-db-button" key="clear-db" onClick={e => e.shiftKey && e.altKey && DocServer.DeleteDatabase()}>Clear Database</button>,
+ <div id="toolbar" key="toolbar">
+ <button className="toolbar-button round-button" title="Undo" onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Redo" onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button>
+ <button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /></button>
+ </div >,
+ <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <SearchBox /> </div>,
+ <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+ <button onClick={() => request.get(DocServer.prepend(RouteStore.logout), emptyFunction)}>Log Out</button></div>
+ ];
+
+ }
+
+ render() {
+ return (
+ <div id="main-div">
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.nodesMenu()}
+ {this.miscButtons}
+ <InkingControl />
+ <MainOverlayTextBox />
+ </div>
+ );
+ }
+}
+
+(async () => {
+ await Docs.initProtos();
+ await CurrentUserUtils.loadCurrentUser();
+ ReactDOM.render(<Main />, document.getElementById('root'));
+})();
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
new file mode 100644
index 000000000..f6a746e63
--- /dev/null
+++ b/src/client/views/MainOverlayTextBox.scss
@@ -0,0 +1,20 @@
+@import "globalCssVariables";
+.mainOverlayTextBox-textInput {
+ background-color: rgba(248, 6, 6, 0.001);
+ width: 200px;
+ height: 200px;
+ position:absolute;
+ overflow: visible;
+ top: 0;
+ left: 0;
+ pointer-events: none;
+ z-index: $mainTextInput-zindex;
+ .formattedTextBox-cont {
+ background-color: rgba(248, 6, 6, 0.001);
+ width: 100%;
+ height: 100%;
+ position:absolute;
+ top: 0;
+ left: 0;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
new file mode 100644
index 000000000..91f626737
--- /dev/null
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -0,0 +1,99 @@
+import { action, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { emptyFunction, returnTrue, returnZero } from '../../Utils';
+import { DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import "normalize.css";
+import "./MainOverlayTextBox.scss";
+import { FormattedTextBox } from './nodes/FormattedTextBox';
+
+interface MainOverlayTextBoxProps {
+}
+
+@observer
+export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
+ public static Instance: MainOverlayTextBox;
+ @observable _textXf: () => Transform = () => Transform.Identity();
+ private _textFieldKey: string = "data";
+ private _textColor: string | null = null;
+ private _textTargetDiv: HTMLDivElement | undefined;
+ private _textProxyDiv: React.RefObject<HTMLDivElement>;
+
+ constructor(props: MainOverlayTextBoxProps) {
+ super(props);
+ this._textProxyDiv = React.createRef();
+ MainOverlayTextBox.Instance = this;
+ reaction(() => FormattedTextBox.InputBoxOverlay,
+ (box?: FormattedTextBox) => {
+ if (box) this.setTextDoc(box.props.fieldKey, box.CurrentDiv, box.props.ScreenToLocalTransform);
+ else this.setTextDoc();
+ });
+ }
+
+ @action
+ private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform) {
+ if (this._textTargetDiv) {
+ this._textTargetDiv.style.color = this._textColor;
+ }
+ this._textFieldKey = textFieldKey!;
+ this._textXf = tx ? tx : () => Transform.Identity();
+ this._textTargetDiv = div;
+ if (div) {
+ if (div.parentElement && div.parentElement instanceof HTMLDivElement && div.parentElement.id === "screenSpace") this._textXf = () => Transform.Identity();
+ this._textColor = div.style.color;
+ div.style.color = "transparent";
+ }
+ }
+
+ @action
+ textScroll = (e: React.UIEvent) => {
+ if (this._textProxyDiv.current && this._textTargetDiv) {
+ this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
+ }
+ }
+
+ textBoxDown = (e: React.PointerEvent) => {
+ if (e.button !== 0 || e.metaKey || e.altKey) {
+ document.addEventListener("pointermove", this.textBoxMove);
+ document.addEventListener('pointerup', this.textBoxUp);
+ }
+ }
+ @action
+ textBoxMove = (e: PointerEvent) => {
+ if (e.movementX > 1 || e.movementY > 1) {
+ document.removeEventListener("pointermove", this.textBoxMove);
+ document.removeEventListener('pointerup', this.textBoxUp);
+ let dragData = new DragManager.DocumentDragData(FormattedTextBox.InputBoxOverlay ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
+ const [left, top] = this._textXf().inverse().transformPoint(0, 0);
+ dragData.xOffset = e.clientX - left;
+ dragData.yOffset = e.clientY - top;
+ DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, {
+ handlers: {
+ dragComplete: action(emptyFunction),
+ },
+ hideSource: false
+ });
+ }
+ }
+ textBoxUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.textBoxMove);
+ document.removeEventListener('pointerup', this.textBoxUp);
+ }
+
+ render() {
+ if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
+ let textRect = this._textTargetDiv.getBoundingClientRect();
+ let s = this._textXf().Scale;
+ return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${textRect.left}px, ${textRect.top}px) scale(${1 / s},${1 / s})`, width: "auto", height: "auto" }} >
+ <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
+ style={{ width: `${textRect.width * s}px`, height: `${textRect.height * s}px` }}>
+ <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document} isSelected={returnTrue} select={emptyFunction} isTopMost={true}
+ selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
+ ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} />
+ </div>
+ </ div>;
+ }
+ else return (null);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PresentationView.scss b/src/client/views/PresentationView.scss
new file mode 100644
index 000000000..fb4a851c4
--- /dev/null
+++ b/src/client/views/PresentationView.scss
@@ -0,0 +1,79 @@
+.presentationView-cont {
+ position: absolute;
+ background: white;
+ z-index: 1;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ right: 0;
+ top: 0;
+ bottom: 0;
+}
+
+.presentationView-item {
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ transition: all .1s;
+}
+
+.presentationView-listCont {
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+.presentationView-item:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentationView-selected {
+ background: gray;
+}
+
+.presentationView-heading {
+ background: lightseagreen;
+ padding: 10px;
+ display: inline-block;
+ width: 100%;
+}
+
+.presentationView-title {
+ padding-top: 3px;
+ padding-bottom: 3px;
+ font-size: 25px;
+ display: inline-block;
+}
+
+.presentation-icon {
+ float: right;
+}
+
+.presentationView-name {
+ font-size: 15px;
+ display: inline-block;
+}
+
+.presentation-button {
+ margin-right: 12.5%;
+ margin-left: 12.5%;
+ width: 25%;
+}
+
+.presentation-buttons {
+ padding: 10px;
+}
+
+.presentation-next:hover {
+ transition: all .1s;
+ background: #AAAAAA
+}
+
+.presentation-back:hover {
+ transition: all .1s;
+ background: #AAAAAA
+} \ No newline at end of file
diff --git a/src/client/views/PresentationView.tsx b/src/client/views/PresentationView.tsx
new file mode 100644
index 000000000..7d0dc2913
--- /dev/null
+++ b/src/client/views/PresentationView.tsx
@@ -0,0 +1,150 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, reaction } from "mobx";
+import "./PresentationView.scss";
+import "./Main.tsx";
+import { DocumentManager } from "../util/DocumentManager";
+import { Utils } from "../../Utils";
+import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue, StrCast } from "../../new_fields/Types";
+import { Id } from "../../new_fields/RefField";
+import { List } from "../../new_fields/List";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+
+export interface PresViewProps {
+ Document: Doc;
+}
+
+interface PresListProps extends PresViewProps {
+ deleteDocument(index: number): void;
+ gotoDocument(index: number): void;
+}
+
+
+@observer
+/**
+ * Component that takes in a document prop and a boolean whether it's collapsed or not.
+ */
+class PresentationViewList extends React.Component<PresListProps> {
+
+ /**
+ * Renders a single child document. It will just append a list element.
+ * @param document The document to render.
+ */
+ renderChild = (document: Doc, index: number) => {
+ let title = document.title;
+
+ //to get currently selected presentation doc
+ let selected = NumCast(this.props.Document.selectedDoc, 0);
+
+ let className = "presentationView-item";
+ if (selected === index) {
+ //this doc is selected
+ className += " presentationView-selected";
+ }
+ return (
+ <div className={className} key={document[Id] + index} onClick={e => { this.props.gotoDocument(index); e.stopPropagation(); }}>
+ <strong className="presentationView-name">
+ {`${index + 1}. ${title}`}
+ </strong>
+ <button className="presentation-icon" onClick={e => { this.props.deleteDocument(index); e.stopPropagation(); }}>X</button>
+ </div>
+ );
+
+ }
+
+ render() {
+ const children = DocListCast(this.props.Document.data);
+
+ return (
+ <div className="presentationView-listCont">
+ {children.map(this.renderChild)}
+ </div>
+ );
+ }
+}
+
+
+@observer
+export class PresentationView extends React.Component<PresViewProps> {
+ public static Instance: PresentationView;
+
+ //observable means render is re-called every time variable is changed
+ @observable
+ collapsed: boolean = false;
+ closePresentation = action(() => this.props.Document.width = 0);
+ next = () => {
+ const current = NumCast(this.props.Document.selectedDoc);
+ this.gotoDocument(current + 1);
+
+ }
+ back = () => {
+ const current = NumCast(this.props.Document.selectedDoc);
+ this.gotoDocument(current - 1);
+ }
+
+ @action
+ public RemoveDoc = (index: number) => {
+ const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+ if (value) {
+ value.splice(index, 1);
+ }
+ }
+
+ public gotoDocument = async (index: number) => {
+ const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+ if (!list) {
+ return;
+ }
+ if (index < 0 || index >= list.length) {
+ return;
+ }
+
+ this.props.Document.selectedDoc = index;
+ const doc = await list[index];
+ DocumentManager.Instance.jumpToDocument(doc);
+ }
+
+ //initilize class variables
+ constructor(props: PresViewProps) {
+ super(props);
+ PresentationView.Instance = this;
+ }
+
+ /**
+ * Adds a document to the presentation view
+ **/
+ @action
+ public PinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const data = Cast(this.props.Document.data, listSpec(Doc));
+ if (data) {
+ data.push(doc);
+ } else {
+ this.props.Document.data = new List([doc]);
+ }
+
+ this.props.Document.width = 300;
+ }
+
+ render() {
+ let titleStr = StrCast(this.props.Document.title);
+ let width = NumCast(this.props.Document.width);
+
+ //TODO: next and back should be icons
+ return (
+ <div className="presentationView-cont" style={{ width: width, overflow: "hidden" }}>
+ <div className="presentationView-heading">
+ <div className="presentationView-title">{titleStr}</div>
+ <button className='presentation-icon' onClick={this.closePresentation}>X</button>
+ </div>
+ <div className="presentation-buttons">
+ <button className="presentation-button" onClick={this.back}>back</button>
+ <button className="presentation-button" onClick={this.next}>next</button>
+ </div>
+ <PresentationViewList Document={this.props.Document} deleteDocument={this.RemoveDoc} gotoDocument={this.gotoDocument} />
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
new file mode 100644
index 000000000..20f9b9a49
--- /dev/null
+++ b/src/client/views/PreviewCursor.scss
@@ -0,0 +1,9 @@
+
+.previewCursor {
+ color: black;
+ position: absolute;
+ transform-origin: left top;
+ top: 0;
+ left:0;
+ pointer-events: none;
+} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
new file mode 100644
index 000000000..7c1d00eb0
--- /dev/null
+++ b/src/client/views/PreviewCursor.tsx
@@ -0,0 +1,64 @@
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import "./PreviewCursor.scss";
+
+@observer
+export class PreviewCursor extends React.Component<{}> {
+ private _prompt = React.createRef<HTMLDivElement>();
+ static _onKeyPress?: (e: KeyboardEvent) => void;
+ @observable static _clickPoint = [0, 0];
+ @observable public static Visible = false;
+ //when focus is lost, this will remove the preview cursor
+ @action onBlur = (): void => {
+ PreviewCursor.Visible = false;
+ }
+
+ constructor(props: any) {
+ super(props);
+ document.addEventListener("keydown", this.onKeyPress);
+ document.addEventListener("paste", this.paste);
+ }
+ paste = (e: ClipboardEvent) => {
+ console.log(e.clipboardData);
+ if (e.clipboardData) {
+ console.log(e.clipboardData.getData("text/html"));
+ console.log(e.clipboardData.getData("text/csv"));
+ console.log(e.clipboardData.getData("text/plain"));
+ }
+ }
+
+ @action
+ onKeyPress = (e: KeyboardEvent) => {
+ // Mixing events between React and Native is finicky. In FormattedTextBox, we set the
+ // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
+ // the keyPress here.
+ //if not these keys, make a textbox if preview cursor is active!
+ if (e.key.startsWith("F") && !e.key.endsWith("F")) {
+ } else if (e.key !== "Escape" && e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+ if ((!e.ctrlKey && !e.metaKey) || (e.key >= "a" && e.key <= "z")) {
+ PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
+ PreviewCursor.Visible = false;
+ }
+ }
+ }
+ @action
+ public static Show(x: number, y: number, onKeyPress: (e: KeyboardEvent) => void) {
+ this._clickPoint = [x, y];
+ this._onKeyPress = onKeyPress;
+ setTimeout(action(() => this.Visible = true), (1));
+ }
+ render() {
+ if (!PreviewCursor._clickPoint) {
+ return (null);
+ }
+ if (PreviewCursor.Visible && this._prompt.current) {
+ this._prompt.current.focus();
+ }
+ return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._prompt}
+ style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}>
+ I
+ </div >;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.scss b/src/client/views/SearchBox.scss
new file mode 100644
index 000000000..b38e6091d
--- /dev/null
+++ b/src/client/views/SearchBox.scss
@@ -0,0 +1,102 @@
+@import "globalCssVariables";
+
+.searchBox-bar {
+ height: 32px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding-left: 2px;
+ padding-right: 2px;
+
+ .searchBox-input {
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ align-self: stretch;
+ }
+
+ .searchBox-input:focus {
+ width: 500px;
+ outline: 3px solid lightblue;
+ }
+
+ .searchBox-barChild {
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px;
+ }
+
+ .searchBox-filter {
+ align-self: stretch;
+ }
+
+ .searchBox-submit {
+ color: $dark-color;
+ }
+
+ .searchBox-submit:hover {
+ color: $main-accent;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
+}
+
+.searchBox-results {
+ margin-left: 27px; //Is there a better way to do this?
+}
+
+.filter-form {
+ background: $dark-color;
+ height: 400px;
+ width: 400px;
+ position: relative;
+ right: 1px;
+ color: $light-color;
+ padding: 10px;
+ flex-direction: column;
+}
+
+#header {
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 100%;
+ height: 40px;
+}
+
+#option {
+ height: 20px;
+}
+
+.searchBox-results {
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+
+ .search-item {
+ width: 500px;
+ height: 50px;
+ background: $light-color-secondary;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ transition: all 0.1s;
+ border-width: 0.11px;
+ border-style: none;
+ border-color: $intermediate-color;
+ border-bottom-style: solid;
+ padding: 10px;
+ white-space: nowrap;
+ font-size: 13px;
+ }
+
+ .search-item:hover {
+ transition: all 0.1s;
+ background: $lighter-alt-accent;
+ }
+
+ .search-title {
+ text-transform: uppercase;
+ text-align: left;
+ width: 8vw;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchBox.tsx b/src/client/views/SearchBox.tsx
new file mode 100644
index 000000000..6e64e1af1
--- /dev/null
+++ b/src/client/views/SearchBox.tsx
@@ -0,0 +1,178 @@
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action, runInAction } from 'mobx';
+import { Utils } from '../../Utils';
+import { MessageStore } from '../../server/Message';
+import "./SearchBox.scss";
+import { faSearch, faObjectGroup } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { library } from '@fortawesome/fontawesome-svg-core';
+// const app = express();
+// import * as express from 'express';
+import { Search } from '../../server/Search';
+import * as rp from 'request-promise';
+import { SearchItem } from './SearchItem';
+import { isString } from 'util';
+import { constant } from 'async';
+import { DocServer } from '../DocServer';
+import { Doc } from '../../new_fields/Doc';
+import { Id } from '../../new_fields/RefField';
+import { DocumentManager } from '../util/DocumentManager';
+import { SetupDrag } from '../util/DragManager';
+import { Docs } from '../documents/Documents';
+
+library.add(faSearch);
+library.add(faObjectGroup);
+
+@observer
+export class SearchBox extends React.Component {
+ @observable
+ searchString: string = "";
+
+ @observable private _open: boolean = false;
+ @observable private _resultsOpen: boolean = false;
+
+ @observable
+ private _results: Doc[] = [];
+
+ @action.bound
+ onChange(e: React.ChangeEvent<HTMLInputElement>) {
+ this.searchString = e.target.value;
+ }
+
+ @action
+ submitSearch = async () => {
+ let query = this.searchString;
+ //gets json result into a list of documents that can be used
+ const results = await this.getResults(query);
+
+ runInAction(() => {
+ this._resultsOpen = true;
+ this._results = results;
+ });
+ }
+
+ @action
+ getResults = async (query: string) => {
+ let response = await rp.get(DocServer.prepend('/search'), {
+ qs: {
+ query
+ }
+ });
+ let res: string[] = JSON.parse(response);
+ const fields = await DocServer.GetRefFields(res);
+ const docs: Doc[] = [];
+ for (const id of res) {
+ const field = fields[id];
+ if (field instanceof Doc) {
+ docs.push(field);
+ }
+ }
+ return docs;
+ }
+
+ @action
+ handleClickFilter = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (className !== "filter-button" && className !== "filter-form") {
+ this._open = false;
+ }
+
+ }
+
+ @action
+ handleClickResults = (e: Event): void => {
+ var className = (e.target as any).className;
+ var id = (e.target as any).id;
+ if (id !== "result") {
+ this._resultsOpen = false;
+ this._results = [];
+ }
+
+ }
+
+ componentWillMount() {
+ document.addEventListener('mousedown', this.handleClickFilter, false);
+ document.addEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('mousedown', this.handleClickFilter, false);
+ document.removeEventListener('mousedown', this.handleClickResults, false);
+ }
+
+ @action
+ toggleFilterDisplay = () => {
+ this._open = !this._open;
+ }
+
+ enter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter") {
+ this.submitSearch();
+ }
+ }
+
+ collectionRef = React.createRef<HTMLSpanElement>();
+ startDragCollection = async () => {
+ const results = await this.getResults(this.searchString);
+ const docs = results.map(doc => {
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ });
+ let x = 0;
+ let y = 0;
+ for (const doc of docs) {
+ doc.x = x;
+ doc.y = y;
+ doc.width = 200;
+ doc.height = 200;
+ x += 250;
+ if (x > 1000) {
+ x = 0;
+ y += 250;
+ }
+ }
+ return Docs.FreeformDocument(docs, { width: 400, height: 400, panX: 175, panY: 175, title: `Search Docs: "${this.searchString}"` });
+ }
+
+ // Useful queries:
+ // Delegates of a document: {!join from=id to=proto_i}id:{protoId}
+ // Documents in a collection: {!join from=data_l to=id}id:{collectionProtoId}
+ render() {
+ return (
+ <div>
+ <div className="searchBox-container">
+ <div className="searchBox-bar">
+ <span onPointerDown={SetupDrag(this.collectionRef, this.startDragCollection)} ref={this.collectionRef}>
+ <FontAwesomeIcon icon="object-group" className="searchBox-barChild" size="lg" />
+ </span>
+ <input value={this.searchString} onChange={this.onChange} type="text" placeholder="Search..."
+ className="searchBox-barChild searchBox-input" onKeyPress={this.enter}
+ style={{ width: this._resultsOpen ? "500px" : undefined }} />
+ <button className="searchBox-barChild searchBox-filter" onClick={this.toggleFilterDisplay}>Filter</button>
+ <FontAwesomeIcon icon="search" size="lg" className="searchBox-barChild searchBox-submit" />
+ </div>
+ {this._resultsOpen ? (
+ <div className="searchBox-results">
+ {this._results.map(result => <SearchItem doc={result} key={result[Id]} />)}
+ </div>
+ ) : null}
+ </div>
+ {this._open ? (
+ <div className="filter-form" id="filter" style={this._open ? { display: "flex" } : { display: "none" }}>
+ <div className="filter-form" id="header">Filter Search Results</div>
+ <div className="filter-form" id="option">
+ filter by collection, key, type of node
+ </div>
+
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
new file mode 100644
index 000000000..01c7316d6
--- /dev/null
+++ b/src/client/views/SearchItem.tsx
@@ -0,0 +1,73 @@
+import React = require("react");
+import { Doc } from "../../new_fields/Doc";
+import { DocumentManager } from "../util/DocumentManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { Cast } from "../../new_fields/Types";
+import { FieldView, FieldViewProps } from './nodes/FieldView';
+import { computed } from "mobx";
+import { IconField } from "../../new_fields/IconField";
+import { SetupDrag } from "../util/DragManager";
+
+
+export interface SearchProps {
+ doc: Doc;
+}
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+export class SearchItem extends React.Component<SearchProps> {
+
+ onClick = () => {
+ DocumentManager.Instance.jumpToDocument(this.props.doc);
+ }
+
+ //needs help
+ // @computed get layout(): string { const field = Cast(this.props.doc[fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
+
+
+ public static DocumentIcon(layout: string) {
+ let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+ layout.indexOf("ImageBox") !== -1 ? faImage :
+ layout.indexOf("Formatted") !== -1 ? faStickyNote :
+ layout.indexOf("Video") !== -1 ? faFilm :
+ layout.indexOf("Collection") !== -1 ? faObjectGroup :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
+ }
+ onPointerEnter = (e: React.PointerEvent) => {
+ this.props.doc.libraryBrush = true;
+ Doc.SetOnPrototype(this.props.doc, "protoBrush", true);
+ }
+ onPointerLeave = (e: React.PointerEvent) => {
+ this.props.doc.libraryBrush = false;
+ Doc.SetOnPrototype(this.props.doc, "protoBrush", false);
+ }
+
+ collectionRef = React.createRef<HTMLDivElement>();
+ startDocDrag = () => {
+ let doc = this.props.doc;
+ const isProto = Doc.GetT(doc, "isPrototype", "boolean", true);
+ if (isProto) {
+ return Doc.MakeDelegate(doc);
+ } else {
+ return Doc.MakeAlias(doc);
+ }
+ }
+ render() {
+ return (
+ <div className="search-item" ref={this.collectionRef} id="result"
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+ onClick={this.onClick} onPointerDown={SetupDrag(this.collectionRef, this.startDocDrag)} >
+ <div className="search-title" id="result" >title: {this.props.doc.title}</div>
+ {/* <div className="search-type" id="result" >Type: {this.props.doc.layout}</div> */}
+ {/* <div className="search-type" >{SearchItem.DocumentIcon(this.layout)}</div> */}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
new file mode 100644
index 000000000..e5b679e24
--- /dev/null
+++ b/src/client/views/TemplateMenu.tsx
@@ -0,0 +1,90 @@
+import { observable, computed, action, trace } from "mobx";
+import React = require("react");
+import { observer } from "mobx-react";
+import './DocumentDecorations.scss';
+import { Template } from "./Templates";
+import { DocumentView } from "./nodes/DocumentView";
+import { List } from "../../new_fields/List";
+import { Doc } from "../../new_fields/Doc";
+import { NumCast } from "../../new_fields/Types";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+@observer
+class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> {
+ render() {
+ if (this.props.template) {
+ return (
+ <li className="templateToggle">
+ <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event, this.props.template)} />
+ {this.props.template.Name}
+ </li>
+ );
+ } else {
+ return (null);
+ }
+ }
+}
+
+export interface TemplateMenuProps {
+ docs: DocumentView[];
+ templates: Map<Template, boolean>;
+}
+
+@observer
+export class TemplateMenu extends React.Component<TemplateMenuProps> {
+ @observable private _hidden: boolean = true;
+
+ constructor(props: TemplateMenuProps) {
+ super(props);
+ }
+
+ @action
+ toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
+ if (event.target.checked) {
+ if (template.Name === "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.addTemplate(template);
+ topDocView.props.Document.subBulletDocs = new List<Doc>(this.props.docs.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+ } else {
+ this.props.docs.map(d => d.addTemplate(template));
+ }
+ this.props.templates.set(template, true);
+ } else {
+ if (template.Name === "Bullet") {
+ let topDocView = this.props.docs[0];
+ topDocView.removeTemplate(template);
+ topDocView.props.Document.subBulletDocs = undefined;
+ } else {
+ this.props.docs.map(d => d.removeTemplate(template));
+ }
+ this.props.templates.set(template, false);
+ }
+ }
+
+ @action
+ componentWillReceiveProps(nextProps: TemplateMenuProps) {
+ // this._templates = nextProps.templates;
+ }
+
+ @action
+ toggleTemplateActivity = (): void => {
+ this._hidden = !this._hidden;
+ }
+
+ render() {
+ let templateMenu: Array<JSX.Element> = [];
+ this.props.templates.forEach((checked, template) =>
+ templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
+
+ return (
+ <div className="templating-menu" >
+ <div className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
+ <ul id="template-list" style={{ display: this._hidden ? "none" : "block" }}>
+ {templateMenu}
+ </ul>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/Templates.tsx b/src/client/views/Templates.tsx
new file mode 100644
index 000000000..5a99b3d90
--- /dev/null
+++ b/src/client/views/Templates.tsx
@@ -0,0 +1,92 @@
+import React = require("react");
+
+export enum TemplatePosition {
+ InnerTop,
+ InnerBottom,
+ InnerRight,
+ InnerLeft,
+ TopRight,
+ OutterTop,
+ OutterBottom,
+ OutterRight,
+ OutterLeft,
+}
+
+export class Template {
+ constructor(name: string, position: TemplatePosition, layout: string) {
+ this._name = name;
+ this._position = position;
+ this._layout = layout;
+ }
+
+ private _name: string;
+ private _position: TemplatePosition;
+ private _layout: string;
+
+ get Name(): string {
+ return this._name;
+ }
+
+ get Position(): TemplatePosition {
+ return this._position;
+ }
+
+ get Layout(): string {
+ return this._layout;
+ }
+}
+
+export namespace Templates {
+ // export const BasicLayout = new Template("Basic layout", "{layout}");
+
+ export const Caption = new Template("Caption", TemplatePosition.OutterBottom,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="screenSpace" style="top: 100%; font-size:14px; background:yellow; width:100%; position:absolute">
+ <FormattedTextBox {...props} fieldKey={"caption"} />
+ </div>
+ </div>` );
+
+ export const TitleOverlay = new Template("TitleOverlay", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div>
+ </div>` );
+
+ export const Title = new Template("Title", TemplatePosition.InnerTop,
+ `<div><div style="height:calc(100% - 25px); margin-top: 25px; width:100%;position:absolute;">{layout}</div>
+ <div style="height:25px; width:100%; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white; ">
+ <span style="text-align:center;width:100%;font-size:20px;position:absolute;overflow:hidden;white-space:nowrap;text-overflow:ellipsis">{props.Document.title}</span>
+ </div></div>` );
+
+ export const Bullet = new Template("Bullet", TemplatePosition.InnerTop,
+ `<div>
+ <div style="height:100%; width:100%;position:absolute;">{layout}</div>
+ <div id="isExpander" style="height:15px; width:15px; margin-left:-16px; pointer-events:all; position:absolute; top: 0; background-color: rgba(0, 0, 0, .4); color: white;">
+ <img id="isExpander" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAOEAAADhCAMAAAAJbSJIAAAAZlBMVEX///8AAABmZmb7+/tYWFhgYGBFRUVSUlL4+Pg/Pz9jY2N5eXmcnJyioqKBgYFzc3NtbW1LS0s3NzfW1taWlpaOjo6IiIgvLy9WVlampqZcXFw5OTlvb28mJiYxMTHe3t7l5eUjIyMY8kIZAAAD2UlEQVR4nO2d61biMBRGW1FBEVHxfp15/5ecOVa5lHxtArmck/Xtn1BotjtNoXQtm4YQQgghhBBCCCGEEEIIIYQQQgghhBBCCCGEEFIrX6UHEA1gsmrneceRjHm7cj28attKFOf/TRyKIliH4vzbZE+xE2zbZYkxRWX5Y9JT/BW0X3G+NtlR3Ahar7jcMtlS3Ba0XXG+Y7JW3BW0XHHZM/lR7AvaVewL/ijuC1pV3Bf8VnQJ2lR0CYriq/Nxg4puwfa1aZ7dz9yUHnEgN26NZ3luWkPFd7fEtHsWVDwpO+YgTgYKCuYn6tAU7TBecaygcGpZEQie7m5luKJPQQFUvCwx5iAuvQoK4KShvSIoOHVtCz7dnOUecxBn7kG/urc2eCz6T9EOcxXDCgpAUetyAwoOCBqrGF5QMKR4mCA8L+pTBIJwkRl95eifJjPHTDYTFQ8vePyrs3BsBfXLzfFHkvKKMY4j1ctNnCmmuGKslfCQT0RZiPdFVmnFmOcy36sDWYn7DU9hxdifRkKuEGQh/pWW0K/QiUlxtUxVxTTXyhQtN6kuI6mpmO5qpxJFIBjl1yMVimmvV4PfrnIq3iYsKICTRj7F9L84gIq38fYwCCj4HnMfRY/FPL8ZFayYo6BQbLlJeZrYpVDFXAUFcMtKWkUgmOhmnwKKOQsK4NaxdIp5CwqZj8X8gv27jNecJ9nZuXtnie/SzjhRQcHkt6Fnq1imoAAUY1csVVDIUrFcQSGDIhC8jriLQZIrli0oXKdVLF1QSFqxfEEBVLyI8NYXCgoKySaqhinakajimxrBRBX1FBQSVNRyDP4SXVGbYHRFfYJN8xhTESwyj5HHHEjEihoLCqDiXfAb3aksKESqCAoqEIxUUW9BAS03E+93mOhcZDYcXVF3QeHBPcI3v4qo4EPiUQcBKr75vHaiv6AAKt6NV0SCqgoKqOKYovpFZgOo+DmsOHkyUlA4ZKKamaIdQPEJK5oqKKCKM7D9zFZBIayiuYICWm5cFWef7o3vs486CP8VdQIEVRcU7sFE7VecgSmqvKDgVxEJqi8ogIof2xVnH2YLCuMT1fAU7RirOPtrXHCsovmCwlDFCgoKWNH4IrMBTdQ/NUzRjiu3CeCq9HAPAVSspaDgX9FkQcG3ollB34qGBf0UTQv6KBoXHFc0LzimWIFg0ywGBBelBxcHXLGKggKqWElBwV2xIkF3xaoEXYqVCe4rVifYV3wpPZwULOouKLzUXVBY1F1QeKm7oLCoXVAqVi7YNM7/F0YIIYQQQgghhBBCCCGEEEIIIYQQQgghhBBCCCG+/ANh4i1CHdc63QAAAABJRU5ErkJggg=="
+ width="15px" height="15px"/>
+ </div>
+ </div>`
+ );
+
+ export function ImageOverlay(width: number, height: number, field: string = "thumbnail") {
+ return (`<div>
+ <div style="height:100%; width:100%; position:absolute;">{layout}</div>
+ <div style="height:auto; width:${width}px; bottom:0; right:0; background:rgba(0,0,0,0.25); position:absolute;overflow:hidden;">
+ <ImageBox id="isExpander" {...props} style="width:100%; height=auto;" PanelWidth={${width}} fieldKey={"${field}"} />
+ </div>
+ </div>`);
+ }
+
+ export const TemplateList: Template[] = [Title, TitleOverlay, Caption, Bullet];
+
+ export function sortTemplates(a: Template, b: Template) {
+ if (a.Position < b.Position) { return -1; }
+ if (a.Position > b.Position) { return 1; }
+ return 0;
+ }
+
+}
+
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
new file mode 100644
index 000000000..6f97e60f8
--- /dev/null
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -0,0 +1,23 @@
+// this file is for overriding all the css from installed node modules
+
+// goldenlayout stuff
+div .lm_header {
+ background: $dark-color;
+ min-height: 2em;
+}
+
+.lm_tab {
+ margin-top: 0.6em !important;
+ padding-top: 0.5em !important;
+ min-height: 1.35em;
+ padding-bottom: 0px;
+ border-radius: 5px;
+ font-family: $sans-serif !important;
+}
+
+.lm_header .lm_controls {
+ right: 1em !important;
+}
+
+// @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out
+// why. Low priority for now but it's bugging me. --Julie
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
new file mode 100644
index 000000000..31bdf213e
--- /dev/null
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -0,0 +1,176 @@
+import { action, computed } 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, 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';
+
+export enum CollectionViewType {
+ Invalid,
+ Freeform,
+ Schema,
+ Docking,
+ Tree,
+}
+
+export interface CollectionRenderProps {
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+}
+
+export interface CollectionViewProps extends FieldViewProps {
+ onContextMenu?: (e: React.MouseEvent) => void;
+ children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null;
+ className?: string;
+ contentRef?: React.Ref<HTMLDivElement>;
+}
+
+
+@observer
+export class CollectionBaseView extends React.Component<CollectionViewProps> {
+ get collectionViewType(): CollectionViewType | undefined {
+ let Document = this.props.Document;
+ let viewField = Cast(Document.viewType, "number");
+ if (viewField !== undefined) {
+ return viewField;
+ } else {
+ return CollectionViewType.Invalid;
+ }
+ }
+
+ active = (): boolean => {
+ var isSelected = this.props.isSelected();
+ var topMost = this.props.isTopMost;
+ return isSelected || this._isChildActive || topMost;
+ }
+
+ //TODO should this be observable?
+ private _isChildActive = false;
+ whenActiveChanged = (isActive: boolean) => {
+ this._isChildActive = isActive;
+ this.props.whenActiveChanged(isActive);
+ }
+
+ createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
+ if (!(documentToAdd instanceof Doc)) {
+ return false;
+ }
+ let data = DocListCast(documentToAdd.data);
+ for (const doc of data) {
+ if (this.createsCycle(doc, containerDocument)) {
+ return true;
+ }
+ }
+ let annots = DocListCast(documentToAdd.annotations);
+ for (const annot of annots) {
+ if (this.createsCycle(annot, containerDocument)) {
+ return true;
+ }
+ }
+ for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
+ if (containerProto[Id] === documentToAdd[Id]) {
+ return true;
+ }
+ }
+ return false;
+ }
+ @computed get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+
+ @action.bound
+ addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
+ let props = this.props;
+ var curPage = NumCast(props.Document.curPage, -1);
+ Doc.SetOnPrototype(doc, "page", curPage);
+ if (curPage >= 0) {
+ Doc.SetOnPrototype(doc, "annotationOn", props.Document);
+ }
+ 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 instanceof Doc && v[Id] === doc[Id])) {
+ alreadyAdded = false;
+ value.push(doc);
+ }
+ } else {
+ 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 (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
+ let zoom = NumCast(this.props.Document.scale, 1);
+ Doc.SetOnPrototype(doc, "zoomBasis", zoom);
+ }
+ }
+ return true;
+ }
+
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ 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++) {
+ let v = value[i];
+ if (v instanceof Doc && v[Id] === doc[Id]) {
+ index = i;
+ break;
+ }
+ }
+ PromiseValue(Cast(doc.annotationOn, Doc)).then((annotationOn) => {
+ if (annotationOn === props.Document) {
+ doc.annotationOn = undefined;
+ }
+ });
+
+ if (index !== -1) {
+ value.splice(index, 1);
+
+ // SelectionManager.DeselectAll()
+ ContextMenu.Instance.clearItems();
+ return true;
+ }
+ return false;
+ }
+
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ if (this.props.Document === targetCollection) {
+ return true;
+ }
+ if (this.removeDocument(doc)) {
+ SelectionManager.DeselectAll();
+ return addDocument(doc);
+ }
+ return false;
+ }
+
+ render() {
+ const props: CollectionRenderProps = {
+ addDocument: this.addDocument,
+ removeDocument: this.removeDocument,
+ moveDocument: this.moveDocument,
+ active: this.active,
+ whenActiveChanged: this.whenActiveChanged,
+ };
+ const viewtype = this.collectionViewType;
+ return (
+ <div className={this.props.className || "collectionView-cont"}
+ style={{ borderRadius: "inherit", pointerEvents: "all" }}
+ onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
+ {viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 2706c3272..0e7e0afa7 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,9 +1,30 @@
+@import "../../views/globalCssVariables.scss";
+
.collectiondockingview-content {
height: 100%;
}
+.lm_active .messageCounter{
+ color:white;
+ background: #999999;
+}
+.messageCounter {
+ width:18px;
+ height:20px;
+ text-align: center;
+ border-radius: 20px;
+ margin-left: 5px;
+ transform: translate(0px, -8px);
+ display: inline-block;
+ background: transparent;
+ border: 1px #999999 solid;
+}
.collectiondockingview-container {
- position: relative;
+ width: 100%;
+ height: 100%;
+ border-style: solid;
+ border-width: $COLLECTION_BORDER_WIDTH;
+ position: absolute;
top: 0;
left: 0;
overflow: hidden;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6a0404663..28624e020 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,41 +1,46 @@
-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 } from "mobx";
+import { action, observable, reaction, Lambda } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
import Measure from "react-measure";
-import { FieldId, Opt, Field } from "../../../fields/Field";
-import { Utils } from "../../../Utils";
-import { Server } from "../../Server";
-import { undoBatch } from "../../util/UndoManager";
+import * as GoldenLayout from "../../../client/goldenLayout";
+import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
+import { FieldId, Id } from "../../../new_fields/RefField";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+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 { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { SubCollectionViewProps } from "./CollectionSubView";
import React = require("react");
-import { SubCollectionViewProps } from "./CollectionViewBase";
+import { ParentDocSelector } from './ParentDocumentSelector';
+import { DocumentManager } from '../../util/DocumentManager';
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
public static Instance: CollectionDockingView;
- public static makeDocumentConfig(document: Document) {
+ public static makeDocumentConfig(document: Doc, width?: number) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
- title: document.Title,
+ title: document.title,
+ width: width,
props: {
- documentId: document.Id,
+ documentId: document[Id],
//collectionDockingView: CollectionDockingView.Instance
}
- }
+ };
}
private _goldenLayout: any = null;
private _containerRef = React.createRef<HTMLDivElement>();
- private _fullScreen: any = null;
private _flush: boolean = false;
+ private _ignoreStateChange = "";
constructor(props: SubCollectionViewProps) {
super(props);
@@ -43,43 +48,84 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
}
- public StartOtherDrag(dragDoc: Document, e: any) {
- this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })
+ hack: boolean = false;
+ undohack: any = null;
+ public StartOtherDrag(dragDocs: Doc[], e: any) {
+ this.hack = true;
+ this.undohack = UndoManager.StartBatch("goldenDrag");
+ dragDocs.map(dragDoc =>
+ this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+ onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
}
@action
- public OpenFullScreen(document: Document) {
+ public OpenFullScreen(document: Doc) {
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
- }
+ };
var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
this._goldenLayout.root.contentItems[0].addChild(docconfig);
docconfig.callDownwards('_$init');
this._goldenLayout._$maximiseItem(docconfig);
- this._fullScreen = docconfig;
+ this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
this.stateChanged();
}
+
+ @undoBatch
@action
- public CloseFullScreen() {
- if (this._fullScreen) {
- this._goldenLayout._$minimiseItem(this._fullScreen);
- this._goldenLayout.root.contentItems[0].removeChild(this._fullScreen);
- this._fullScreen = null;
+ 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: Document, minimize: boolean = false) {
- this._goldenLayout.emit('stateChanged');
+ public AddRightSplit(document: Doc, minimize: boolean = false) {
+ let docs = Cast(this.props.Document.data, listSpec(Doc));
+ if (docs) {
+ docs.push(document);
+ }
let newItemStackConfig = {
type: 'stack',
content: [CollectionDockingView.makeDocumentConfig(document)]
- }
+ };
var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
@@ -94,32 +140,45 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
newRow.addChild(newContentItem, undefined, true);
newRow.addChild(collayout, 0, true);
- collayout.config["width"] = 50;
- newContentItem.config["width"] = 50;
+ collayout.config.width = 50;
+ 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.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 = this.props.Document.GetText(KeyStore.Data, "");
+ var config = StrCast(this.props.Document.dockingConfig);
if (config) {
if (!this._goldenLayout) {
this._goldenLayout = new GoldenLayout(JSON.parse(config));
}
else {
- if (config == JSON.stringify(this._goldenLayout.toConfig()))
+ if (config === JSON.stringify(this._goldenLayout.toConfig())) {
return;
+ }
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();
@@ -127,6 +186,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;
@@ -140,22 +200,39 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.init();
}
}
+ reactionDisposer?: Lambda;
componentDidMount: () => void = () => {
if (this._containerRef.current) {
- reaction(
- () => this.props.Document.GetText(KeyStore.Data, ""),
- () => this.setupGoldenLayout(), { fireImmediately: true });
+ 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 = "";
+ }, { fireImmediately: true });
window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
}
}
componentWillUnmount: () => void = () => {
- this._goldenLayout.unbind('itemDropped', this.itemDropped);
- this._goldenLayout.unbind('tabCreated', this.tabCreated);
- this._goldenLayout.unbind('stackCreated', this.stackCreated);
- this._goldenLayout.destroy();
+ try {
+ 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) => {
@@ -175,7 +252,36 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@action
onPointerDown = (e: React.PointerEvent): void => {
var className = (e.target as any).className;
- if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") {
+ if (className === "messageCounter") {
+ e.stopPropagation();
+ e.preventDefault();
+ let x = e.clientX;
+ let y = e.clientY;
+ let docid = (e.target as any).DashDocId;
+ let tab = (e.target as any).parentElement as HTMLElement;
+ DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
+ (sourceDoc instanceof Doc) && DragLinksAsDocuments(tab, x, y, sourceDoc)));
+ } else
+ if ((className === "lm_title" || className === "lm_tab lm_active") && !e.shiftKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ let x = e.clientX;
+ let y = e.clientY;
+ let docid = (e.target as any).DashDocId;
+ let tab = (e.target as any).parentElement as HTMLElement;
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
+ {
+ handlers: {
+ dragComplete: emptyFunction,
+ },
+ hideSource: false
+ });
+ }
+ }));
+ }
+ if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {
this._flush = true;
}
if (this.props.active()) {
@@ -185,20 +291,77 @@ 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.SetText(KeyStore.Data, json)
+ this.props.Document.dockingConfig = json;
+ if (this.undohack && !this.hack) {
+ this.undohack.end();
+ this.undohack = undefined;
+ }
+ this.hack = false;
}
itemDropped = () => {
this.stateChanged();
}
- tabCreated = (tab: any) => {
+
+ htmlToElement(html: string) {
+ var template = document.createElement('template');
+ html = html.trim(); // Never return a text node of whitespace as the result
+ template.innerHTML = html;
+ return template.content.firstChild;
+ }
+
+ tabCreated = async (tab: any) => {
+ if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
+ 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");
+ ReactDOM.render(<ParentDocSelector Document={doc} />, upDiv);
+ tab.reactComponents = [upDiv];
+ tab.element.append(upDiv);
+ counter.DashDocId = tab.contentItem.config.props.documentId;
+ tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+ () => {
+ 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.closeElement.off('click') //unbind the current click handler
- .click(function () {
+ .click(async function () {
+ if (tab.reactionDisposer) {
+ tab.reactionDisposer();
+ }
+ 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
@@ -206,70 +369,103 @@ 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 () {
+ 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");
+ }));
}
render() {
return (
<div className="collectiondockingview-container" id="menuContainer"
- onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef}
- style={{
- width: "100%",
- height: "100%",
- borderStyle: "solid",
- borderWidth: `${COLLECTION_BORDER_WIDTH}px`,
- }} />
+ onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef} />
);
}
+
}
interface DockedFrameProps {
- documentId: FieldId,
+ documentId: FieldId;
//collectionDockingView: CollectionDockingView
}
@observer
export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
-
- private _mainCont = React.createRef<HTMLDivElement>();
+ _mainCont = React.createRef<HTMLDivElement>();
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
- @observable private _document: Opt<Document>;
-
+ @observable private _document: Opt<Doc>;
+ _stack: any;
constructor(props: any) {
super(props);
- Server.GetField(this.props.documentId, action((f: Opt<Field>) => this._document = f as Document));
+ this._stack = (this.props as any).glContainer.parent.parent;
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
}
- private _nativeWidth = () => { return this._document!.GetNumber(KeyStore.NativeWidth, this._panelWidth); }
- private _nativeHeight = () => { return this._document!.GetNumber(KeyStore.NativeHeight, this._panelHeight); }
- private _contentScaling = () => { return this._panelWidth / (this._nativeWidth() ? this._nativeWidth() : this._panelWidth); }
+ nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
+ nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight);
+ contentScaling = () => {
+ const nativeH = this.nativeHeight();
+ const nativeW = this.nativeWidth();
+ let wscale = this._panelWidth / nativeW;
+ return wscale * nativeH > this._panelHeight ? this._panelHeight / nativeH : wscale;
+ }
ScreenToLocalTransform = () => {
- let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current!);
- return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this._contentScaling())
+ if (this._mainCont.current && this._mainCont.current.children) {
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._mainCont.current.children[0].firstChild as HTMLElement);
+ scale = Utils.GetScreenTransform(this._mainCont.current).scale;
+ return CollectionDockingView.Instance.props.ScreenToLocalTransform().translate(-translateX, -translateY).scale(scale / this.contentScaling());
+ }
+ return Transform.Identity();
}
+ get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
- render() {
- if (!this._document)
+ addDocTab = (doc: Doc) => {
+ CollectionDockingView.Instance.AddTab(this._stack, doc);
+ }
+ get content() {
+ if (!this._document) {
return (null);
- var content =
- <div className="collectionDockingView-content" ref={this._mainCont}>
- <DocumentView key={this._document.Id} Document={this._document}
- AddDocument={undefined}
- RemoveDocument={undefined}
- ContentScaling={this._contentScaling}
- PanelWidth={this._nativeWidth}
- PanelHeight={this._nativeHeight}
+ }
+ 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}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={undefined}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.nativeWidth}
+ PanelHeight={this.nativeHeight}
ScreenToLocalTransform={this.ScreenToLocalTransform}
isTopMost={true}
- SelectOnLoad={false}
- focus={(doc: Document) => { }}
+ selectOnLoad={false}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ addDocTab={this.addDocTab}
ContainingCollectionView={undefined} />
- </div>
+ </div >);
+ }
- return <Measure onResize={action((r: any) => { this._panelWidth = r.entry.width; this._panelHeight = r.entry.height; })}>
- {({ measureRef }) => <div ref={measureRef}> {content} </div>}
- </Measure>
+ render() {
+ let theContent = this.content;
+ return !this._document ? (null) :
+ <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
+ {({ measureRef }) => <div ref={measureRef}> {theContent} </div>}
+ </Measure>;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
deleted file mode 100644
index d487cd7ce..000000000
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ /dev/null
@@ -1,75 +0,0 @@
-.collectionfreeformview-container {
-
- .collectionfreeformview > .jsx-parser{
- position:absolute;
- height: 100%;
- width: 100%;
- }
-
- border-style: solid;
- box-sizing: border-box;
- position: relative;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- .collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width:100%;
- height: 100%;
- }
-}
-.collectionfreeformview-marquee{
- border-style: dashed;
- box-sizing: border-box;
- position: absolute;
- border-width: 1px;
- border-color: black;
-}
-.collectionfreeformview-overlay {
-
- .collectionfreeformview > .jsx-parser{
- position:absolute;
- height: 100%;
- }
- .formattedTextBox-cont {
- background:yellow;
- }
-
- border-style: solid;
- box-sizing: border-box;
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: hidden;
- .collectionfreeformview {
- position: absolute;
- top: 0;
- left: 0;
- width:100%;
- height: 100%;
- }
-}
-
-.border {
- border-style: solid;
- box-sizing: border-box;
- width: 100%;
- height: 100%;
-}
-
-//this is an animation for the blinking cursor!
-@keyframes blink {
- 0% {opacity: 0}
- 49%{opacity: 0}
- 50% {opacity: 1}
-}
-
-#prevCursor {
- animation: blink 1s infinite;
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
deleted file mode 100644
index b0cd7e017..000000000
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ /dev/null
@@ -1,394 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { TextField } from "../../../fields/TextField";
-import { Documents } from "../../documents/Documents";
-import { DragManager } from "../../util/DragManager";
-import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionView } from "../collections/CollectionView";
-import { InkingCanvas } from "../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DocumentView } from "../nodes/DocumentView";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
-import { ImageBox } from "../nodes/ImageBox";
-import { KeyValueBox } from "../nodes/KeyValueBox";
-import { PDFBox } from "../nodes/PDFBox";
-import { WebBox } from "../nodes/WebBox";
-import "./CollectionFreeFormView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { CollectionViewBase } from "./CollectionViewBase";
-import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
-
-@observer
-export class CollectionFreeFormView extends CollectionViewBase {
- private _canvasRef = React.createRef<HTMLDivElement>();
- @observable
- private _lastX: number = 0;
- @observable
- private _lastY: number = 0;
- private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
-
- @observable
- private _downX: number = 0;
- @observable
- private _downY: number = 0;
-
- //determines whether the blinking cursor for indicating whether a text will be made on key down is visible
- @observable
- private _previewCursorVisible: boolean = false;
-
- @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
- @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
- @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
- @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
- @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
- @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
- @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; } // shift so pan position is at center of window for non-overlay collections
- @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections
-
- @undoBatch
- @action
- drop = (e: Event, de: DragManager.DropEvent) => {
- super.drop(e, de);
- const docView: DocumentView = de.data["documentView"];
- let doc: Document = docView ? docView.props.Document : de.data["document"];
- if (doc) {
- let screenX = de.x - (de.data["xOffset"] as number || 0);
- let screenY = de.y - (de.data["yOffset"] as number || 0);
- const [x, y] = this.getTransform().transformPoint(screenX, screenY);
- doc.SetNumber(KeyStore.X, x);
- doc.SetNumber(KeyStore.Y, y);
- this.bringToFront(doc);
- }
- }
-
- @observable
- _marquee = false;
-
- @action
- onPointerDown = (e: React.PointerEvent): void => {
- if (((e.button === 2 && this.props.active()) || !e.defaultPrevented) && !e.shiftKey &&
- (!this.isAnnotationOverlay || this.zoomScaling != 1 || e.button == 0)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- this._lastX = e.pageX;
- this._lastY = e.pageY;
- this._downX = e.pageX;
- this._downY = e.pageY;
- }
- }
-
- @action
- onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- e.stopPropagation();
-
- if (this._marquee) {
- if (!e.shiftKey) {
- SelectionManager.DeselectAll();
- }
- var selectedDocs = this.marqueeSelect();
- selectedDocs.map(s => this.props.CollectionView.SelectedDocs.push(s.Id));
- this._marquee = false;
- }
- else if (!this._marquee && Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3) {
- //show preview text cursor on tap
- this._previewCursorVisible = true;
- //select is not already selected
- if (!this.props.isSelected()) {
- this.props.select(false);
- }
- }
-
- }
-
- intersectRect(r1: { left: number, right: number, top: number, bottom: number },
- r2: { left: number, right: number, top: number, bottom: number }) {
- return !(r2.left > r1.right ||
- r2.right < r1.left ||
- r2.top > r1.bottom ||
- r2.bottom < r1.top);
- }
-
- marqueeSelect() {
- this.props.CollectionView.SelectedDocs.length = 0;
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
- let p = this.getTransform().transformPoint(this._downX, this._downY);
- let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- let selRect = { left: p[0], top: p[1], right: p[0] + v[0], bottom: p[1] + v[1] }
-
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
- const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
- let selection: Document[] = [];
- if (lvalue && lvalue != FieldWaiting) {
- lvalue.Data.map(doc => {
- var page = doc.GetNumber(KeyStore.Page, 0);
- if (page == curPage || page == 0) {
- var x = doc.GetNumber(KeyStore.X, 0);
- var y = doc.GetNumber(KeyStore.Y, 0);
- var w = doc.GetNumber(KeyStore.Width, 0);
- var h = doc.GetNumber(KeyStore.Height, 0);
- if (this.intersectRect({ left: x, top: y, right: x + w, bottom: y + h }, selRect))
- selection.push(doc)
- }
- })
- }
- return selection;
- }
-
- @action
- onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && this.props.active()) {
- e.stopPropagation();
- e.preventDefault();
- let wasMarquee = this._marquee;
- this._marquee = e.buttons != 2;
- if (this._marquee && !wasMarquee) {
- document.addEventListener("keydown", this.marqueeCommand);
- }
-
- if (!this._marquee) {
- let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
- let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
- let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- this._previewCursorVisible = false;
- this.SetPan(x - dx, y - dy);
- }
- }
- this._lastX = e.pageX;
- this._lastY = e.pageY;
- }
-
- @action
- marqueeCommand = (e: KeyboardEvent) => {
- if (e.key == "Backspace") {
- this.marqueeSelect().map(d => this.props.removeDocument(d));
- }
- if (e.key == "c") {
- }
- }
-
- @action
- onPointerWheel = (e: React.WheelEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- let coefficient = 1000;
-
- if (e.ctrlKey) {
- var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
- const coefficient = 1000;
- let deltaScale = (1 - (e.deltaY / coefficient));
- this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale);
- this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale);
- e.stopPropagation();
- e.preventDefault();
- } else {
- // if (modes[e.deltaMode] == 'pixels') coefficient = 50;
- // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height??
- let transform = this.getTransform();
-
- let deltaScale = (1 - (e.deltaY / coefficient));
- if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay)
- deltaScale = 1 / this.zoomScaling;
- let [x, y] = transform.transformPoint(e.clientX, e.clientY);
-
- let localTransform = this.getLocalTransform()
- localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
- // console.log(localTransform)
-
- this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
- this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale);
- }
- }
-
- @action
- private SetPan(panX: number, panY: number) {
- var x1 = this.getLocalTransform().inverse().Scale;
- var x2 = this.getTransform().inverse().Scale;
- const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
- this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
- this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
- }
-
- @action
- onDrop = (e: React.DragEvent): void => {
- var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- super.onDrop(e, { x: pt[0], y: pt[1] });
- }
-
- onDragOver = (): void => {
- }
-
- @action
- onKeyDown = (e: React.KeyboardEvent<Element>) => {
- //if not these keys, make a textbox if preview cursor is active!
- if (!e.ctrlKey && !e.altKey) {
- if (this._previewCursorVisible) {
- //make textbox and add it to this collection
- let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY);
- let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" });
- // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself
- this._selectOnLoaded = newBox.Id;
- //set text to be the typed key and get focus on text box
- this.props.CollectionView.addDocument(newBox);
- //remove cursor from screen
- this._previewCursorVisible = false;
- }
- }
- }
-
- @action
- bringToFront(doc: Document) {
- const { fieldKey: fieldKey, Document: Document } = this.props;
-
- const value: Document[] = Document.GetList<Document>(fieldKey, []).slice();
- value.sort((doc1, doc2) => {
- if (doc1 === doc) {
- return 1;
- }
- if (doc2 === doc) {
- return -1;
- }
- return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
- }).map((doc, index) => {
- doc.SetNumber(KeyStore.ZIndex, index + 1)
- });
- }
-
- @computed get backgroundLayout(): string | undefined {
- let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
- if (field && field !== "<Waiting>") {
- return field.Data;
- }
- }
- @computed get overlayLayout(): string | undefined {
- let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField);
- if (field && field !== "<Waiting>") {
- return field.Data;
- }
- }
-
- focusDocument = (doc: Document) => {
- let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2;
- let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2;
- this.SetPan(x, y);
- this.props.focus(this.props.Document);
- }
-
-
- @computed
- get views() {
- var curPage = this.props.Document.GetNumber(KeyStore.CurPage, 1);
- const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
- if (lvalue && lvalue != FieldWaiting) {
- return lvalue.Data.map(doc => {
- var page = doc.GetNumber(KeyStore.Page, 0);
- return (page != curPage && page != 0) ? (null) :
- (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
- AddDocument={this.props.addDocument}
- RemoveDocument={this.props.removeDocument}
- ScreenToLocalTransform={this.getTransform}
- isTopMost={false}
- SelectOnLoad={doc.Id === this._selectOnLoaded}
- ContentScaling={this.noScaling}
- PanelWidth={doc.Width}
- PanelHeight={doc.Height}
- ContainingCollectionView={this.props.CollectionView}
- focus={this.focusDocument}
- />);
- })
- }
- return null;
- }
-
- @computed
- get backgroundView() {
- return !this.backgroundLayout ? (null) :
- (<JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
- bindings={this.props.bindings}
- jsx={this.backgroundLayout}
- showWarnings={true}
- onError={(test: any) => console.log(test)}
- />);
- }
- @computed
- get overlayView() {
- return !this.overlayLayout ? (null) :
- (<JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, PDFBox }}
- bindings={this.props.bindings}
- jsx={this.overlayLayout}
- showWarnings={true}
- onError={(test: any) => console.log(test)}
- />);
- }
-
- getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform())
- getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY);
- noScaling = () => 1;
-
- //when focus is lost, this will remove the preview cursor
- @action
- onBlur = (e: React.FocusEvent<HTMLDivElement>): void => {
- this._previewCursorVisible = false;
- }
-
- render() {
- //determines whether preview text cursor should be visible (ie when user taps this collection it should)
- let cursor = null;
- if (this._previewCursorVisible) {
- //get local position and place cursor there!
- let [x, y] = this.getTransform().transformPoint(this._downX, this._downY);
- cursor = <div id="prevCursor" onKeyPress={this.onKeyDown} style={{ color: "black", position: "absolute", transformOrigin: "left top", transform: `translate(${x}px, ${y}px)` }}>I</div>
- }
-
- let p = this.getTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
- let v = this.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- var marquee = this._marquee ? <div className="collectionfreeformview-marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }}></div> : (null);
-
- let [dx, dy] = [this.centeringShiftX, this.centeringShiftY];
-
- const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
- const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0);
-
- return (
- <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
- onPointerDown={this.onPointerDown}
- onKeyPress={this.onKeyDown}
- onWheel={this.onPointerWheel}
- onDrop={this.onDrop.bind(this)}
- onDragOver={this.onDragOver}
- onBlur={this.onBlur}
- style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
- tabIndex={0}
- ref={this.createDropTarget}>
- <div className="collectionfreeformview"
- style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
- ref={this._canvasRef}>
- {this.backgroundView}
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
- {cursor}
- {this.views}
- {marquee}
- </div>
- {this.overlayView}
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
new file mode 100644
index 000000000..f6fb79582
--- /dev/null
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -0,0 +1,49 @@
+.collectionPdfView-buttonTray {
+ top : 15px;
+ left : 20px;
+ position: relative;
+ transform-origin: left top;
+ position: absolute;
+}
+.collectionPdfView-thumb {
+ width:25px;
+ height:25px;
+ transform-origin: left top;
+ position: absolute;
+ background: darkgray;
+}
+.collectionPdfView-slider {
+ width:25px;
+ height:25px;
+ transform-origin: left top;
+ position: absolute;
+ background: lightgray;
+}
+.collectionPdfView-cont{
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left:0;
+}
+.collectionPdfView-cont-dragging {
+ span {
+ user-select: none;
+ }
+}
+.collectionPdfView-backward {
+ color : white;
+ font-size: 24px;
+ top :0px;
+ left : 0px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+}
+.collectionPdfView-forward {
+ color : white;
+ font-size: 24px;
+ 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 bcb1cd2f7..a6614da21 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -1,57 +1,85 @@
-import { action, computed } from "mobx";
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
import { ContextMenu } from "../ContextMenu";
-import { CollectionView, CollectionViewType } from "./CollectionView";
-import { CollectionViewProps } from "./CollectionViewBase";
+import "./CollectionPDFView.scss";
import React = require("react");
-import { FieldId } from "../../../fields/Field";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+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";
@observer
-export class CollectionPDFView extends React.Component<CollectionViewProps> {
+export class CollectionPDFView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string = "DataKey") {
- return `<${CollectionPDFView.name} Document={Document}
- ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
- isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
+ public static LayoutString(fieldKey: string = "data") {
+ return FieldView.LayoutString(CollectionPDFView, fieldKey);
}
+ @observable _inThumb = false;
- public SelectedDocs: FieldId[] = []
- @action onPageBack = () => this.curPage > 1 ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage - 1) : 0;
- @action onPageForward = () => this.curPage < this.numPages ? this.props.Document.SetNumber(KeyStore.CurPage, this.curPage + 1) : 0;
+ private set curPage(value: number) { this.props.Document.curPage = value; }
+ private get curPage() { return NumCast(this.props.Document.curPage, -1); }
+ private get numPages() { return NumCast(this.props.Document.numPages); }
+ @action onPageBack = () => this.curPage > 1 ? (this.props.Document.curPage = this.curPage - 1) : -1;
+ @action onPageForward = () => this.curPage < this.numPages ? (this.props.Document.curPage = this.curPage + 1) : -1;
- @computed private get curPage() { return this.props.Document.GetNumber(KeyStore.CurPage, 0); }
- @computed private get numPages() { return this.props.Document.GetNumber(KeyStore.NumPages, 0); }
- @computed private get uIButtons() {
+ @action
+ onThumbDown = (e: React.PointerEvent) => {
+ document.addEventListener("pointermove", this.onThumbMove, false);
+ document.addEventListener("pointerup", this.onThumbUp, false);
+ e.stopPropagation();
+ this._inThumb = true;
+ }
+ @action
+ onThumbMove = (e: PointerEvent) => {
+ let pso = (e.clientY - (e as any).target.parentElement.getBoundingClientRect().top) / (e as any).target.parentElement.getBoundingClientRect().height;
+ this.curPage = Math.trunc(Math.min(this.numPages, pso * this.numPages + 1));
+ e.stopPropagation();
+ }
+ @action
+ onThumbUp = (e: PointerEvent) => {
+ this._inThumb = false;
+ document.removeEventListener("pointermove", this.onThumbMove);
+ document.removeEventListener("pointerup", this.onThumbUp);
+ }
+ nativeWidth = () => NumCast(this.props.Document.nativeWidth);
+ nativeHeight = () => NumCast(this.props.Document.nativeHeight);
+ private get uIButtons() {
+ let ratio = (this.curPage - 1) / this.numPages * 100;
return (
- <div className="pdfBox-buttonTray" key="tray">
- <button className="pdfButton" onClick={this.onPageBack}>{"<"}</button>
- <button className="pdfButton" onClick={this.onPageForward}>{">"}</button>
- </div>);
+ <div className="collectionPdfView-buttonTray" key="tray" style={{ height: "100%" }}>
+ <button className="collectionPdfView-backward" onClick={this.onPageBack}>{"<"}</button>
+ <button className="collectionPdfView-forward" onClick={this.onPageForward}>{">"}</button>
+ <div className="collectionPdfView-slider" onPointerDown={this.onThumbDown} style={{ top: 60, left: -20, width: 50, height: `calc(100% - 80px)` }} >
+ <div className="collectionPdfView-thumb" onPointerDown={this.onThumbDown} style={{ top: `${ratio}%`, width: 50, height: 50 }} />
+ </div>
+ </div>
+ );
}
- // "inherited" CollectionView API starts here...
-
- public active: () => boolean = () => CollectionView.Active(this);
-
- addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
- removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
-
- specificContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "PDFOptions", event: () => { }, icon: "file-pdf" });
+ 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, icon: "file-pdf" });
}
}
- get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
- get subView(): any { return CollectionView.SubView(this); }
+ private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ let props = { ...this.props, ...renderProps };
+ return (
+ <>
+ <CollectionFreeFormView {...props} CollectionView={this} />
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </>
+ );
+ }
render() {
- return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}>
- {this.subView}
- {this.props.isSelected() ? this.uIButtons : (null)}
- </div>)
+ return (
+ <CollectionBaseView {...this.props} className={`collectionPdfView-cont${this._inThumb ? "-dragging" : ""}`} onContextMenu={this.onContextMenu}>
+ {this.subView}
+ </CollectionBaseView>
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index d40e6d314..5e9d2ac67 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -1,54 +1,110 @@
+@import "../globalCssVariables";
+
.collectionSchemaView-container {
+ border-width: $COLLECTION_BORDER_WIDTH;
+ border-color : $intermediate-color;
border-style: solid;
+ border-radius: $border-radius;
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
+
+ .collectionSchemaView-cellContents {
+ height: $MAX_ROW_HEIGHT;
+ img {
+ width:auto;
+ max-height: $MAX_ROW_HEIGHT;
+ }
+ }
+
.collectionSchemaView-previewRegion {
- position: relative;
- background: black;
- float: left;
+ position: relative;
+ background: $light-color;
+ float: left;
height: 100%;
+ .collectionSchemaView-previewDoc {
+ height: 100%;
+ width: 100%;
+ position: absolute;
+ }
+ .collectionSchemaView-input {
+ position: absolute;
+ max-width: 150px;
+ width: 100%;
+ bottom: 0px;
+ }
+ .documentView-node:first-child {
+ position: relative;
+ background: $light-color;
+ }
}
.collectionSchemaView-previewHandle {
position: absolute;
- height: 37px;
- width: 20px;
+ height: 15px;
+ width: 15px;
z-index: 20;
right: 0;
- top: 0;
+ top: 20px;
background: Black ;
}
.collectionSchemaView-dividerDragger{
position: relative;
background: black;
float: left;
+ height: 37px;
+ width: 20px;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ background: $main-accent;
+ }
+ .collectionSchemaView-columnsHandle {
+ position: absolute;
+ height: 37px;
+ width: 20px;
+ z-index: 20;
+ left: 0;
+ bottom: 0;
+ background: $main-accent;
+ }
+ .collectionSchemaView-colDividerDragger {
+ position: relative;
+ box-sizing: border-box;
+ border-top: 1px solid $intermediate-color;
+ border-bottom: 1px solid $intermediate-color;
+ float: top;
+ width: 100%;
+ }
+ .collectionSchemaView-dividerDragger {
+ position: relative;
+ box-sizing: border-box;
+ border-left: 1px solid $intermediate-color;
+ border-right: 1px solid $intermediate-color;
+ float: left;
height: 100%;
}
.collectionSchemaView-tableContainer {
position: relative;
float: left;
- height: 100%;
+ height: 100%;
}
-
.ReactTable {
- position: absolute;
- // display: inline-block;
- // overflow: auto;
+ // position: absolute; // display: inline-block;
+ // overflow: auto;
width: 100%;
height: 100%;
- background: white;
+ background: $light-color;
box-sizing: border-box;
+ border: none !important;
.rt-table {
overflow-y: auto;
overflow-x: auto;
height: 100%;
-
display: -webkit-inline-box;
- direction: ltr;
- // direction:rtl;
+ direction: ltr; // direction:rtl;
// display:block;
}
.rt-tbody {
@@ -57,45 +113,113 @@
}
.rt-tr-group {
direction: ltr;
- max-height: 44px;
+ max-height: $MAX_ROW_HEIGHT;
}
.rt-td {
- border-width: 1;
- border-right-color: #aaa;
+ border-width: 1px;
+ border-right-color: $intermediate-color;
.imageBox-cont {
- position:relative;
- max-height:100%;
+ position: relative;
+ max-height: 100%;
}
.imageBox-cont img {
object-fit: contain;
max-width: 100%;
- height: 100%
+ height: 100%;
+ }
+ .videoBox-cont {
+ object-fit: contain;
+ width: auto;
+ height: 100%;
}
- }
- .rt-tr-group {
- border-width: 1;
- border-bottom-color: #aaa
}
}
.ReactTable .rt-thead.-header {
- background:grey;
- }
- .ReactTable .rt-th, .ReactTable .rt-td {
- max-height: 44;
+ background: $intermediate-color;
+ color: $light-color;
+ 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;
padding: 3px 7px;
+ font-size: 13px;
+ text-align: center;
}
.ReactTable .rt-tbody .rt-tr-group:last-child {
- border-bottom: grey;
+ border-bottom: $intermediate-color;
border-bottom-style: solid;
border-bottom-width: 1;
}
+ .documentView-node-topmost {
+ text-align:left;
+ transform-origin: center top;
+ display: inline-block;
+ }
.documentView-node:first-child {
- background: grey;
- .imageBox-cont img {
- object-fit: contain;
- }
+ background: $light-color;
}
}
+//options menu styling
+#schemaOptionsMenuBtn {
+ position: absolute;
+ height: 20px;
+ width: 20px;
+ border-radius: 50%;
+ z-index: 21;
+ right: 4px;
+ top: 4px;
+ pointer-events: auto;
+ background-color:black;
+ display:inline-block;
+ padding: 0px;
+ font-size: 100%;
+}
+
+ul {
+ list-style-type: disc;
+}
+
+#schema-options-header {
+ text-align: center;
+ padding: 0px;
+ margin: 0px;
+}
+.schema-options-subHeader {
+ color: $intermediate-color;
+ margin-bottom: 5px;
+}
+#schemaOptionsMenuBtn:hover {
+ transform: scale(1.15);
+}
+
+#preview-schema-checkbox-div {
+ margin-left: 20px;
+ font-size: 12px;
+}
+
+ #options-flyout-div {
+ text-align: left;
+ padding:0px;
+ z-index: 100;
+ font-family: $sans-serif;
+ padding-left: 5px;
+ }
+
+ #schema-col-checklist {
+ overflow: scroll;
+ text-align: left;
+ //background-color: $light-color-secondary;
+ line-height: 25px;
+ max-height: 175px;
+ font-family: $sans-serif;
+ font-size: 12px;
+ }
+
.Resizer {
box-sizing: border-box;
@@ -204,4 +328,12 @@
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
+}
+
+.-even {
+ background: $light-color !important;
+}
+
+.-odd {
+ background: $light-color-secondary !important;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 04f017378..e2b90e26d 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,84 +1,131 @@
-import React = require("react")
-import { action, observable } from "mobx";
+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 { observer } from "mobx-react";
-import Measure from "react-measure";
import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
+import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
import "react-table/react-table.css";
-import { Document } from "../../../fields/Document";
-import { Field } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { CompileScript, ToField } from "../../util/Scripting";
+import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
+import { SetupDrag } from "../../util/DragManager";
+import { CompileScript } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
-import { ContextMenu } from "../ContextMenu";
+import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss";
+import { anchorPoints, Flyout } from "../DocumentDecorations";
+import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { DocumentView } from "../nodes/DocumentView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
import "./CollectionSchemaView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { CollectionViewBase } from "./CollectionViewBase";
-import { setupDrag } from "../../util/DragManager";
+import { CollectionSubView } from "./CollectionSubView";
+import { Opt, Field, 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 { Gateway } from "../../northstar/manager/Gateway";
+import { Docs } from "../../documents/Documents";
+import { ContextMenu } from "../ContextMenu";
+
// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
@observer
-export class CollectionSchemaView extends CollectionViewBase {
- private _mainCont = React.createRef<HTMLDivElement>();
- private DIVIDER_WIDTH = 5;
-
- @observable _contentScaling = 1; // used to transfer the dimensions of the content pane in the DOM to the ContentScaling prop of the DocumentView
- @observable _dividerX = 0;
- @observable _panelWidth = 0;
- @observable _panelHeight = 0;
+class KeyToggle extends React.Component<{ keyName: string, checked: boolean, toggle: (key: string) => void }> {
+ constructor(props: any) {
+ super(props);
+ }
+
+ render() {
+ return (
+ <div key={this.props.keyName}>
+ <input type="checkbox" checked={this.props.checked} onChange={() => this.props.toggle(this.props.keyName)} />
+ {this.props.keyName}
+ </div>
+ );
+ }
+}
+
+@observer
+export class CollectionSchemaView extends CollectionSubView(doc => doc) {
+ private _mainCont?: HTMLDivElement;
+ private _startSplitPercent = 0;
+ private DIVIDER_WIDTH = 4;
+
+ @observable _columns: Array<string> = ["title", "data", "author"];
@observable _selectedIndex = 0;
- @observable _splitPercentage: number = 50;
+ @observable _columnsPercentage = 0;
+ @observable _keys: string[] = [];
+ @observable _newKeyName: string = "";
+
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); }
+ @computed get columns() { return Cast(this.props.Document.schemaColumns, listSpec("string"), []); }
+ @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
renderCell = (rowProps: CellInfo) => {
let props: FieldViewProps = {
- doc: rowProps.value[0],
+ Document: rowProps.value[0],
fieldKey: rowProps.value[1],
- isSelected: () => false,
- select: () => { },
+ ContainingCollectionView: this.props.CollectionView,
+ isSelected: returnFalse,
+ select: emptyFunction,
isTopMost: false,
- bindings: {},
selectOnLoad: false,
- }
- let contents = (
- <FieldView {...props} />
- )
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ active: returnFalse,
+ whenActiveChanged: emptyFunction,
+ PanelHeight: returnZero,
+ PanelWidth: returnZero,
+ };
+ let fieldContentView = <FieldView {...props} />;
let reference = React.createRef<HTMLDivElement>();
- let onItemDown = setupDrag(reference, () => props.doc);
+ let onItemDown = (e: React.PointerEvent) =>
+ (this.props.CollectionView.props.isSelected() ?
+ SetupDrag(reference, () => props.Document, this.props.moveDocument)(e) : undefined);
+ let applyToDoc = (doc: Doc, run: (args?: { [name: string]: any }) => any) => {
+ const res = run({ this: doc });
+ if (!res.success) return false;
+ doc[props.fieldKey] = res.result;
+ return true;
+ };
return (
- <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}>
- <EditableView contents={contents}
- height={36} GetValue={() => {
- let field = props.doc.Get(props.fieldKey);
- if (field && field instanceof Field) {
- return field.ToScriptString();
+ <div className="collectionSchemaView-cellContents" onPointerDown={onItemDown} key={props.Document[Id]} ref={reference}>
+ <EditableView
+ display={"inline"}
+ contents={fieldContentView}
+ height={Number(MAX_ROW_HEIGHT)}
+ GetValue={() => {
+ let field = props.Document[props.fieldKey];
+ if (field) {
+ //TODO Types
+ // return field.ToScriptString();
+ return String(field);
}
- return field || "";
+ return "";
}}
SetValue={(value: string) => {
- let script = CompileScript(value, undefined, true);
+ let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
if (!script.compiled) {
return false;
}
- let field = script();
- if (field instanceof Field) {
- props.doc.Set(props.fieldKey, field);
- return true;
- } else {
- let dataField = ToField(field);
- if (dataField) {
- props.doc.Set(props.fieldKey, dataField);
- return true;
- }
+ return applyToDoc(props.Document, script.run);
+ }}
+ OnFillDown={async (value: string) => {
+ let script = CompileScript(value, { addReturn: true, params: { this: Document.name } });
+ if (!script.compiled) {
+ return;
}
- return false;
+ const run = script.run;
+ //TODO This should be able to be refactored to compile the script once
+ const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ val && val.forEach(doc => applyToDoc(doc, run));
}}>
</EditableView>
- </div>
- )
+ </div >
+ );
}
private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
@@ -88,159 +135,240 @@ export class CollectionSchemaView extends CollectionViewBase {
}
return {
onClick: action((e: React.MouseEvent, handleOriginal: Function) => {
+ that.props.select(e.ctrlKey);
that._selectedIndex = rowInfo.index;
- this._splitPercentage += 0.05; // bcz - ugh - needed to force Measure to do its thing and call onResize
if (handleOriginal) {
- handleOriginal()
+ handleOriginal();
}
}),
style: {
- background: rowInfo.index == this._selectedIndex ? "lightGray" : "white",
- //color: rowInfo.index == this._selectedIndex ? "white" : "black"
+ background: rowInfo.index === this._selectedIndex ? "lightGray" : "white",
+ //color: rowInfo.index === this._selectedIndex ? "white" : "black"
}
};
}
- _startSplitPercent = 0;
+ private createTarget = (ele: HTMLDivElement) => {
+ this._mainCont = ele;
+ super.CreateDropTarget(ele);
+ }
+
+ @action
+ toggleKey = (key: string) => {
+ let list = Cast(this.props.Document.schemaColumns, listSpec("string"));
+ if (list === undefined) {
+ this.props.Document.schemaColumns = list = new List<string>([key]);
+ } else {
+ const index = list.indexOf(key);
+ if (index === -1) {
+ list.push(key);
+ } else {
+ list.splice(index, 1);
+ }
+ }
+ }
+
+ //toggles preview side-panel of schema
+ @action
+ toggleExpander = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
+ }
+
@action
onDividerMove = (e: PointerEvent): void => {
- let nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this._splitPercentage = Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100);
+ let nativeWidth = this._mainCont!.getBoundingClientRect();
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
}
@action
onDividerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 1 ? 66 : 100;
+ if (this._startSplitPercent === this.splitPercentage) {
+ this.props.Document.schemaSplitPercentage = this.splitPercentage === 0 ? 33 : 0;
}
}
onDividerDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
+ this._startSplitPercent = this.splitPercentage;
e.stopPropagation();
e.preventDefault();
document.addEventListener("pointermove", this.onDividerMove);
document.addEventListener('pointerup', this.onDividerUp);
}
- @action
- onExpanderMove = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (this.props.isSelected()) e.stopPropagation();
+ else e.preventDefault();
+ }
}
- @action
- onExpanderUp = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- document.removeEventListener("pointermove", this.onExpanderMove);
- document.removeEventListener('pointerup', this.onExpanderUp);
- if (this._startSplitPercent == this._splitPercentage) {
- this._splitPercentage = this._splitPercentage == 100 ? 66 : 100;
+
+ onWheel = (e: React.WheelEvent): void => {
+ if (this.props.active()) {
+ e.stopPropagation();
}
}
- onExpanderDown = (e: React.PointerEvent) => {
- this._startSplitPercent = this._splitPercentage;
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onExpanderMove);
- document.addEventListener('pointerup', this.onExpanderUp);
- }
-
- onPointerDown = (e: React.PointerEvent) => {
- // if (e.button === 2 && this.active) {
- // e.stopPropagation();
- // e.preventDefault();
- // } else
- {
- if (e.buttons === 1) {
- if (this.props.isSelected()) {
- e.stopPropagation();
- }
+
+ onContextMenu = (e: React.MouseEvent): void => {
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB });
+ }
+ }
+
+ @action
+ makeDB = async () => {
+ let csv: string = this.columns.reduce((val, col) => val + col + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ let self = this;
+ DocListCast(this.props.Document.data).map(doc => {
+ csv += self.columns.reduce((val, col) => val + (doc[col] ? doc[col]!.toString() : "") + ",", "");
+ csv = csv.substr(0, csv.length - 1) + "\n";
+ });
+ csv.substring(0, csv.length - 1);
+ let dbName = StrCast(this.props.Document.title);
+ let res = await Gateway.Instance.PostSchema(csv, dbName);
+ if (self.props.CollectionView.props.addDocument) {
+ let schemaDoc = await Docs.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName });
+ if (schemaDoc) {
+ self.props.CollectionView.props.addDocument(schemaDoc, false);
}
}
}
@action
- setScaling = (r: any) => {
- const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
- const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
- this._panelWidth = r.entry.width;
- this._panelHeight = r.entry.height ? r.entry.height : this._panelHeight;
- this._contentScaling = r.entry.width / selected!.GetNumber(KeyStore.NativeWidth, r.entry.width);
+ addColumn = () => {
+ this.columns.push(this._newKeyName);
+ this._newKeyName = "";
}
- getContentScaling = (): number => this._contentScaling;
- getPanelWidth = (): number => this._panelWidth;
- getPanelHeight = (): number => this._panelHeight;
- getTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._contentScaling);
+ @action
+ newKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._newKeyName = e.currentTarget.value;
}
- focusDocument = (doc: Document) => { }
+ @observable previewScript: string = "";
+ @action
+ onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.previewScript = e.currentTarget.value;
+ }
- render() {
- const columns = this.props.Document.GetList(KeyStore.ColumnsKey, [KeyStore.Title, KeyStore.Data, KeyStore.Author])
- const children = this.props.Document.GetList<Document>(this.props.fieldKey, []);
- const selected = children.length > this._selectedIndex ? children[this._selectedIndex] : undefined;
- let content = this._selectedIndex == -1 || !selected ? (null) : (
- <Measure onResize={this.setScaling}>
- {({ measureRef }) =>
- <div className="collectionSchemaView-content" ref={measureRef}>
- <DocumentView Document={selected}
- AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
- isTopMost={false}
- SelectOnLoad={false}
- ScreenToLocalTransform={this.getTransform}
- ContentScaling={this.getContentScaling}
- PanelWidth={this.getPanelWidth}
- PanelHeight={this.getPanelHeight}
- ContainingCollectionView={this.props.CollectionView}
- focus={this.focusDocument}
- />
+ @computed
+ get previewDocument(): Doc | undefined {
+ const children = DocListCast(this.props.Document[this.props.fieldKey]);
+ const selected = children.length > this._selectedIndex ? FieldValue(children[this._selectedIndex]) : undefined;
+ return selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
+ }
+ get tableWidth() { return (this.props.PanelWidth() - 2 * this.borderWidth - this.DIVIDER_WIDTH) * (1 - this.splitPercentage / 100); }
+ get previewRegionHeight() { return this.props.PanelHeight() - 2 * this.borderWidth; }
+ 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;
+ }
+ private previewPanelWidth = () => this.previewDocNativeWidth() * this.previewContentScaling();
+ private previewPanelHeight = () => this.previewDocNativeHeight() * this.previewContentScaling();
+ get previewPanelCenteringOffset() { return (this.previewRegionWidth - this.previewDocNativeWidth() * this.previewContentScaling()) / 2; }
+ getPreviewTransform = (): Transform => this.props.ScreenToLocalTransform().translate(
+ - this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth - this.previewPanelCenteringOffset,
+ - this.borderWidth).scale(1 / this.previewContentScaling())
+
+ @computed
+ get previewPanel() {
+ // let doc = CompileScript(this.previewScript, { this: selected }, true)();
+ const previewDoc = this.previewDocument;
+ return (<div className="collectionSchemaView-previewRegion" style={{ width: `${Math.max(0, this.previewRegionWidth - 1)}px` }}>
+ {!previewDoc || !this.previewRegionWidth ? (null) : (
+ <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
+ <DocumentView Document={previewDoc} isTopMost={false} selectOnLoad={false}
+ toggleMinimized={emptyFunction}
+ 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}
+ bringToFront={emptyFunction}
+ />
+ </div>)}
+ <input className="collectionSchemaView-input" value={this.previewScript} onChange={this.onPreviewScriptChange}
+ style={{ left: `calc(50% - ${Math.min(75, (previewDoc ? this.previewPanelWidth() / 2 : 75))}px)` }} />
+ </div>);
+ }
+
+ get documentKeysCheckList() {
+ 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
+ // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
+ // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
+ // is displayed (unlikely) it won't show up until something else changes.
+ //TODO Types
+ untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => keys[key] = false))));
+
+ this.columns.forEach(key => keys[key] = true);
+ return Array.from(Object.keys(keys)).map(item =>
+ (<KeyToggle checked={keys[item]} key={item} keyName={item} toggle={this.toggleKey} />));
+ }
+
+ get tableOptionsPanel() {
+ return !this.props.active() ? (null) :
+ (<Flyout
+ anchorPoint={anchorPoints.RIGHT_TOP}
+ content={<div>
+ <div id="schema-options-header"><h5><b>Options</b></h5></div>
+ <div id="options-flyout-div">
+ <h6 className="schema-options-subHeader">Preview Window</h6>
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.splitPercentage !== 0} onChange={this.toggleExpander} /> Show Preview </div>
+ <h6 className="schema-options-subHeader" >Displayed Columns</h6>
+ <ul id="schema-col-checklist" >
+ {this.documentKeysCheckList}
+ </ul>
+ <input value={this._newKeyName} onChange={this.newKeyChange} />
+ <button onClick={this.addColumn}><FontAwesomeIcon style={{ color: "white" }} icon="plus" size="lg" /></button>
</div>
- }
- </Measure>
- )
- let previewHandle = !this.props.active() ? (null) : (
- <div className="collectionSchemaView-previewHandle" onPointerDown={this.onExpanderDown} />);
- let dividerDragger = this._splitPercentage == 100 ? (null) :
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />
+ </div>
+ }>
+ <button id="schemaOptionsMenuBtn" ><FontAwesomeIcon style={{ color: "white" }} icon="cog" size="sm" /></button>
+ </Flyout>);
+ }
+
+ @computed
+ get dividerDragger() {
+ return this.splitPercentage === 0 ? (null) :
+ <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown} style={{ width: `${this.DIVIDER_WIDTH}px` }} />;
+ }
+
+ render() {
+ library.add(faCog);
+ library.add(faPlus);
+ const children = this.children;
return (
- <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} ref={this._mainCont} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} >
- <div className="collectionSchemaView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
- <Measure onResize={action((r: any) => {
- this._dividerX = r.entry.width;
- this._panelHeight = r.entry.height;
- })}>
- {({ measureRef }) =>
- <div ref={measureRef} className="collectionSchemaView-tableContainer" style={{ width: `${this._splitPercentage}%` }}>
- <ReactTable
- data={children}
- pageSize={children.length}
- page={0}
- showPagination={false}
- columns={columns.map(col => ({
- Header: col.Name,
- accessor: (doc: Document) => [doc, col],
- id: col.Id
- }))}
- column={{
- ...ReactTableDefaults.column,
- Cell: this.renderCell,
-
- }}
- getTrProps={this.getTrProps}
- />
- </div>
- }
- </Measure>
- {dividerDragger}
- <div className="collectionSchemaView-previewRegion" style={{ width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)` }}>
- {content}
- </div>
- {previewHandle}
+ <div className="collectionSchemaView-container" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} onContextMenu={this.onContextMenu} ref={this.createTarget}>
+ <div className="collectionSchemaView-tableContainer" style={{ width: `${this.tableWidth}px` }}>
+ <ReactTable data={children} page={0} pageSize={children.length} showPagination={false}
+ columns={this.columns.map(col => ({
+ Header: col,
+ accessor: (doc: Doc) => [doc, col],
+ id: col
+ }))}
+ column={{ ...ReactTableDefaults.column, Cell: this.renderCell, }}
+ getTrProps={this.getTrProps}
+ />
</div>
- </div >
- )
+ {this.dividerDragger}
+ {this.previewPanel}
+ {this.tableOptionsPanel}
+ </div>
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
new file mode 100644
index 000000000..ffd3e0659
--- /dev/null
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -0,0 +1,231 @@
+import { action, runInAction } from "mobx";
+import React = require("react");
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { RouteStore } from "../../../server/RouteStore";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { FieldViewProps } from "../nodes/FieldView";
+import * as rp from 'request-promise';
+import { CollectionView } from "./CollectionView";
+import { CollectionPDFView } from "./CollectionPDFView";
+import { CollectionVideoView } from "./CollectionVideoView";
+import { Doc, Opt, FieldResult, DocListCast } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { listSpec } from "../../../new_fields/Schema";
+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, { CursorPosition, CursorMetadata } from "../../../new_fields/CursorField";
+import { url } from "inspector";
+
+export interface CollectionViewProps extends FieldViewProps {
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+}
+
+export interface SubCollectionViewProps extends CollectionViewProps {
+ CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+}
+
+export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
+ class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
+ private dropDisposer?: DragManager.DragDropDisposer;
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
+ }
+ protected CreateDropTarget(ele: HTMLDivElement) {
+ this.createDropTarget(ele);
+ }
+
+ get children() {
+ //TODO tfs: This might not be what we want?
+ //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
+ return DocListCast(this.props.Document[this.props.fieldKey]);
+ }
+
+ @action
+ protected async setCursorPosition(position: [number, number]) {
+ 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) {
+ const proto = await doc.proto;
+ if (!proto) {
+ return;
+ }
+ let cursors = Cast(proto.cursors, listSpec(CursorField));
+ if (!cursors) {
+ proto.cursors = cursors = new List<CursorField>();
+ }
+ if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) {
+ cursors[ind].setPosition(pos);
+ } else {
+ let entry = new CursorField({ metadata: { id: id, identifier: email }, position: pos });
+ cursors.push(entry);
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.dropAction || de.data.userDropAction) {
+ ["width", "height", "curPage"].map(key =>
+ de.data.draggedDocuments.map((draggedDocument: Doc, i: number) =>
+ PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f))));
+ }
+ let added = false;
+ if (de.data.dropAction || de.data.userDropAction) {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ } else if (de.data.moveDocument) {
+ const move = de.data.moveDocument;
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = move(d, this.props.Document, this.props.addDocument);
+ return moved || added;
+ }, false);
+ } else {
+ added = de.data.droppedDocuments.reduce((added: boolean, d) => {
+ let moved = this.props.addDocument(d);
+ return moved || added;
+ }, false);
+ }
+ e.stopPropagation();
+ return added;
+ }
+ return false;
+ }
+
+ protected async getDocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise<Doc | undefined>)) | undefined = undefined;
+ if (type.indexOf("image") !== -1) {
+ ctor = Docs.ImageDocument;
+ }
+ if (type.indexOf("video") !== -1) {
+ ctor = Docs.VideoDocument;
+ }
+ if (type.indexOf("audio") !== -1) {
+ ctor = Docs.AudioDocument;
+ }
+ if (type.indexOf("pdf") !== -1) {
+ ctor = Docs.PdfDocument;
+ options.nativeWidth = 1200;
+ }
+ if (type.indexOf("excel") !== -1) {
+ ctor = Docs.DBDocument;
+ options.dropAction = "copy";
+ }
+ if (type.indexOf("html") !== -1) {
+ if (path.includes('localhost')) {
+ let s = path.split('/');
+ let id = s[s.length - 1];
+ DocServer.GetRefField(id).then(field => {
+ if (field instanceof Doc) {
+ let alias = Doc.MakeAlias(field);
+ alias.x = options.x || 0;
+ alias.y = options.y || 0;
+ alias.width = options.width || 300;
+ alias.height = options.height || options.width || 300;
+ this.props.addDocument(alias, false);
+ }
+ });
+ return undefined;
+ }
+ ctor = Docs.WebDocument;
+ options = { height: options.width, ...options, title: path, nativeWidth: undefined };
+ }
+ return ctor ? ctor(path, options) : undefined;
+ }
+
+ @undoBatch
+ @action
+ protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+ let html = e.dataTransfer.getData("text/html");
+ let text = e.dataTransfer.getData("text/plain");
+
+ if (text && text.startsWith("<div")) {
+ return;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+
+ if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) {
+ let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
+ 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>[] = [];
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ const upload = window.location.origin + RouteStore.upload;
+ let item = e.dataTransfer.items[i];
+ if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
+ let str: string;
+ let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
+ .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s)))))
+ .then(result => {
+ let type = result["content-type"];
+ if (type) {
+ this.getDocumentFromType(type, str, { ...options, width: 300, nativeWidth: 300 })
+ .then(doc => doc && this.props.addDocument(doc, false));
+ }
+ });
+ promises.push(prom);
+ }
+ let type = item.type;
+ if (item.kind === "file") {
+ let file = item.getAsFile();
+ let formData = new FormData();
+
+ if (file) {
+ formData.append('file', file);
+ }
+ let dropFileName = file ? file.name : "-empty-";
+
+ let prom = fetch(upload, {
+ method: 'POST',
+ body: formData
+ }).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 });
+
+ docPromise.then(doc => doc && this.props.addDocument(doc));
+ }));
+ });
+ promises.push(prom);
+ }
+ }
+
+ if (promises.length) {
+ Promise.all(promises).finally(() => batch.end());
+ } else {
+ batch.end();
+ }
+ }
+ }
+ return CollectionSubView;
+}
+
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index f8d580a7b..411d67ff7 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -1,37 +1,88 @@
-#body {
+@import "../globalCssVariables";
+
+.collectionTreeView-dropTarget {
+ border-width: $COLLECTION_BORDER_WIDTH;
+ border-color: transparent;
+ border-style: solid;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ height: 100%;
padding: 20px;
- background: #bbbbbb;
-}
+ padding-left: 10px;
+ padding-right: 0px;
+ background: $light-color-secondary;
+ font-size: 13px;
+ overflow: scroll;
-ul {
- list-style: none;
-}
+ ul {
+ list-style: none;
+ padding-left: 20px;
+ }
-li {
- margin: 5px 0;
-}
+ li {
+ margin: 5px 0;
+ }
-.no-indent {
- padding-left: 0;
-}
-.bullet {
- width: 1.5em;
- display: inline-block;
-}
+ .no-indent {
+ padding-left: 0;
+ }
-.collectionTreeView-dropTarget {
- border-style: solid;
- box-sizing: border-box;
- height: 100%;
-}
+ .bullet {
+ float:left;
+ position: relative;
+ width: 15px;
+ display: block;
+ color: $intermediate-color;
+ margin-top: 3px;
+ transform: scale(1.3,1.3);
+ }
+
+ .docContainer {
+ margin-left: 10px;
+ display: block;
+ // width:100%;//width: max-content;
+ }
+ .docContainer:hover {
+ .treeViewItem-openRight {
+ display:inline;
+ }
+ }
+
+
+ .editableView-container {
+ font-weight: bold;
+ }
-.docContainer {
- display: inline-table;
-}
+ .delete-button {
+ color: $intermediate-color;
+ // float: right;
+ margin-left: 15px;
+ // margin-top: 3px;
+ display: inline;
+ }
+ .treeViewItem-openRight {
+ margin-left: 5px;
+ display:none;
+ }
+ .docContainer:hover {
+ .delete-button {
+ display: inline;
+ // width: auto;
+ }
+ }
-.delete-button {
- color: #999999;
- float: right;
- margin-left: 1em;
+ .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 8b06d9ac4..6acef434e 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,20 +1,32 @@
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+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";
-import { CollectionViewBase } from "./CollectionViewBase";
-import { Document } from "../../../fields/Document";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import React = require("react")
-import { TextField } from "../../../fields/TextField";
-import { observable, action } from "mobx";
-import "./CollectionTreeView.scss";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
import { EditableView } from "../EditableView";
-import { setupDrag } from "../../util/DragManager";
-import { FieldWaiting } from "../../../fields/Field";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+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, DocListCast } from '../../../new_fields/Doc';
+import { Id } from '../../../new_fields/RefField';
+import { ContextMenu } from '../ContextMenu';
+import { undoBatch } from '../../util/UndoManager';
+import { Main } from '../Main';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { CollectionDockingView } from './CollectionDockingView';
+import { DocumentManager } from '../../util/DocumentManager';
+import { List } from '../../../new_fields/List';
+import { Docs } from '../../documents/Documents';
+
export interface TreeViewProps {
- document: Document;
- deleteDoc: (doc: Document) => void;
+ document: Doc;
+ deleteDoc: (doc: Doc) => void;
+ moveDocument: DragManager.MoveFunction;
+ dropAction: "alias" | "copy" | undefined;
}
export enum BulletType {
@@ -23,151 +35,219 @@ export enum BulletType {
List
}
+library.add(faTrashAlt);
+library.add(faAngleRight);
+library.add(faCaretDown);
+library.add(faCaretRight);
+
@observer
/**
* Component that takes in a document prop and a boolean whether it's collapsed or not.
*/
class TreeView extends React.Component<TreeViewProps> {
- @observable
- collapsed: boolean = false;
+ @observable _collapsed: boolean = true;
+
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
- delete = () => {
- this.props.deleteDoc(this.props.document);
+ @undoBatch openRight = async () => {
+ if (this.props.document.dockingConfig) {
+ Main.Instance.openWorkspace(this.props.document);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(this.props.document);
+ }
}
+ get children() {
+ return Cast(this.props.document.data, listSpec(Doc), []); // bcz: needed? .filter(doc => FieldValue(doc));
+ }
+
+ onPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ }
@action
- remove = (document: Document) => {
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ remove = (document: Document, key: string) => {
+ let children = Cast(this.props.document[key], listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
}
}
- renderBullet(type: BulletType) {
- let onClicked = action(() => this.collapsed = !this.collapsed);
+ @action
+ move: DragManager.MoveFunction = (document, target, addDoc) => {
+ if (this.props.document === target) {
+ return true;
+ }
+ //TODO This should check if it was removed
+ this.remove(document, "data");
+ return addDoc(document);
+ }
+ renderBullet(type: BulletType) {
+ let onClicked = action(() => this._collapsed = !this._collapsed);
+ let bullet: IconProp | undefined = undefined;
switch (type) {
- case BulletType.Collapsed:
- return <div className="bullet" onClick={onClicked}>&#9654;</div>
- case BulletType.Collapsible:
- return <div className="bullet" onClick={onClicked}>&#9660;</div>
- case BulletType.List:
- return <div className="bullet">&mdash;</div>
+ case BulletType.Collapsed: bullet = "caret-right"; break;
+ case BulletType.Collapsible: bullet = "caret-down"; break;
}
+ 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.
*/
renderTitle() {
- let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField);
-
- // if the title hasn't loaded, immediately return the div
- if (!title || title === "<Waiting>") {
- return <div key={this.props.document.Id}></div>;
- }
-
- return <div className="docContainer"> <EditableView contents={title.Data}
- height={36} GetValue={() => {
- let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField);
- if (title && title !== "<Waiting>")
- return title.Data;
- return "";
- }} SetValue={(value: string) => {
- this.props.document.SetData(KeyStore.Title, value, TextField);
- return true;
- }} />
- <div className="delete-button" onClick={this.delete}>x</div>
- </div >
+ let reference = React.createRef<HTMLDivElement>();
+ let onItemDown = SetupDrag(reference, () => this.props.document, this.props.moveDocument, this.props.dropAction);
+ let editableView = (titleString: string) =>
+ (<EditableView
+ oneLine={!this._isOver ? true : false}
+ display={"block"}
+ contents={titleString}
+ height={36}
+ GetValue={() => StrCast(this.props.document.title)}
+ SetValue={(value: string) => {
+ let target = this.props.document.proto ? this.props.document.proto : this.props.document;
+ target.title = value;
+ return true;
+ }}
+ />);
+ let dataDocs = 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} 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))}
+ {openRight}
+ {/* {<div className="delete-button" onClick={this.delete}><FontAwesomeIcon icon="trash-alt" size="xs" /></div>} */}
+ </div >);
}
- render() {
- var children = this.props.document.GetT<ListField<Document>>(KeyStore.Data, ListField);
-
- let reference = React.createRef<HTMLDivElement>();
- let onItemDown = setupDrag(reference, () => this.props.document);
- let titleElement = this.renderTitle();
-
- // check if this document is a collection
- if (children && children !== FieldWaiting) {
- let subView;
-
- // if uncollapsed, then add the children elements
- if (!this.collapsed) {
- // render all children elements
- let childrenElement = (children.Data.map(value =>
- <TreeView document={value} deleteDoc={this.remove} />)
- )
- subView =
- <li key={this.props.document.Id} >
- {this.renderBullet(BulletType.Collapsible)}
- {titleElement}
- <ul key={this.props.document.Id}>
- {childrenElement}
- </ul>
- </li>
- } else {
- subView = <li key={this.props.document.Id}>
- {this.renderBullet(BulletType.Collapsed)}
- {titleElement}
- </li>
+ 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) });
+ ContextMenu.Instance.addItem({
+ description: "Open Fields", event: () => CollectionDockingView.Instance.AddRightSplit(Docs.KVPDocument(this.props.document,
+ { title: this.props.document.title + ".kvp", width: 300, height: 300 }))
+ });
+ 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)) });
}
-
- return <div className="treeViewItem-container" onPointerDown={onItemDown} ref={reference}>
- {subView}
- </div>
+ 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; };
- // otherwise this is a normal leaf node
- else {
- return <li key={this.props.document.Id}>
- {this.renderBullet(BulletType.List)}
- {titleElement}
- </li>;
+ render() {
+ let bulletType = BulletType.List;
+ 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)}
+ </div>
+ </ul >);
+ } else {
+ bulletType = BulletType.Collapsed;
+ }
+ }
+ });
+ return <div className="treeViewItem-container"
+ onContextMenu={this.onWorkspaceContextMenu}>
+ <li className="collection-child">
+ {this.renderBullet(bulletType)}
+ {this.renderTitle()}
+ {contentElement}
+ </li>
+ </div>;
+ }
+ public static GetChildElements(docs: Doc[], allowMinimized: boolean, remove: ((doc: Doc) => void), move: DragManager.MoveFunction, dropAction: dropActionType) {
+ return docs.filter(child => !child.excludeFromLibrary && (allowMinimized || !child.isMinimized)).map(child =>
+ <TreeView document={child} key={child[Id]} deleteDoc={remove} moveDocument={move} dropAction={dropAction} />);
}
}
-
@observer
-export class CollectionTreeView extends CollectionViewBase {
-
+export class CollectionTreeView extends CollectionSubView(Document) {
@action
remove = (document: Document) => {
- var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- if (children && children !== FieldWaiting) {
- children.Data.splice(children.Data.indexOf(document), 1);
+ let children = Cast(this.props.Document.data, listSpec(Doc), []);
+ if (children) {
+ children.splice(children.indexOf(document), 1);
+ }
+ }
+ 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)) });
}
}
-
render() {
- let titleStr = "";
- let title = this.props.Document.GetT<TextField>(KeyStore.Title, TextField);
- if (title && title !== FieldWaiting) {
- titleStr = title.Data;
+ let dropAction = StrCast(this.props.Document.dropAction, "alias") as dropActionType;
+ if (!this.children) {
+ return (null);
}
-
- var children = this.props.Document.GetT<ListField<Document>>(KeyStore.Data, ListField);
- let childrenElement = !children || children === FieldWaiting ? (null) :
- (children.Data.map(value =>
- <TreeView document={value} key={value.Id} deleteDoc={this.remove} />)
- )
+ let childElements = TreeView.GetChildElements(this.children, false, this.remove, this.props.moveDocument, dropAction);
return (
- <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}>
- <h3>
- <EditableView contents={titleStr}
- height={72} GetValue={() => {
- return this.props.Document.Title;
- }} SetValue={(value: string) => {
- this.props.Document.SetData(KeyStore.Title, value, TextField);
+ <div id="body" className="collectionTreeView-dropTarget"
+ style={{ borderRadius: "inherit" }}
+ onContextMenu={this.onContextMenu}
+ onWheel={(e: React.WheelEvent) => e.stopPropagation()}
+ onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}>
+ <div className="coll-title">
+ <EditableView
+ contents={this.props.Document.title}
+ display={"inline"}
+ height={72}
+ GetValue={() => StrCast(this.props.Document.title)}
+ SetValue={(value: string) => {
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = value;
return true;
}} />
- </h3>
+ </div>
<ul className="no-indent">
- {childrenElement}
+ {childElements}
</ul>
</div >
);
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
new file mode 100644
index 000000000..db8b84832
--- /dev/null
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -0,0 +1,42 @@
+
+.collectionVideoView-cont{
+ width: 100%;
+ height: 100%;
+ position: inherit;
+ top: 0;
+ left:0;
+
+}
+.collectionVideoView-time{
+ color : white;
+ top :25px;
+ left : 25px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+}
+.collectionVideoView-play {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ left : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: left bottom;
+}
+.collectionVideoView-full {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ right : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: right bottom;
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
new file mode 100644
index 000000000..9ab959f3c
--- /dev/null
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -0,0 +1,89 @@
+import { action, observable, trace } from "mobx";
+import { observer } from "mobx-react";
+import { ContextMenu } from "../ContextMenu";
+import { CollectionViewType, CollectionBaseView, CollectionRenderProps } from "./CollectionBaseView";
+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 { VideoBox } from "../nodes/VideoBox";
+import { NumCast } from "../../../new_fields/Types";
+
+
+@observer
+export class CollectionVideoView extends React.Component<FieldViewProps> {
+ 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(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._videoBox && this._videoBox.Playing ? "\"" : ">"}
+ </div>,
+ <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
+ F
+ </div>
+ ]);
+ }
+
+ @action
+ onPlayDown = () => {
+ if (this._videoBox && this._videoBox.player) {
+ if (this._videoBox.Playing) {
+ this._videoBox.Pause();
+ } else {
+ this._videoBox.Play();
+ }
+ }
+ }
+
+ @action
+ onFullDown = (e: React.PointerEvent) => {
+ if (this._videoBox) {
+ this._videoBox.FullScreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onResetDown = () => {
+ 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
+ }
+ }
+
+ setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
+
+ private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ let props = { ...this.props, ...renderProps };
+ return (<>
+ <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} />
+ {this.props.isSelected() ? this.uIButtons : (null)}
+ </>);
+ }
+
+ render() {
+ trace();
+ return (
+ <CollectionBaseView {...this.props} className="collectionVideoView-cont" onContextMenu={this.onContextMenu}>
+ {this.subView}
+ </CollectionBaseView>);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index d0e2162b1..3d9b4990a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,132 +1,56 @@
-import { action, computed, observable } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faProjectDiagram, faSquare, faTh, faTree } from '@fortawesome/free-solid-svg-icons';
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
-import { SelectionManager } from "../../util/SelectionManager";
+import * as React from 'react';
+import { Id } from '../../../new_fields/RefField';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../ContextMenu";
-import React = require("react");
-import { KeyStore } from "../../../fields/KeyStore";
-import { NumberField } from "../../../fields/NumberField";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
import { CollectionDockingView } from "./CollectionDockingView";
import { CollectionSchemaView } from "./CollectionSchemaView";
-import { CollectionViewProps } from "./CollectionViewBase";
import { CollectionTreeView } from "./CollectionTreeView";
-import { Field, FieldId } from "../../../fields/Field";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faTh } from '@fortawesome/free-solid-svg-icons';
-import { faTree } from '@fortawesome/free-solid-svg-icons';
-import { faSquare } from '@fortawesome/free-solid-svg-icons';
-import { faProjectDiagram } from '@fortawesome/free-solid-svg-icons';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+export const COLLECTION_BORDER_WIDTH = 2;
library.add(faTh);
library.add(faTree);
library.add(faSquare);
library.add(faProjectDiagram);
-export enum CollectionViewType {
- Invalid,
- Freeform,
- Schema,
- Docking,
- Tree
-}
-
-export const COLLECTION_BORDER_WIDTH = 2;
-
@observer
-export class CollectionView extends React.Component<CollectionViewProps> {
-
- @observable
- public SelectedDocs: FieldId[] = [];
-
- public static LayoutString(fieldKey: string = "DataKey") {
- return `<${CollectionView.name} Document={Document}
- ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
- isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
- }
-
- public active: () => boolean = () => CollectionView.Active(this);
- addDocument = (doc: Document): void => { CollectionView.AddDocument(this.props, doc); }
- removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
- get subView() { return CollectionView.SubView(this); }
-
- public static Active(self: CollectionView): boolean {
- var isSelected = self.props.isSelected();
- var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == self);
- var topMost = self.props.isTopMost;
- return isSelected || childSelected || topMost;
- }
-
- @action
- public static AddDocument(props: CollectionViewProps, doc: Document) {
- doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, 0));
- if (props.Document.Get(props.fieldKey) instanceof Field) {
- //TODO This won't create the field if it doesn't already exist
- const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
- value.push(doc);
- } else {
- props.Document.SetData(props.fieldKey, [doc], ListField);
- }
- }
-
- @action
- public static RemoveDocument(props: CollectionViewProps, doc: Document): boolean {
- //TODO This won't create the field if it doesn't already exist
- const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
- let index = -1;
- for (let i = 0; i < value.length; i++) {
- if (value[i].Id == doc.Id) {
- index = i;
- break;
- }
- }
-
- if (index !== -1) {
- value.splice(index, 1)
-
- SelectionManager.DeselectAll()
- ContextMenu.Instance.clearItems()
- return true;
- }
- return false
- }
-
- get collectionViewType(): CollectionViewType {
- let Document = this.props.Document;
- let viewField = Document.GetT(KeyStore.ViewType, NumberField);
- if (viewField === "<Waiting>") {
- return CollectionViewType.Invalid;
- } else if (viewField) {
- return viewField.Data;
- } else {
- return CollectionViewType.Freeform;
+export class CollectionView extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
+
+ private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
+ let props = { ...this.props, ...renderProps };
+ switch (type) {
+ case CollectionViewType.Schema: return (<CollectionSchemaView {...props} CollectionView={this} />);
+ case CollectionViewType.Docking: return (<CollectionDockingView {...props} CollectionView={this} />);
+ case CollectionViewType.Tree: return (<CollectionTreeView {...props} CollectionView={this} />);
+ case CollectionViewType.Freeform:
+ default:
+ return (<CollectionFreeFormView {...props} CollectionView={this} />);
}
+ return (null);
}
- specificContextMenu = (e: React.MouseEvent): void => {
- if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- ContextMenu.Instance.addItem({ description: "Freeform", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform), icon: "project-diagram" })
- ContextMenu.Instance.addItem({ description: "Schema", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema), icon: "th" })
- ContextMenu.Instance.addItem({ description: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree), icon: "tree" })
- ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking), icon: "square" })
- }
- }
+ get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } // bcz: ? Why do we need to compare Id's?
- public static SubView(self: CollectionView) {
- let subProps = { ...self.props, addDocument: self.addDocument, removeDocument: self.removeDocument, active: self.active, CollectionView: self }
- switch (self.collectionViewType) {
- case CollectionViewType.Freeform: return (<CollectionFreeFormView {...subProps} />)
- case CollectionViewType.Schema: return (<CollectionSchemaView {...subProps} />)
- case CollectionViewType.Docking: return (<CollectionDockingView {...subProps} />)
- case CollectionViewType.Tree: return (<CollectionTreeView {...subProps} />)
+ 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), icon: "project-diagram" });
+ ContextMenu.Instance.addItem({ description: "Schema", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Schema), icon: "project-diagram" });
+ ContextMenu.Instance.addItem({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
}
- return (null);
}
render() {
- return (<div className="collectionView-cont" onContextMenu={this.specificContextMenu}>
- {this.subView}
- </div>)
+ return (
+ <CollectionBaseView {...this.props} onContextMenu={this.onContextMenu}>
+ {this.SubView}
+ </CollectionBaseView>
+ );
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
deleted file mode 100644
index b126b40a9..000000000
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import { action, runInAction } from "mobx";
-import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
-import React = require("react");
-import { KeyStore } from "../../../fields/KeyStore";
-import { FieldWaiting } from "../../../fields/Field";
-import { undoBatch } from "../../util/UndoManager";
-import { DragManager } from "../../util/DragManager";
-import { DocumentView } from "../nodes/DocumentView";
-import { Documents, DocumentOptions } from "../../documents/Documents";
-import { Key } from "../../../fields/Key";
-import { Transform } from "../../util/Transform";
-import { CollectionView } from "./CollectionView";
-
-export interface CollectionViewProps {
- fieldKey: Key;
- Document: Document;
- ScreenToLocalTransform: () => Transform;
- isSelected: () => boolean;
- isTopMost: boolean;
- select: (ctrlPressed: boolean) => void;
- bindings: any;
- panelWidth: () => number;
- panelHeight: () => number;
- focus: (doc: Document) => void;
-}
-export interface SubCollectionViewProps extends CollectionViewProps {
- active: () => boolean;
- addDocument: (doc: Document) => void;
- removeDocument: (doc: Document) => boolean;
- CollectionView: CollectionView;
-}
-
-export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
- private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
- }
-
- @undoBatch
- @action
- protected drop(e: Event, de: DragManager.DropEvent) {
- const docView: DocumentView = de.data["documentView"];
- const doc: Document = de.data["document"];
- if (docView && (!docView.props.ContainingCollectionView || docView.props.ContainingCollectionView !== this.props.CollectionView)) {
- if (docView.props.RemoveDocument) {
- docView.props.RemoveDocument(docView.props.Document);
- }
- this.props.addDocument(docView.props.Document);
- } else if (doc) {
- this.props.removeDocument(doc);
- this.props.addDocument(doc);
- }
- e.stopPropagation();
- }
-
- @action
- protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
- e.stopPropagation()
- e.preventDefault()
- let that = this;
-
- let html = e.dataTransfer.getData("text/html");
- let text = e.dataTransfer.getData("text/plain");
- if (html && html.indexOf("<img") != 0) {
- console.log("not good");
- let htmlDoc = Documents.HtmlDocument(html, { ...options, width: 300, height: 300 });
- htmlDoc.SetText(KeyStore.DocumentText, text);
- this.props.addDocument(htmlDoc);
- return;
- }
-
- console.log(e.dataTransfer.items.length);
-
- for (let i = 0; i < e.dataTransfer.items.length; i++) {
- const upload = window.location.origin + "/upload";
- let item = e.dataTransfer.items[i];
- if (item.kind === "string" && item.type.indexOf("uri") != -1) {
- e.dataTransfer.items[i].getAsString(function (s) {
- action(() => {
- var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, width: 300, })
-
- let docs = that.props.Document.GetT(KeyStore.Data, ListField);
- if (docs != FieldWaiting) {
- if (!docs) {
- docs = new ListField<Document>();
- that.props.Document.Set(KeyStore.Data, docs)
- }
- docs.Data.push(img);
- }
- })()
-
- })
- }
- let type = item.type
- console.log(type)
- if (item.kind == "file") {
- let fReader = new FileReader()
- let file = item.getAsFile();
- let formData = new FormData()
-
- if (file) {
- formData.append('file', file)
- }
-
- fetch(upload, {
- method: 'POST',
- body: formData
- })
- .then((res: Response) => {
- return res.json()
- }).then(json => {
-
- json.map((file: any) => {
- let path = window.location.origin + file
- runInAction(() => {
- var doc: any;
-
- if (type.indexOf("image") !== -1) {
- doc = Documents.ImageDocument(path, { ...options, nativeWidth: 300, width: 300, })
- }
- if (type.indexOf("video") !== -1) {
- doc = Documents.VideoDocument(path, { ...options, nativeWidth: 300, width: 300, })
- }
- if (type.indexOf("audio") !== -1) {
- doc = Documents.AudioDocument(path, { ...options, nativeWidth: 300, width: 300, })
- }
- let docs = that.props.Document.GetT(KeyStore.Data, ListField);
- if (docs != FieldWaiting) {
- if (!docs) {
- docs = new ListField<Document>();
- that.props.Document.Set(KeyStore.Data, docs)
- }
- if (doc) {
- docs.Data.push(doc);
- }
-
- }
- })
- })
- })
-
-
- }
- }
- }
-}
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
new file mode 100644
index 000000000..f3c605f3e
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -0,0 +1,8 @@
+.PDS-flyout {
+ position: absolute;
+ z-index: 9999;
+ background-color: #d3d3d3;
+ box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
+ min-width: 150px;
+ color: black;
+} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
new file mode 100644
index 000000000..52f7914f3
--- /dev/null
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -0,0 +1,66 @@
+import * as React from "react";
+import './ParentDocumentSelector.scss';
+import { Doc } from "../../../new_fields/Doc";
+import { observer } from "mobx-react";
+import { observable, action, runInAction } from "mobx";
+import { Id } from "../../../new_fields/RefField";
+import { SearchUtil } from "../../util/SearchUtil";
+import { CollectionDockingView } from "./CollectionDockingView";
+
+@observer
+export class SelectorContextMenu extends React.Component<{ Document: Doc }> {
+ @observable private _docs: Doc[] = [];
+
+ constructor(props: { Document: Doc }) {
+ super(props);
+
+ this.fetchDocuments();
+ }
+
+ async fetchDocuments() {
+ const docs = await SearchUtil.Search(`data_l:"${this.props.Document[Id]}"`, true);
+ runInAction(() => this._docs = docs);
+ }
+
+ render() {
+ return (
+ <>
+ {this._docs.map(doc => <p><a onClick={() => CollectionDockingView.Instance.AddRightSplit(doc)}>{doc.title}</a></p>)}
+ </>
+ );
+ }
+}
+
+@observer
+export class ParentDocSelector extends React.Component<{ Document: Doc }> {
+ @observable hover = false;
+
+ @action
+ onMouseLeave = () => {
+ this.hover = false;
+ }
+
+ @action
+ onMouseEnter = () => {
+ this.hover = true;
+ }
+
+ render() {
+ let flyout;
+ if (this.hover) {
+ flyout = (
+ <div className="PDS-flyout">
+ <SelectorContextMenu Document={this.props.Document} />
+ </div>
+ );
+ }
+ return (
+ <span style={{ position: "relative", display: "inline-block" }}
+ onMouseEnter={this.onMouseEnter}
+ onMouseLeave={this.onMouseLeave}>
+ <p>^</p>
+ {flyout}
+ </span>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
new file mode 100644
index 000000000..737ffba7d
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -0,0 +1,12 @@
+.collectionfreeformlinkview-linkLine {
+ stroke: black;
+ transform: translate(10000px,10000px);
+ opacity: 0.5;
+ pointer-events: all;
+}
+.collectionfreeformlinkview-linkCircle {
+ stroke: rgb(0,0,0);
+ opacity: 0.5;
+ transform: translate(10000px,10000px);
+ pointer-events: all;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
new file mode 100644
index 000000000..63d2f7642
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -0,0 +1,58 @@
+import { observer } from "mobx-react";
+import { Utils } from "../../../../Utils";
+import "./CollectionFreeFormLinkView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { StrCast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { InkingControl } from "../../InkingControl";
+
+export interface CollectionFreeFormLinkViewProps {
+ A: Doc;
+ B: Doc;
+ LinkDocs: Doc[];
+ addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument: (document: Doc) => boolean;
+}
+
+@observer
+export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
+
+ onPointerDown = (e: React.PointerEvent) => {
+ if (e.button === 0 && !InkingControl.Instance.selectedTool) {
+ let a = this.props.A;
+ let b = this.props.B;
+ let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+ let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
+ let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
+ let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
+ this.props.LinkDocs.map(l => {
+ let width = l[WidthSym]();
+ l.x = (x1 + x2) / 2 - width / 2;
+ l.y = (y1 + y2) / 2 + 10;
+ if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+ });
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ render() {
+ 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);
+ return (
+ <>
+ <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine"
+ style={{ strokeWidth: `${l.length / 2}` }}
+ x1={`${x1}`} y1={`${y1}`}
+ x2={`${x2}`} y2={`${y2}`} />
+ <circle key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkCircle"
+ cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={5} onPointerDown={this.onPointerDown} />
+ </>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
new file mode 100644
index 000000000..30e158603
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -0,0 +1,12 @@
+.collectionfreeformlinksview-svgCanvas{
+ transform: translate(-10000px,-10000px);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 20000px;
+ height: 20000px;
+ pointer-events: none;
+ }
+ .collectionfreeformlinksview-container {
+ pointer-events: none;
+ } \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
new file mode 100644
index 000000000..d5ce4e1e7
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -0,0 +1,130 @@
+import { computed, IReactionDisposer, reaction, trace } from "mobx";
+import { observer } from "mobx-react";
+import { Utils } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DocumentView } from "../../nodes/DocumentView";
+import { CollectionViewProps } from "../CollectionSubView";
+import "./CollectionFreeFormLinksView.scss";
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+import React = require("react");
+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";
+
+@observer
+export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+
+ _brushReactionDisposer?: IReactionDisposer;
+ componentDidMount() {
+ this._brushReactionDisposer = reaction(
+ () => {
+ 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 x1 = NumCast(srcDoc.x);
+ let x2 = NumCast(dstDoc.x);
+ let x1w = NumCast(srcDoc.width, -1);
+ let x2w = NumCast(dstDoc.width, -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);
+ }
+ };
+ 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);
+ }
+ };
+ }
+ let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ if (dstBrushDocs === undefined) dstTarg.brushingDocs = dstBrushDocs = new List<Doc>();
+ else brushAction(dstBrushDocs);
+ if (srcBrushDocs === undefined) srcTarg.brushingDocs = srcBrushDocs = new List<Doc>();
+ else brushAction(srcBrushDocs);
+ }
+ });
+ });
+ });
+ }
+ componentWillUnmount() {
+ if (this._brushReactionDisposer) {
+ this._brushReactionDisposer();
+ }
+ }
+ documentAnchors(view: DocumentView) {
+ let equalViews = [view];
+ let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
+ if (containerDoc) {
+ equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
+ }
+ if (view.props.ContainingCollectionView) {
+ let collid = view.props.ContainingCollectionView.props.Document[Id];
+ DocListCast(this.props.Document[this.props.fieldKey]).
+ filter(child =>
+ child[Id] === collid).map(view =>
+ DocumentManager.Instance.getDocumentViews(view).map(view =>
+ equalViews.push(view)));
+ }
+ return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
+ }
+
+ @computed
+ get uniqueConnections() {
+ let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+ let srcViews = this.documentAnchors(connection.a);
+ 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);
+ 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] })
+ );
+ 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} />);
+ }
+
+ render() {
+ return (
+ <div className="collectionfreeformlinksview-container">
+ <svg className="collectionfreeformlinksview-svgCanvas">
+ {this.uniqueConnections}
+ </svg>
+ {this.props.children}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
new file mode 100644
index 000000000..c5b8fc5e8
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.scss
@@ -0,0 +1,24 @@
+@import "globalCssVariables";
+
+.collectionFreeFormRemoteCursors-cont {
+
+ position:absolute;
+ z-index: $remoteCursors-zindex;
+ transform-origin: 'center center';
+}
+.collectionFreeFormRemoteCursors-canvas {
+
+ position:absolute;
+ width: 20px;
+ height: 20px;
+ opacity: 0.5;
+ border-radius: 50%;
+ border: 2px solid black;
+}
+.collectionFreeFormRemoteCursors-symbol {
+ font-size: 14;
+ color: black;
+ // fontStyle: "italic",
+ margin-left: -12;
+ margin-top: 4;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
new file mode 100644
index 000000000..642118d75
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -0,0 +1,87 @@
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+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";
+
+@observer
+export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
+
+ protected getCursors(): CursorField[] {
+ let doc = this.props.Document;
+
+ let id = CurrentUserUtils.id;
+ if (!id) {
+ return [];
+ }
+
+ let cursors = Cast(doc.cursors, listSpec(CursorField));
+
+ return (cursors || []).filter(cursor => cursor.data.metadata.id !== id);
+ }
+
+ private crosshairs?: HTMLCanvasElement;
+ drawCrosshairs = (backgroundColor: string) => {
+ if (this.crosshairs) {
+ let ctx = this.crosshairs.getContext('2d');
+ if (ctx) {
+ ctx.fillStyle = backgroundColor;
+ ctx.fillRect(0, 0, 20, 20);
+
+ ctx.fillStyle = "black";
+ ctx.lineWidth = 0.5;
+
+ ctx.beginPath();
+
+ ctx.moveTo(10, 0);
+ ctx.lineTo(10, 8);
+
+ ctx.moveTo(10, 20);
+ ctx.lineTo(10, 12);
+
+ ctx.moveTo(0, 10);
+ ctx.lineTo(8, 10);
+
+ ctx.moveTo(20, 10);
+ ctx.lineTo(12, 10);
+
+ ctx.stroke();
+
+ // ctx.font = "10px Arial";
+ // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
+ }
+ }
+ }
+
+ get sharedCursors() {
+ 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>
+ );
+ });
+ }
+
+ render() {
+ return this.sharedCursors;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
new file mode 100644
index 000000000..063c9e2cf
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -0,0 +1,107 @@
+@import "../../globalCssVariables";
+
+.collectionfreeformview-ease {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+ transition: transform 1s;
+}
+
+.collectionfreeformview-none {
+ position: inherit;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ transform-origin: left top;
+}
+
+.collectionfreeformview-container {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ width: 100%;
+ }
+
+ //nested freeform views
+ // .collectionfreeformview-container {
+ // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
+ // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
+ // background-size: 30px 30px;
+ // }
+ box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+ border: 0px solid $light-color-secondary;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position: absolute;
+ .marqueeView {
+ overflow: hidden;
+ }
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+}
+
+
+.collectionfreeformview-overlay {
+ .collectionfreeformview>.jsx-parser {
+ position: inherit;
+ height: 100%;
+ }
+
+ .formattedTextBox-cont {
+ background: $light-color-secondary;
+ overflow: visible;
+ }
+
+ opacity: 0.99;
+ border: 0px solid transparent;
+ border-radius: $border-radius;
+ box-sizing: border-box;
+ position:absolute;
+ .marqueeView {
+ overflow: hidden;
+ }
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ .collectionfreeformview {
+ .formattedTextBox-cont {
+ background: yellow;
+ }
+ }
+}
+
+// selection border...?
+.border {
+ border-style: solid;
+ box-sizing: border-box;
+ width: 98%;
+ height: 98%;
+ border-radius: $border-radius;
+}
+
+//this is an animation for the blinking cursor!
+@keyframes blink {
+ 0% {
+ opacity: 0;
+ }
+
+ 49% {
+ opacity: 0;
+ }
+
+ 50% {
+ opacity: 1;
+ }
+}
+
+#prevCursor {
+ animation: blink 1s infinite;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
new file mode 100644
index 000000000..ad32f70dc
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -0,0 +1,404 @@
+import { action, computed, trace } from "mobx";
+import { observer } from "mobx-react";
+import { emptyFunction, returnFalse, returnOne } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DragManager } from "../../../util/DragManager";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch } from "../../../util/UndoManager";
+import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { CollectionSubView } from "../CollectionSubView";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema";
+import { Doc, WidthSym, HeightSym } from "../../../../new_fields/Doc";
+import { FieldValue, Cast, NumCast, BoolCast } from "../../../../new_fields/Types";
+import { pageSchema } from "../../nodes/ImageBox";
+import { Id } from "../../../../new_fields/RefField";
+import { InkField, StrokeData } from "../../../../new_fields/InkField";
+import { HistoryUtil } from "../../../util/History";
+
+export const panZoomSchema = createSchema({
+ panX: "number",
+ panY: "number",
+ scale: "number"
+});
+
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
+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 this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
+ private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
+ 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 + 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) => {
+ this._selectOnLoaded = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ this.addDocument(newBox, false);
+ }
+ private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
+ this.props.addDocument(newBox, false);
+ this.bringToFront(newBox);
+ return true;
+ }
+ private selectDocuments = (docs: Doc[]) => {
+ SelectionManager.DeselectAll;
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv =>
+ SelectionManager.SelectDoc(dv!, true));
+ }
+ public getActiveDocuments = () => {
+ const curPage = FieldValue(this.Document.curPage, -1);
+ return this.children.filter(doc => {
+ var page = NumCast(doc.page, -1);
+ return page === curPage || page === -1;
+ });
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) {
+ if (de.data.droppedDocuments.length) {
+ let dragDoc = de.data.droppedDocuments[0];
+ let zoom = NumCast(dragDoc.zoomBasis, 1);
+ let [xp, yp] = this.getTransform().transformPoint(de.x, de.y);
+ let x = xp - de.data.xOffset / zoom;
+ 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 => {
+ d.x = x + NumCast(d.x) - dropX;
+ d.y = y + NumCast(d.y) - dropY;
+ if (!NumCast(d.width)) {
+ d.width = 300;
+ }
+ if (!NumCast(d.height)) {
+ let nw = NumCast(d.nativeWidth);
+ let nh = NumCast(d.nativeHeight);
+ d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ }
+ this.bringToFront(d);
+ });
+ SelectionManager.ReselectAll();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ if ((CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) ||
+ (e.button === 0 && e.altKey)) && this.props.active())) ||
+ (!CollectionFreeFormView.RIGHT_BTN_DRAG &&
+ ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && this.props.active()))) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ }
+ }
+
+ onPointerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (!e.cancelBubble) {
+ let x = this.Document.panX || 0;
+ let y = this.Document.panY || 0;
+ let docs = this.children || [];
+ let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+ if (!this.isAnnotationOverlay) {
+ let minx = docs.length ? NumCast(docs[0].x) : 0;
+ let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+ let miny = docs.length ? NumCast(docs[0].y) : 0;
+ let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ let x = NumCast(doc.x);
+ let xe = x + NumCast(doc.width);
+ let y = NumCast(doc.y);
+ let ye = y + NumCast(doc.height);
+ 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 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 panelwidth = this._pwidth / this.zoomScaling() / 2;
+ let panelheight = this._pheight / this.zoomScaling() / 2;
+ if (x - dx < ranges[0][0] - panelwidth) x = ranges[0][1] + panelwidth + dx;
+ if (x - dx > ranges[0][1] + panelwidth) x = ranges[0][0] - panelwidth + dx;
+ if (y - dy < ranges[1][0] - panelheight) y = ranges[1][1] + panelheight + dy;
+ if (y - dy > ranges[1][1] + panelheight) y = ranges[1][0] - panelheight + dy;
+ }
+ this.setPan(x - dx, y - dy);
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerWheel = (e: React.WheelEvent): void => {
+ // if (!this.props.active()) {
+ // return;
+ // }
+ let childSelected = this.children.some(doc => {
+ var dv = DocumentManager.Instance.getDocumentView(doc);
+ return dv && SelectionManager.IsSelected(dv) ? true : false;
+ });
+ if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
+ return;
+ }
+ e.stopPropagation();
+ const coefficient = 1000;
+
+ if (e.ctrlKey) {
+ let deltaScale = (1 - (e.deltaY / coefficient));
+ 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 {
+ // if (modes[e.deltaMode] === 'pixels') coefficient = 50;
+ // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height??
+ let deltaScale = (1 - (e.deltaY / coefficient));
+ if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
+ deltaScale = 1 / this.zoomScaling();
+ }
+ if (deltaScale < 0) deltaScale = -deltaScale;
+ 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);
+ this.props.Document.scale = Math.abs(safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+ e.stopPropagation();
+ }
+ }
+
+ @action
+ setPan(panX: number, panY: number) {
+ this.panDisposer && clearTimeout(this.panDisposer);
+ this.props.Document.panTransformType = "None";
+ var scale = this.getLocalTransform().inverse().Scale;
+ const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
+ const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY));
+ this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ }
+
+ @action
+ onDrop = (e: React.DragEvent): void => {
+ var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+ super.onDrop(e, { x: pt[0], y: pt[1] });
+ }
+
+ onDragOver = (): void => {
+ }
+
+ bringToFront = (doc: Doc) => {
+ const docs = this.children;
+ 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;
+ }
+
+ panDisposer?: NodeJS.Timeout;
+ focusDocument = (doc: Doc) => {
+ const panX = this.Document.panX;
+ const panY = this.Document.panY;
+ const id = this.Document[Id];
+ const state = HistoryUtil.getState();
+ // TODO This technically isn't correct if type !== "doc", as
+ // currently nothing is done, but we should probably push a new state
+ if (state.type === "doc" && panX !== undefined && panY !== undefined) {
+ const init = state.initializers[id];
+ if (!init) {
+ state.initializers[id] = {
+ panX, panY
+ };
+ HistoryUtil.pushState(state);
+ } else if (init.panX !== panX || init.panY !== panY) {
+ init.panX = panX;
+ init.panY = panY;
+ HistoryUtil.pushState(state);
+ }
+ }
+ SelectionManager.DeselectAll();
+ const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ const newState = HistoryUtil.getState();
+ newState.initializers[id] = { panX: newPanX, panY: newPanY };
+ HistoryUtil.pushState(newState);
+ this.setPan(newPanX, newPanY);
+ this.props.Document.panTransformType = "Ease";
+ this.props.focus(this.props.Document);
+ this.panDisposer = 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,
+ ScreenToLocalTransform: this.getTransform,
+ isTopMost: false,
+ selectOnLoad: document[Id] === this._selectOnLoaded,
+ PanelWidth: document[WidthSym],
+ PanelHeight: document[HeightSym],
+ ContentScaling: returnOne,
+ ContainingCollectionView: this.props.CollectionView,
+ focus: this.focusDocument,
+ parentActive: this.props.active,
+ whenActiveChanged: this.props.whenActiveChanged,
+ bringToFront: this.bringToFront,
+ addDocTab: this.props.addDocTab,
+ };
+ }
+
+ @computed.struct
+ get views() {
+ let curPage = FieldValue(this.Document.curPage, -1);
+ let docviews = this.children.reduce((prev, doc) => {
+ if (!(doc instanceof Doc)) return prev;
+ var page = NumCast(doc.page, -1);
+ if (page === curPage || page === -1) {
+ let minim = BoolCast(doc.isMinimized, false);
+ if (minim === undefined || !minim) {
+ prev.push(<CollectionFreeFormDocumentView key={doc[Id]} {...this.getDocumentViewProps(doc)} />);
+ }
+ }
+ return prev;
+ }, [] as JSX.Element[]);
+
+ setTimeout(() => this._selectOnLoaded = "", 600);// bcz: surely there must be a better way ....
+
+ return docviews;
+ }
+
+ @action
+ onCursorMove = (e: React.PointerEvent) => {
+ super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
+ }
+
+ private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />];
+ render() {
+ const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
+ const easing = () => this.props.Document.panTransformType === "Ease";
+ return (
+ <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
+ style={{ borderRadius: "inherit" }}
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} >
+ <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
+ addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
+ getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+ <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
+ easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
+
+ <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+ {this.childViews}
+ </InkingCanvas>
+ </CollectionFreeFormLinksView>
+ <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
+ </CollectionFreeFormViewPannableContents>
+ </MarqueeView>
+ <CollectionFreeFormOverlayView {...this.getDocumentViewProps(this.props.Document)} {...this.props} />
+ </div>
+ );
+ }
+}
+
+@observer
+class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
+ @computed get overlayView() {
+ return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ }
+ render() {
+ return this.overlayView;
+ }
+}
+
+@observer
+class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
+ @computed get backgroundView() {
+ return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
+ isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+ }
+ render() {
+ return this.props.Document.backgroundLayout ? this.backgroundView : (null);
+ }
+}
+
+interface CollectionFreeFormViewPannableContentsProps {
+ centeringShiftX: () => number;
+ centeringShiftY: () => number;
+ panX: () => number;
+ panY: () => number;
+ zoomScaling: () => number;
+ easing: () => boolean;
+}
+
+@observer
+class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{
+ render() {
+ let freeformclass = "collectionfreeformview" + (this.props.easing() ? "-ease" : "-none");
+ const cenx = this.props.centeringShiftX();
+ const ceny = this.props.centeringShiftY();
+ const panx = -this.props.panX();
+ const pany = -this.props.panY();
+ const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire
+ return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
+ {this.props.children}
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
new file mode 100644
index 000000000..6e8ec8662
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -0,0 +1,26 @@
+
+.marqueeView {
+ position: inherit;
+ top:0;
+ left:0;
+ width:100%;
+ height:100%;
+}
+.marquee {
+ border-style: dashed;
+ box-sizing: border-box;
+ position: absolute;
+ border-width: 1px;
+ border-color: black;
+ pointer-events: none;
+ .marquee-legend {
+ bottom:-18px;
+ left:0;
+ position: absolute;
+ font-size: 9;
+ white-space:nowrap;
+ }
+ .marquee-legend::after {
+ content: "Press: c (collection), 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
new file mode 100644
index 000000000..c3c4115b8
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -0,0 +1,367 @@
+import * as htmlToImage from "html-to-image";
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Docs } from "../../../documents/Documents";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { PreviewCursor } from "../../PreviewCursor";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./MarqueeView.scss";
+import React = require("react");
+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 { List } from "../../../../new_fields/List";
+import { ImageField } from "../../../../new_fields/URLField";
+import { Template, Templates } from "../../Templates";
+import { Gateway } from "../../../northstar/manager/Gateway";
+import { DocServer } from "../../../DocServer";
+import { Id } from "../../../../new_fields/RefField";
+
+interface MarqueeViewProps {
+ getContainerTransform: () => Transform;
+ getTransform: () => Transform;
+ container: CollectionFreeFormView;
+ addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ activeDocuments: () => Doc[];
+ selectDocuments: (docs: Doc[]) => void;
+ removeDocument: (doc: Doc) => boolean;
+ addLiveTextDocument: (doc: Doc) => void;
+ isSelected: () => boolean;
+}
+
+@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;
+ @observable _downY: number = 0;
+ @observable _visible: boolean = false;
+ _commandExecuted = false;
+
+ @action
+ cleanupInteractions = (all: boolean = false) => {
+ if (all) {
+ document.removeEventListener("pointerup", this.onPointerUp, true);
+ document.removeEventListener("pointermove", this.onPointerMove, true);
+ }
+ document.removeEventListener("keydown", this.marqueeCommand, true);
+ 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);
+ 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) {
+ //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
+ e.preventDefault();
+ (async () => {
+ let text: string = await navigator.clipboard.readText();
+ let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
+ while (ns.length > 0 && ns[0].split("\t").length < 2) {
+ ns.splice(0, 1);
+ }
+ if (ns.length > 0) {
+ let columns = ns[0].split("\t");
+ let docList: Doc[] = [];
+ let groupAttr: string | number = "";
+ 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 doc = new Doc();
+ columns.forEach((col, i) => doc[columns[i]] = (values.length > i ? ((values[i].indexOf(Number(values[i]).toString()) !== -1) ? Number(values[i]) : values[i]) : undefined));
+ if (groupAttr) {
+ doc._group = groupAttr;
+ }
+ doc.title = i.toString();
+ 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);
+ }
+ })();
+ } else {
+ let newBox = Docs.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
+ this.props.addLiveTextDocument(newBox);
+ }
+ e.stopPropagation();
+ }
+ @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())) {
+ document.addEventListener("pointermove", this.onPointerMove, true);
+ document.addEventListener("pointerup", this.onPointerUp, true);
+ document.addEventListener("keydown", this.marqueeCommand, true);
+ // bcz: do we need this? it kills the context menu on the main collection
+ // e.stopPropagation();
+ }
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ this._lastX = e.pageX;
+ this._lastY = e.pageY;
+ if (!e.cancelBubble) {
+ if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD ||
+ Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) {
+ if (!this._commandExecuted) {
+ this._visible = true;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ }
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ if (this._visible) {
+ let mselect = this.marqueeSelect();
+ if (!e.shiftKey) {
+ SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+ }
+ this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+ }
+ this.cleanupInteractions(true);
+ if (e.altKey) {
+ e.preventDefault();
+ }
+ }
+
+ @action
+ onClick = (e: React.MouseEvent): void => {
+ if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress);
+ // let the DocumentView stopPropagation of this event when it selects this document
+ } else { // why do we get a click event when the cursor have moved a big distance?
+ // let's cut it off here so no one else has to deal with it.
+ e.stopPropagation();
+ }
+ }
+
+ intersectRect(r1: { left: number, top: number, width: number, height: number },
+ r2: { left: number, top: number, width: number, height: number }) {
+ return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+ }
+
+ @computed
+ get Bounds() {
+ let left = this._downX < this._lastX ? this._downX : this._lastX;
+ let top = this._downY < this._lastY ? this._downY : this._lastY;
+ let topLeft = this.props.getTransform().transformPoint(left, top);
+ let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ }
+
+ @undoBatch
+ @action
+ marqueeCommand = async (e: KeyboardEvent) => {
+ if (this._commandExecuted || (e as any).propagationIsStopped) {
+ return;
+ }
+ if (e.key === "Backspace" || e.key === "Delete" || e.key === "d") {
+ this._commandExecuted = true;
+ e.stopPropagation();
+ (e as any).propagationIsStopped = true;
+ this.marqueeSelect().map(d => this.props.removeDocument(d));
+ let ink = Cast(this.props.container.props.Document.ink, InkField);
+ if (ink) {
+ this.marqueeInkDelete(ink.inkData);
+ }
+ SelectionManager.DeselectAll();
+ this.cleanupInteractions(false);
+ e.stopPropagation();
+ }
+ if (e.key === "c" || e.key === "s" || e.key === "e" || e.key === "p") {
+ this._commandExecuted = true;
+ e.stopPropagation();
+ (e as any).propagationIsStopped = true;
+ let bounds = this.Bounds;
+ let selected = this.marqueeSelect();
+ 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;
+ });
+ }
+ 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);
+ let newCollection = Docs.FreeformDocument(selected, {
+ x: bounds.left,
+ y: bounds.top,
+ panX: 0,
+ panY: 0,
+ borderRounding: e.key === "e" ? -1 : undefined,
+ scale: zoomBasis,
+ width: bounds.width * zoomBasis,
+ height: bounds.height * zoomBasis,
+ ink: inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined,
+ title: e.key === "s" ? "-summary-" : e.key === "p" ? "-summary-" : "a nested collection",
+ });
+ this.marqueeInkDelete(inkData);
+
+ if (e.key === "s" || e.key === "p") {
+
+ htmlToImage.toPng(this._mainCont.current!, { width: bounds.width * zoomBasis, height: bounds.height * zoomBasis, quality: 1 }).then((dataUrl) => {
+ selected.map(d => {
+ this.props.removeDocument(d);
+ d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
+ d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
+ d.page = -1;
+ return d;
+ });
+ let summary = Docs.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" });
+ summary.proto!.thumbnail = new ImageField(new URL(dataUrl));
+ summary.proto!.templates = new List<string>([Templates.ImageOverlay(Math.min(50, bounds.width), bounds.height * Math.min(50, bounds.width) / bounds.width, "thumbnail")]);
+ newCollection.proto!.summaryDoc = summary;
+ selected = [newCollection];
+ newCollection.x = bounds.left + bounds.width;
+ this.props.addDocument(newCollection, false);
+ summary.proto!.summarizedDocs = new List<Doc>(selected.map(s => s.proto!));
+ //summary.proto!.maximizeOnRight = true;
+ //summary.proto!.isButton = true;
+ //let scrpt = this.props.getTransform().inverse().transformPoint(bounds.left, bounds.top);
+ // selected.map(summarizedDoc => {
+ // let maxx = NumCast(summarizedDoc.x, undefined);
+ // let maxy = NumCast(summarizedDoc.y, undefined);
+ // let maxw = NumCast(summarizedDoc.width, undefined);
+ // let maxh = NumCast(summarizedDoc.height, undefined);
+ // summarizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), 0]);
+ // });
+ this.props.addLiveTextDocument(summary);
+ });
+ }
+ else {
+ this.props.addDocument(newCollection, false);
+ SelectionManager.DeselectAll();
+ this.props.selectDocuments([newCollection]);
+ }
+ this.cleanupInteractions(false);
+ }
+ }
+ @action
+ marqueeInkSelect(ink: Map<any, any>) {
+ let idata = new Map();
+ let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+ let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
+ ink.forEach((value: StrokeData, key: string, map: any) => {
+ if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+ idata.set(key,
+ {
+ pathData: value.pathData.map(val => ({ x: val.x + centerShiftX, y: val.y + centerShiftY })),
+ color: value.color,
+ width: value.width,
+ tool: value.tool,
+ page: -1
+ });
+ }
+ });
+ return idata;
+ }
+
+ @action
+ marqueeInkDelete(ink?: Map<any, any>) {
+ // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
+ // ink.forEach((value: StrokeData, key: string, map: any) =>
+ // InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
+
+ if (ink) {
+ let idata = new Map();
+ ink.forEach((value: StrokeData, key: string, map: any) =>
+ !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+ Doc.SetOnPrototype(this.props.container.props.Document, "ink", new InkField(idata));
+ }
+ }
+
+ marqueeSelect() {
+ let selRect = this.Bounds;
+ let selection: Doc[] = [];
+ this.props.activeDocuments().map(doc => {
+ var z = NumCast(doc.zoomBasis, 1);
+ var x = NumCast(doc.x);
+ var y = NumCast(doc.y);
+ var w = NumCast(doc.width) / z;
+ var h = NumCast(doc.height) / z;
+ if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
+ selection.push(doc);
+ }
+ });
+ return selection;
+ }
+
+ @computed
+ get marqueeDiv() {
+ let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+ 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 = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+ return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
+ {!this._visible ? null : this.marqueeDiv}
+ <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
diff --git a/src/client/views/globalCssVariables.scss b/src/client/views/globalCssVariables.scss
new file mode 100644
index 000000000..838d4d9ac
--- /dev/null
+++ b/src/client/views/globalCssVariables.scss
@@ -0,0 +1,35 @@
+@import url("https://fonts.googleapis.com/css?family=Noto+Sans:400,700|Crimson+Text:400,400i,700");
+// colors
+$light-color: #fcfbf7;
+$light-color-secondary:#f1efeb;
+//$main-accent: #61aaa3;
+$main-accent: #aaaaa3;
+// $alt-accent: #cdd5ec;
+// $alt-accent: #cdeceb;
+//$alt-accent: #59dff7;
+$alt-accent: #c2c2c5;
+$lighter-alt-accent: rgb(207, 220, 240);
+$intermediate-color: #9c9396;
+$dark-color: #121721;
+// fonts
+$sans-serif: "Noto Sans", sans-serif;
+// $sans-serif: "Roboto Slab", sans-serif;
+$serif: "Crimson Text", serif;
+// misc values
+$border-radius: 0.3em;
+//
+
+ // dragged items
+$contextMenu-zindex: 1000; // context menu shows up over everything
+$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
+$docDecorations-zindex: 998; // then doc decorations appear over everything else
+$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
+$COLLECTION_BORDER_WIDTH: 1;
+$MINIMIZED_ICON_SIZE:25;
+$MAX_ROW_HEIGHT: 44px;
+:export {
+ contextMenuZindex: $contextMenu-zindex;
+ COLLECTION_BORDER_WIDTH: $COLLECTION_BORDER_WIDTH;
+ MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE;
+ MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
+} \ No newline at end of file
diff --git a/src/client/views/globalCssVariables.scss.d.ts b/src/client/views/globalCssVariables.scss.d.ts
new file mode 100644
index 000000000..9788d31f7
--- /dev/null
+++ b/src/client/views/globalCssVariables.scss.d.ts
@@ -0,0 +1,10 @@
+
+interface IGlobalScss {
+ contextMenuZindex: string; // context menu shows up over everything
+ COLLECTION_BORDER_WIDTH: string;
+ MINIMIZED_ICON_SIZE: string;
+ MAX_ROW_HEIGHT: string;
+}
+declare const globalCssVariables: IGlobalScss;
+
+export = globalCssVariables; \ No newline at end of file
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx
index a2c7be1a8..3e4ed6bf1 100644
--- a/src/client/views/nodes/Annotation.tsx
+++ b/src/client/views/nodes/Annotation.tsx
@@ -1,16 +1,16 @@
import "./ImageBox.scss";
-import React = require("react")
-import { observer } from "mobx-react"
+import React = require("react");
+import { observer } from "mobx-react";
import { observable, action } from 'mobx';
-import 'react-pdf/dist/Page/AnnotationLayer.css'
+import 'react-pdf/dist/Page/AnnotationLayer.css';
-interface IProps{
+interface IProps {
Span: HTMLSpanElement;
- X: number;
- Y: number;
- Highlights: any[];
- Annotations: any[];
- CurrAnno: any[];
+ X: number;
+ Y: number;
+ Highlights: any[];
+ Annotations: any[];
+ CurrAnno: any[];
}
@@ -23,95 +23,95 @@ interface IProps{
*/
@observer
export class Annotation extends React.Component<IProps> {
-
+
/**
* changes color of the span (highlighted section)
*/
- onColorChange = (e:React.PointerEvent) => {
- if (e.currentTarget.innerHTML == "r"){
- this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)"
- } else if (e.currentTarget.innerHTML == "b"){
- this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)"
- } else if (e.currentTarget.innerHTML == "y"){
- this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)"
- } else if (e.currentTarget.innerHTML == "g"){
- this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)"
+ onColorChange = (e: React.PointerEvent) => {
+ if (e.currentTarget.innerHTML === "r") {
+ this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)";
+ } else if (e.currentTarget.innerHTML === "b") {
+ this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)";
+ } else if (e.currentTarget.innerHTML === "y") {
+ this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)";
+ } else if (e.currentTarget.innerHTML === "g") {
+ this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)";
}
-
+
}
/**
* removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this
*/
@action
- onRemove = (e:any) => {
- let index:number = -1;
+ onRemove = (e: any) => {
+ let index: number = -1;
//finding the highlight in the highlight array
this.props.Highlights.forEach((e) => {
- for (let i = 0; i < e.spans.length; i++){
- if (e.spans[i] == this.props.Span){
- index = this.props.Highlights.indexOf(e);
- this.props.Highlights.splice(index, 1);
+ for (const span of e.spans) {
+ if (span === this.props.Span) {
+ index = this.props.Highlights.indexOf(e);
+ this.props.Highlights.splice(index, 1);
}
}
- })
+ });
//removing from CurrAnno and Annotation array
- this.props.Annotations.splice(index, 1);
- this.props.CurrAnno.pop()
-
+ this.props.Annotations.splice(index, 1);
+ this.props.CurrAnno.pop();
+
//removing span from div
- if(this.props.Span.parentElement){
- let nodesArray = this.props.Span.parentElement.childNodes;
+ if (this.props.Span.parentElement) {
+ let nodesArray = this.props.Span.parentElement.childNodes;
nodesArray.forEach((e) => {
- if (e == this.props.Span){
- if (this.props.Span.parentElement){
+ if (e === this.props.Span) {
+ if (this.props.Span.parentElement) {
this.props.Highlights.forEach((item) => {
- if (item == e){
- item.remove();
+ if (item === e) {
+ item.remove();
}
- })
- e.remove();
+ });
+ e.remove();
}
}
- })
+ });
}
-
-
+
+
}
render() {
return (
- <div
- style = {{
- position: "absolute",
- top: "20px",
- left: "0px",
- zIndex: 1,
- transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
-
- }}>
- <div style = {{width:"200px", height:"50px", backgroundColor: "orange"}}>
+ <div
+ style={{
+ position: "absolute",
+ top: "20px",
+ left: "0px",
+ zIndex: 1,
+ transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
+
+ }}>
+ <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}>
<button
- style = {{borderRadius: "25px", width:"25%", height:"100%"}}
- onClick = {this.onRemove}
+ style={{ borderRadius: "25px", width: "25%", height: "100%" }}
+ onClick={this.onRemove}
>x</button>
- <div style = {{width:"75%", height: "100%" , display:"inline-block"}}>
- <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"red", borderRadius:"50%", color: "transparent"}}>r</button>
- <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"blue", borderRadius:"50%", color: "transparent"}}>b</button>
- <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"yellow", borderRadius:"50%", color:"transparent"}}>y</button>
- <button onPointerDown = {this.onColorChange} style = {{backgroundColor:"green", borderRadius:"50%", color:"transparent"}}>g</button>
+ <div style={{ width: "75%", height: "100%", display: "inline-block" }}>
+ <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button>
+ <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button>
+ <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button>
+ <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button>
</div>
-
+
</div>
- <div style = {{width:"200px", height:"200"}}>
- <textarea style = {{width: "100%", height: "100%"}}
- defaultValue = "Enter Text Here..."
-
+ <div style={{ width: "200px", height: "200" }}>
+ <textarea style={{ width: "100%", height: "100%" }}
+ defaultValue="Enter Text Here..."
+
></textarea>
</div>
</div>
-
+
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index f7d89843d..be12dced3 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,44 +1,27 @@
-import React = require("react")
+import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
-import { observer } from "mobx-react"
-import { ContextMenu } from "../../views/ContextMenu";
-import { observable, action } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
-import { AudioField } from "../../../fields/AudioField";
-import "./AudioBox.scss"
-import { NumberField } from "../../../fields/NumberField";
+import { observer } from "mobx-react";
+import "./AudioBox.scss";
+import { Cast } from "../../../new_fields/Types";
+import { AudioField } from "../../../new_fields/URLField";
+const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
export class AudioBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(AudioBox) }
+ public static LayoutString() { return FieldView.LayoutString(AudioBox); }
- constructor(props: FieldViewProps) {
- super(props);
- }
-
-
-
- componentDidMount() {
- }
-
- componentWillUnmount() {
- }
-
-
render() {
- let field = this.props.doc.Get(this.props.fieldKey)
- let path = field == FieldWaiting ? "http://techslides.com/demos/samples/sample.mp3":
- field instanceof AudioField ? field.Data.href : "http://techslides.com/demos/samples/sample.mp3";
-
+ let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
+ let path = field.url.href;
+
return (
<div>
- <audio controls className = "audiobox-cont">
- <source src = {path} type="audio/mpeg"/>
+ <audio controls className="audiobox-cont">
+ <source src={path} type="audio/mpeg" />
Not supported.
</audio>
</div>
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 50dc5a619..a0efc3154 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,84 +1,288 @@
-import { computed, trace } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { KeyStore } from "../../../fields/KeyStore";
-import { NumberField } from "../../../fields/NumberField";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { OmitKeys, Utils } from "../../../Utils";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
-import { DocumentView, DocumentViewProps } from "./DocumentView";
+import { UndoManager } from "../../util/UndoManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocComponent } from "../DocComponent";
+import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
+export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+}
+
+const schema = createSchema({
+ zoomBasis: "number",
+ zIndex: "number",
+});
+
+//TODO Types: The import order is wrong, so positionSchema is undefined
+type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>;
+const FreeformDocument = makeInterface(schema, positionSchema);
@observer
-export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
private _mainCont = React.createRef<HTMLDivElement>();
+ private _downX: number = 0;
+ private _downY: number = 0;
+ _bringToFrontDisposer?: IReactionDisposer;
- constructor(props: DocumentViewProps) {
- super(props);
- }
- get screenRect(): ClientRect | DOMRect {
- if (this._mainCont.current) {
- return this._mainCont.current.getBoundingClientRect();
- }
- return new DOMRect();
- }
-
- @computed
- get transform(): string {
- return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`;
+ @computed get transform() {
+ return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `;
}
- @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); }
- @computed get width(): number { return this.props.Document.Width(); }
- @computed get height(): number { return this.props.Document.Height(); }
- @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
- @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+ @computed get X() { return FieldValue(this.Document.x, 0); }
+ @computed get Y() { return FieldValue(this.Document.y, 0); }
+ @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
+ @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
+ @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
- this.props.Document.SetData(KeyStore.Width, w, NumberField)
+ this.Document.width = w;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w)
+ this.Document.height = this.nativeHeight / this.nativeWidth * w;
}
}
-
set height(h: number) {
- this.props.Document.SetData(KeyStore.Height, h, NumberField);
+ this.Document.height = h;
if (this.nativeWidth && this.nativeHeight) {
- this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h)
+ this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
+ contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
+ panelWidth = () => this.props.PanelWidth();
+ panelHeight = () => this.props.PanelHeight();
+ toggleMinimized = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs));
+ getTransform = (): Transform => this.props.ScreenToLocalTransform()
+ .translate(-this.X, -this.Y)
+ .scale(1 / this.contentScaling()).scale(1 / this.zoom)
- set zIndex(h: number) {
- this.props.Document.SetData(KeyStore.ZIndex, h, NumberField)
+ @computed
+ get docView() {
+ return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit}
+ toggleMinimized={this.toggleMinimized}
+ ContentScaling={this.contentScaling}
+ ScreenToLocalTransform={this.getTransform}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ />;
}
- contentScaling = () => {
- return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
+ componentDidMount() {
+ this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => {
+ this.props.bringToFront(this.props.Document);
+ if (values instanceof List) {
+ let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
+ this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
+ this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
+ }
+ }, { fireImmediately: true });
}
- getTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().
- translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling());
+ componentWillUnmount() {
+ if (this._bringToFrontDisposer) this._bringToFrontDisposer();
}
- @computed
- get docView() {
- return <DocumentView {...this.props}
- ContentScaling={this.contentScaling}
- ScreenToLocalTransform={this.getTransform}
- />
+ animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
+
+ setTimeout(() => {
+ let now = Date.now();
+ let progress = Math.min(1, (now - stime) / 200);
+ let pval = maximizing ?
+ [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
+ [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
+ this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ this.props.Document.x = pval[0];
+ this.props.Document.y = pval[1];
+ if (first) {
+ this.props.Document.proto!.willMaximize = false;
+ }
+ if (now < stime + 200) {
+ this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
+ }
+ else {
+ if (!maximizing) {
+ this.props.Document.proto!.isMinimized = true;
+ this.props.Document.x = targ[0];
+ this.props.Document.y = targ[1];
+ this.props.Document.width = width;
+ this.props.Document.height = height;
+ }
+ this.props.Document.proto!.isIconAnimating = undefined;
+ }
+ },
+ 2);
+ }
+ @action
+ public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => {
+ SelectionManager.DeselectAll();
+ let isMinimized: boolean | undefined;
+ let minimizedDoc: Doc | undefined = this.props.Document;
+ if (!maximizedDocs) {
+ minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) maximizedDocs = await DocListCastAsync(minimizedDoc.maximizedDocs);
+ }
+ if (minimizedDoc && maximizedDocs) {
+ let minimizedTarget = minimizedDoc;
+ if (!CollectionFreeFormDocumentView._undoBatch) {
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
+ maximizedDocs.map(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2;
+ let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2;
+ if (minx !== undefined && miny !== undefined) {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
+ maximizedDoc.willMaximize = isMinimized;
+ maximizedDoc.isMinimized = false;
+ maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
+ }
+ }
+ });
+ setTimeout(() => {
+ CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end();
+ CollectionFreeFormDocumentView._undoBatch = undefined;
+ }, 500);
+ }
+ }
+ private _lastTap: number = 0;
+ static _undoBatch?: UndoManager.Batch = undefined;
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if (e.button === 0 && e.altKey) {
+ e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started
+ } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document
+ if (Date.now() - this._lastTap < 300) {
+ if (e.buttons === 1) {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ } else {
+ this._lastTap = Date.now();
+ }
+ }
+ onPointerUp = (e: PointerEvent): void => {
+
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) {
+ this.props.addDocTab(this.props.Document);
+ }
+ e.stopPropagation();
+ }
+ onClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ let altKey = e.altKey;
+ if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ let isExpander = (e.target as any).id === "isExpander";
+ if (BoolCast(this.props.Document.isButton, false) || isExpander) {
+ let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
+ let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+ let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
+ let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let expandedDocs: Doc[] = [];
+ expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+ // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []),
+ // ...(maximizedDocs ? maximizedDocs : []),
+ // ...(summarizedDocs ? summarizedDocs : []),];
+ if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
+ if (!hasView && ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight))) {
+ let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ SelectionManager.DeselectAll();
+ expandedDocs.forEach(maxDoc => {
+ maxDoc.isMinimized = false;
+ if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) {
+ CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(maxDoc));
+ }
+ });
+ }
+ } else {
+ //if (altKey) this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false));
+ this.toggleIcon(expandedDocs);
+ }
+ }
+ else if (linkedToDocs.length || linkedFromDocs.length) {
+ SelectionManager.DeselectAll();
+ let linkedFwdDocs = [
+ linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
+ linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+ if (linkedFwdDocs) {
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], altKey);
+ }
+ }
+ }
+ }
+ }
+
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
+
+ borderRounding = () => {
+ let br = NumCast(this.props.Document.borderRounding);
+ return br >= 0 ? br :
+ NumCast(this.props.Document.nativeWidth) === 0 ?
+ Math.min(this.props.PanelWidth(), this.props.PanelHeight())
+ : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0);
}
render() {
+ let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc)));
+ let zoomFade = 1;
+ //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1);
+ let transform = this.getTransform().scale(this.contentScaling()).inverse();
+ var [sptX, sptY] = transform.transformPoint(0, 0);
+ let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight());
+ let w = bptX - sptX;
+ //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1;
+ const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800);
+ let fadeUp = .75 * screenWidth;
+ let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth;
+ zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1;
+
return (
- <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{
- transformOrigin: "left top",
- transform: this.transform,
- width: this.width,
- height: this.height,
- position: "absolute",
- zIndex: this.zIndex,
- backgroundColor: "transparent"
- }} >
+ <div className="collectionFreeFormDocumentView-container" ref={this._mainCont}
+ onPointerDown={this.onPointerDown}
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter}
+ onClick={this.onClick}
+ style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
+ BoolCast(this.props.Document.protoBrush, false) ?
+ `${1 * this.getTransform().Scale}px` : "0px",
+ opacity: zoomFade,
+ borderRadius: `${this.borderRounding()}px`,
+ transformOrigin: "left top",
+ transform: this.transform,
+ pointerEvents: (zoomFade < 0.09 ? "none" : "all"),
+ width: this.width,
+ height: this.height,
+ position: "absolute",
+ zIndex: this.Document.zIndex || 0,
+ backgroundColor: "transparent"
+ }} >
{this.docView}
</div>
);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
new file mode 100644
index 000000000..d2cb11586
--- /dev/null
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -0,0 +1,111 @@
+import { computed, trace } from "mobx";
+import { observer } from "mobx-react";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionSchemaView } from "../collections/CollectionSchemaView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
+import { AudioBox } from "./AudioBox";
+import { DocumentViewProps } from "./DocumentView";
+import "./DocumentView.scss";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { ImageBox } from "./ImageBox";
+import { IconBox } from "./IconBox";
+import { KeyValueBox } from "./KeyValueBox";
+import { PDFBox } from "./PDFBox";
+import { VideoBox } from "./VideoBox";
+import { FieldView } from "./FieldView";
+import { WebBox } from "./WebBox";
+import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
+import React = require("react");
+import { FieldViewProps } from "./FieldView";
+import { Without, OmitKeys } from "../../../Utils";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
+import { List } from "../../../new_fields/List";
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+
+type BindingProps = Without<FieldViewProps, 'fieldKey'>;
+export interface JsxBindings {
+ props: BindingProps;
+}
+
+class ObserverJsxParser1 extends JsxParser {
+ constructor(props: any) {
+ super(props);
+ observer(this as any);
+ }
+}
+
+const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+
+@observer
+export class DocumentContentsView extends React.Component<DocumentViewProps & {
+ isSelected: () => boolean,
+ select: (ctrl: boolean) => void,
+ layoutKey: string,
+}> {
+ @computed get layout(): string {
+ const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ if (layout === undefined) {
+ return this.props.Document.data ?
+ "<FieldView {...props} fieldKey='data' />" :
+ KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ } else if (typeof layout === "string") {
+ return layout;
+ } else {
+ return "<p>Loading layout</p>";
+ }
+ }
+
+ CreateBindings(): JsxBindings {
+ return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+ }
+
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
+ }
+ return new List<string>();
+ }
+ @computed get finalLayout() {
+ const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
+ let base = baseLayout;
+ let layout = baseLayout;
+
+ // bcz: templates are intended only for a document's primary layout or overlay (not background). However,
+ // a DocumentContentsView is used to render annotation overlays, so we detect that here
+ // by checking the layoutKey. This should probably be moved into
+ // a prop so that the overlay can explicitly turn off templates.
+ if ((this.props.layoutKey === "overlayLayout" && StrCast(this.props.Document.layout).indexOf("CollectionView") !== -1) ||
+ (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) {
+ this.templates.forEach(template => {
+ let self = this;
+ // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller
+ // than the width/height of the containing document
+ function convertConstantsToNative(match: string, offset: number, x: string) {
+ let px = Number(match.replace("px", ""));
+ return `${Math.min(NumCast(self.props.Document.height, 0),
+ Math.min(NumCast(self.props.Document.width, 0),
+ px * self.props.ScreenToLocalTransform().Scale))}px`;
+ }
+ let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative);
+ layout = nativizedTemplate.replace("{layout}", base);
+ base = layout;
+ });
+ }
+ return layout;
+ }
+
+ render() {
+ if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
+ return <ObserverJsxParser
+ components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+ bindings={this.CreateBindings()}
+ jsx={this.finalLayout}
+ showWarnings={true}
+ onError={(test: any) => { console.log(test); }}
+ />;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index ab913897b..7c72fb6e6 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,23 +1,33 @@
-.documentView-node {
- position: absolute;
- background: #cdcdcd;
- //overflow: hidden;
- &.minimized {
- width: 30px;
- height: 30px;
- }
- .top {
- background: #232323;
- height: 20px;
- cursor: pointer;
- }
- .content {
- padding: 20px 20px;
- height: auto;
- box-sizing: border-box;
- }
- .scroll-box {
- overflow-y: scroll;
- height: calc(100% - 20px);
- }
+@import "../globalCssVariables";
+
+.documentView-node, .documentView-node-topmost {
+ position: inherit;
+ top: 0;
+ left:0;
+ // background: $light-color; //overflow: hidden;
+ transform-origin: left top;
+
+ &.minimized {
+ width: 30px;
+ height: 30px;
+ }
+
+ .top {
+ height: 20px;
+ cursor: pointer;
+ }
+
+ .content {
+ padding: 20px 20px;
+ height: auto;
+ box-sizing: border-box;
+ }
+
+ .scroll-box {
+ overflow-y: scroll;
+ height: calc(100% - 20px);
+ }
+}
+.documentView-node-topmost {
+ background: white;
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 907762837..d690893ef 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,40 +1,36 @@
-import { action, computed, IReactionDisposer, runInAction, reaction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Field, FieldWaiting, Opt } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from "../../../fields/KeyStore";
-import { ListField } from "../../../fields/ListField";
-import { DragManager } from "../../util/DragManager";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { Copy, ObjectField } from "../../../new_fields/ObjectField";
+import { Id } from "../../../new_fields/RefField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { emptyFunction, Utils } from "../../../Utils";
+import { DocServer } from "../../DocServer";
+import { Docs } from "../../documents/Documents";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager, dropActionType } from "../../util/DragManager";
+import { SearchUtil } from "../../util/SearchUtil";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
-import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionView, CollectionViewType } from "../collections/CollectionView";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
-import { ImageBox } from "../nodes/ImageBox";
-import { VideoBox } from "../nodes/VideoBox";
-import { AudioBox } from "../nodes/AudioBox";
-import { Documents } from "../../documents/Documents"
-import { KeyValueBox } from "./KeyValueBox"
-import { WebBox } from "../nodes/WebBox";
-import { PDFBox } from "../nodes/PDFBox";
+import { DocComponent } from "../DocComponent";
+import { PresentationView } from "../PresentationView";
+import { Template } from "./../Templates";
+import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { TextField } from "../../../fields/TextField";
-import { DocumentManager } from "../../util/DocumentManager";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faTrash } from '@fortawesome/free-solid-svg-icons';
-import { faExpandArrowsAlt } from '@fortawesome/free-solid-svg-icons';
-import { faCompressArrowsAlt } from '@fortawesome/free-solid-svg-icons';
-import { faLayerGroup } from '@fortawesome/free-solid-svg-icons';
-import { faAlignCenter } from '@fortawesome/free-solid-svg-icons';
-import { faCaretSquareRight } from '@fortawesome/free-solid-svg-icons';
-import { faSquare } from '@fortawesome/free-solid-svg-icons';
library.add(faTrash);
library.add(faExpandArrowsAlt);
@@ -44,308 +40,333 @@ library.add(faAlignCenter);
library.add(faCaretSquareRight);
library.add(faSquare);
+const linkSchema = createSchema({
+ title: "string",
+ linkDescription: "string",
+ linkTags: "string",
+ linkedTo: Doc,
+ linkedFrom: Doc
+});
+
+type LinkDoc = makeInterface<[typeof linkSchema]>;
+const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
- ContainingCollectionView: Opt<CollectionView>;
- Document: Document;
- AddDocument?: (doc: Document) => void;
- RemoveDocument?: (doc: Document) => boolean;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ Document: Doc;
+ addDocument?: (doc: Document, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (doc: Document) => boolean;
+ moveDocument?: (doc: Document, targetCollection: Document, addDocument: (document: Document) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
focus: (doc: Document) => void;
- SelectOnLoad: boolean;
-}
-export interface JsxArgs extends DocumentViewProps {
- Keys: { [name: string]: Key }
- Fields: { [name: string]: Field }
+ selectOnLoad: boolean;
+ parentActive: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ toggleMinimized: () => void;
+ bringToFront: (doc: Doc) => void;
+ addDocTab: (doc: Doc) => void;
}
-/*
-This function is pretty much a hack that lets us fill out the fields in JsxArgs with something that
-jsx-to-string can recover the jsx from
-Example usage of this function:
- public static LayoutString() {
- let args = FakeJsxArgs(["Data"]);
- return jsxToString(
- <CollectionFreeFormView
- doc={args.Document}
- fieldKey={args.Keys.Data}
- DocumentViewForField={args.DocumentView} />,
- { useFunctionCode: true, functionNameOnly: true }
- )
- }
-*/
-export function FakeJsxArgs(keys: string[], fields: string[] = []): JsxArgs {
- let Keys: { [name: string]: any } = {}
- let Fields: { [name: string]: any } = {}
- for (const key of keys) {
- let fn = () => { }
- Object.defineProperty(fn, "name", { value: key + "Key" })
- Keys[key] = fn;
- }
- for (const field of fields) {
- let fn = () => { }
- Object.defineProperty(fn, "name", { value: field })
- Fields[field] = fn;
- }
- let args: JsxArgs = {
- Document: function Document() { },
- DocumentView: function DocumentView() { },
- Keys,
- Fields
- } as any;
- return args;
-}
+const schema = createSchema({
+ layout: "string",
+ nativeWidth: "number",
+ nativeHeight: "number",
+ backgroundColor: "string"
+});
+
+export const positionSchema = createSchema({
+ nativeWidth: "number",
+ nativeHeight: "number",
+ width: "number",
+ height: "number",
+ x: "number",
+ y: "number",
+});
+
+export type PositionDocument = makeInterface<[typeof positionSchema]>;
+export const PositionDocument = makeInterface(positionSchema);
+
+type Document = makeInterface<[typeof schema]>;
+const Document = makeInterface(schema);
@observer
-export class DocumentView extends React.Component<DocumentViewProps> {
- private _mainCont = React.createRef<HTMLDivElement>();
- private _documentBindings: any = null;
+export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
- private _reactionDisposer: Opt<IReactionDisposer>;
- @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); }
- @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; }
- @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
- @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
- @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
- screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- if (e.shiftKey && e.buttons === 2) {
- if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY);
- }
- else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e);
- e.stopPropagation();
- } else {
- if (this.active && !e.isDefaultPrevented()) {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onPointerMove)
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp)
- document.addEventListener("pointerup", this.onPointerUp);
- }
- }
- }
-
- private dropDisposer?: DragManager.DragDropDisposer;
- protected createDropTarget = (ele: HTMLDivElement) => {
+ private _mainCont = React.createRef<HTMLDivElement>();
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ public get ContentDiv() { return this._mainCont.current; }
+ @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
+ @computed get topMost(): boolean { return this.props.isTopMost; }
+ @computed get templates(): List<string> {
+ let field = this.props.Document.templates;
+ if (field && field instanceof List) {
+ return field;
+ }
+ return new List<string>();
}
+ set templates(templates: List<string>) { this.props.Document.templates = templates; }
+ screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+ _reactionDisposer?: IReactionDisposer;
+ @action
componentDidMount() {
if (this._mainCont.current) {
- this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
}
- runInAction(() => {
- DocumentManager.Instance.DocumentViews.push(this);
- })
- this._reactionDisposer = reaction(
- () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(),
+ // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
+ this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
() => {
- if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.indexOf(this.props.Document.Id) != -1)
- SelectionManager.SelectDoc(this, true);
- });
+ let maxDoc = DocListCast(this.props.Document.maximizedDocs);
+ if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
+ this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon";
+ }
+ let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
+ if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) {
+ this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
+ }
+ }, { fireImmediately: true });
+ DocumentManager.Instance.DocumentViews.push(this);
}
-
+ @action
componentDidUpdate() {
- if (this.dropDisposer) {
- this.dropDisposer();
+ if (this._dropDisposer) {
+ this._dropDisposer();
}
if (this._mainCont.current) {
- this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+ this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
}
}
-
+ @action
componentWillUnmount() {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- runInAction(() => {
- DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
+ if (this._reactionDisposer) this._reactionDisposer();
+ if (this._dropDisposer) this._dropDisposer();
+ DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
+ }
- })
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
+ stopPropagation = (e: React.SyntheticEvent) => {
+ e.stopPropagation();
}
- startDragging(x: number, y: number) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
- const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- let dragData: { [id: string]: any } = {};
- dragData["documentView"] = this;
- dragData["xOffset"] = x - left;
- dragData["yOffset"] = y - top;
- DragManager.StartDrag(this._mainCont.current, dragData, {
+ let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+ const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
+ let dragData = new DragManager.DocumentDragData(allConnected);
+ const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
+ dragData.dropAction = dropAction;
+ dragData.xOffset = xoff;
+ dragData.yOffset = yoff;
+ dragData.moveDocument = this.props.moveDocument;
+ DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
- dragComplete: action(() => { }),
+ dragComplete: action(emptyFunction)
},
- hideSource: true
- })
+ hideSource: !dropAction
+ });
}
}
- onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) {
+ onClick = (e: React.MouseEvent): void => {
+ if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+ (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
+ Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
+ SelectionManager.SelectDoc(this, e.ctrlKey);
+ }
+ }
+ _hitExpander = false;
+ onPointerDown = (e: React.PointerEvent): void => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) {
return;
}
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- document.removeEventListener("pointermove", this.onPointerMove)
+ this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
+ if (e.shiftKey && e.buttons === 1) {
+ if (this.props.isTopMost) {
+ this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander);
+ } else if (this.props.Document) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
+ }
+ e.stopPropagation();
+ } else if (this.active) {
+ //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- if (!this.topMost || e.buttons == 2) {
- this.startDragging(e.x, e.y);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ }
+ onPointerMove = (e: PointerEvent): void => {
+ if (!e.cancelBubble) {
+ if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) {
+ this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander);
+ }
}
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
}
- e.stopPropagation();
- e.preventDefault();
}
onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onPointerMove)
- document.removeEventListener("pointerup", this.onPointerUp)
- e.stopPropagation();
- if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) {
- SelectionManager.SelectDoc(this, e.ctrlKey);
- }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
}
deleteClicked = (): void => {
- if (this.props.RemoveDocument) {
- this.props.RemoveDocument(this.props.Document);
- }
+ this.props.removeDocument && this.props.removeDocument(this.props.Document);
}
-
fieldsClicked = (e: React.MouseEvent): void => {
- if (this.props.AddDocument) {
- this.props.AddDocument(Documents.KVPDocument(this.props.Document));
+ let kvp = Docs.KVPDocument(this.props.Document, { title: this.props.Document.title + ".kvp", width: 300, height: 300 });
+ CollectionDockingView.Instance.AddRightSplit(kvp);
+ }
+ makeButton = (e: React.MouseEvent): void => {
+ let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ doc.isButton = !BoolCast(doc.isButton, false);
+ if (doc.isButton && !doc.nativeWidth) {
+ doc.nativeWidth = this.props.Document[WidthSym]();
+ doc.nativeHeight = this.props.Document[HeightSym]();
+ } else {
+
+ doc.nativeWidth = doc.nativeHeight = undefined;
}
}
fullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.OpenFullScreen(this.props.Document);
+ const doc = Doc.MakeCopy(this.props.Document, false);
+ if (doc) {
+ CollectionDockingView.Instance.OpenFullScreen(doc);
+ }
ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked, icon: "compress-arrows-alt" });
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ SelectionManager.DeselectAll();
}
- closeFullScreenClicked = (e: React.MouseEvent): void => {
- CollectionDockingView.Instance.CloseFullScreen();
- ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
+
+ if (de.mods === "AltKey") {
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ let src = protoSrc ? protoSrc : sourceDoc;
+ let dst = protoDest ? protoDest : destDoc;
+ dst.data = (src.data! as ObjectField)[Copy]();
+ dst.nativeWidth = src.nativeWidth;
+ dst.nativeHeight = src.nativeHeight;
+ }
+ else {
+ Doc.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ }
+ e.stopPropagation();
+ }
}
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
- console.log("drop");
- const sourceDocView: DocumentView = de.data["linkSourceDoc"];
- if (!sourceDocView) {
- return;
- }
- let sourceDoc: Document = sourceDocView.props.Document;
- let destDoc: Document = this.props.Document;
- if (this.props.isTopMost) {
- return;
+ onDrop = (e: React.DragEvent) => {
+ let text = e.dataTransfer.getData("text/plain");
+ if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
+ let oldLayout = FieldValue(this.Document.layout) || "";
+ let layout = text.replace("{layout}", oldLayout);
+ this.Document.layout = layout;
+ e.stopPropagation();
+ e.preventDefault();
}
- let linkDoc: Document = new Document();
-
- linkDoc.Set(KeyStore.Title, new TextField("New Link"));
- linkDoc.Set(KeyStore.LinkDescription, new TextField(""));
- linkDoc.Set(KeyStore.LinkTags, new TextField("Default"));
-
- sourceDoc.GetOrCreateAsync(KeyStore.LinkedToDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) });
- linkDoc.Set(KeyStore.LinkedToDocs, destDoc);
- destDoc.GetOrCreateAsync(KeyStore.LinkedFromDocs, ListField, field => { (field as ListField<Document>).Data.push(linkDoc) });
- linkDoc.Set(KeyStore.LinkedFromDocs, sourceDoc);
-
+ }
+ @action
+ addTemplate = (template: Template) => {
+ this.templates.push(template.Layout);
+ this.templates = this.templates;
+ }
- e.stopPropagation();
+ @action
+ removeTemplate = (template: Template) => {
+ for (let i = 0; i < this.templates.length; i++) {
+ if (this.templates[i] === template.Layout) {
+ this.templates.splice(i, 1);
+ break;
+ }
+ }
+ this.templates = this.templates;
}
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
- let moved = Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3;
- if (moved || e.isDefaultPrevented()) {
- e.preventDefault()
+ if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
+ e.isDefaultPrevented()) {
+ e.preventDefault();
return;
}
- e.preventDefault()
+ e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" })
- ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" })
- ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" })
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" })
- ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking), icon: "square" })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
+ const cm = ContextMenu.Instance;
+ cm.addItem({ description: "Full Screen", event: this.fullScreenClicked, icon: "expand-arrows-alt" });
+ cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton, icon: "expand-arrows-alt" });
+ cm.addItem({ description: "Fields", event: this.fieldsClicked, icon: "layer-group" });
+ cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document), icon: "align-center" });
+ cm.addItem({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document), icon: "expand-arrows-alt" });
+ cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document), icon: "caret-square-right" });
+ cm.addItem({
+ description: "Find aliases", event: async () => {
+ const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
+ CollectionDockingView.Instance.AddRightSplit(Docs.SchemaDocument(["title"], aliases, {}));
+ }, icon: "expand-arrows-alt"
+ });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "expand-arrows-alt" });
+ cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "expand-arrows-alt" });
+ cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "expand-arrows-alt" });
+ cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
}
-
- ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" })
- ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
- SelectionManager.SelectDoc(this, e.ctrlKey);
- }
-
- get mainContent() {
- return <JsxParser
- components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, WebBox, KeyValueBox, VideoBox, AudioBox, PDFBox }}
- bindings={this._documentBindings}
- jsx={this.layout}
- showWarnings={true}
- onError={(test: any) => { console.log(test) }}
- />
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
+ if (!SelectionManager.IsSelected(this)) {
+ SelectionManager.SelectDoc(this, false);
+ }
}
- isSelected = () => {
- return SelectionManager.IsSelected(this);
- }
+ isSelected = () => SelectionManager.IsSelected(this);
+ select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
- select = (ctrlPressed: boolean) => {
- SelectionManager.SelectDoc(this, ctrlPressed)
- }
+ @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+ @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+ @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); }
render() {
- if (!this.props.Document) return <div></div>
- let lkeys = this.props.Document.GetT(KeyStore.LayoutKeys, ListField);
- if (!lkeys || lkeys === "<Waiting>") {
- return <p>Error loading layout keys</p>;
- }
- this._documentBindings = {
- ...this.props,
- isSelected: this.isSelected,
- select: this.select,
- focus: this.props.focus
- };
- for (const key of this.layoutKeys) {
- this._documentBindings[key.Name + "Key"] = key; // this maps string values of the form <keyname>Key to an actual key Kestore.keyname e.g, "DataKey" => KeyStore.Data
- }
- for (const key of this.layoutFields) {
- let field = this.props.Document.Get(key);
- this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
- }
- this._documentBindings.bindings = this._documentBindings;
var scaling = this.props.ContentScaling();
- var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
- var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+ var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto");
+ var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
+
return (
- <div className="documentView-node" ref={this._mainCont}
+ <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
+ ref={this._mainCont}
style={{
- width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%",
- height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%",
- transformOrigin: "left top",
- transform: `scale(${scaling} , ${scaling})`
+ borderRadius: "inherit",
+ background: this.Document.backgroundColor || "",
+ width: nativeWidth,
+ height: nativeHeight,
+ transform: `scale(${scaling}, ${scaling})`
}}
- onContextMenu={this.onContextMenu}
- onPointerDown={this.onPointerDown} >
- {this.mainContent}
+ onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+ >
+ {this.contents}
</div>
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldTextBox.scss b/src/client/views/nodes/FieldTextBox.scss
index b6ce2fabc..d2cd61b0d 100644
--- a/src/client/views/nodes/FieldTextBox.scss
+++ b/src/client/views/nodes/FieldTextBox.scss
@@ -1,14 +1,14 @@
.ProseMirror {
- margin-top: -1em;
- width: 100%;
- height: 100%;
+ margin-top: -1em;
+ width: 100%;
+ height: 100%;
}
.ProseMirror:focus {
- outline: none !important
+ outline: none !important;
}
.fieldTextBox-cont {
- background: white;
- padding: 1vw;
-} \ No newline at end of file
+ background: white;
+ padding: 1vw;
+}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 49f4cefce..b5dfd18eb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,21 +1,24 @@
-import React = require("react")
+import React = require("react");
import { observer } from "mobx-react";
-import { computed } from "mobx";
-import { Field, FieldWaiting, FieldValue } from "../../../fields/Field";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
-import { NumberField } from "../../../fields/NumberField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { ImageField } from "../../../fields/ImageField";
-import { WebField } from "../../../fields/WebField";
-import { VideoField } from "../../../fields/VideoField"
-import { Key } from "../../../fields/Key";
+import { computed, observable } from "mobx";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
-import { WebBox } from "./WebBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
-import { AudioField } from "../../../fields/AudioField";
+import { DocumentContentsView } from "./DocumentContentsView";
+import { Transform } from "../../util/Transform";
+import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
+import { CollectionView } from "../collections/CollectionView";
+import { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
+import { IconBox } from "./IconBox";
+import { Opt, Doc, FieldResult } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField";
+import { IconField } from "../../../new_fields/IconField";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { DateField } from "../../../new_fields/DateField";
+import { NumCast } from "../../../new_fields/Types";
//
@@ -24,61 +27,101 @@ import { AudioField } from "../../../fields/AudioField";
// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
//
export interface FieldViewProps {
- fieldKey: Key;
- doc: Document;
+ fieldKey: string;
+ ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ Document: Doc;
isSelected: () => boolean;
- select: () => void;
+ select: (isCtrlPressed: boolean) => void;
isTopMost: boolean;
selectOnLoad: boolean;
- bindings: any;
+ addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocTab: (document: Doc) => boolean;
+ removeDocument?: (document: Doc) => boolean;
+ moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ ScreenToLocalTransform: () => Transform;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ focus: (doc: Doc) => void;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ setVideoBox?: (player: VideoBox) => void;
}
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "DataKey") {
- return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={${fieldStr}} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost} />`;
+ public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
}
@computed
- get field(): FieldValue<Field> {
- const { doc, fieldKey } = this.props;
- return doc.Get(fieldKey);
+ get field(): FieldResult {
+ const { Document, fieldKey } = this.props;
+ return Document[fieldKey];
}
render() {
const field = this.field;
- if (!field) {
- return <p>{'<null>'}</p>
- }
- if (field instanceof TextField) {
- return <p>{field.Data}</p>
+ if (field === undefined) {
+ return <p>{'<null>'}</p>;
}
+ // if (typeof field === "string") {
+ // return <p>{field}</p>;
+ // }
else if (field instanceof RichTextField) {
- return <FormattedTextBox {...this.props} />
+ return <FormattedTextBox {...this.props} />;
}
else if (field instanceof ImageField) {
- return <ImageBox {...this.props} />
+ return <ImageBox {...this.props} />;
+ }
+ else if (field instanceof IconField) {
+ return <IconBox {...this.props} />;
+ }
+ else if (field instanceof VideoField) {
+ return <VideoBox {...this.props} />;
+ }
+ else if (field instanceof AudioField) {
+ return <AudioBox {...this.props} />;
+ } else if (field instanceof DateField) {
+ return <p>{field.date.toLocaleString()}</p>;
}
- else if (field instanceof WebField) {
- return <WebBox {...this.props} />
- }
- else if (field instanceof VideoField){
- return <VideoBox {...this.props}/>
+ else if (field instanceof Doc) {
+ let returnHundred = () => 100;
+ return (
+ <DocumentContentsView Document={field}
+ addDocument={undefined}
+ addDocTab={this.props.addDocTab}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={returnHundred}
+ PanelHeight={returnHundred}
+ isTopMost={true} //TODO Why is this top most?
+ selectOnLoad={false}
+ focus={emptyFunction}
+ isSelected={this.props.isSelected}
+ select={returnFalse}
+ layoutKey={"layout"}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ parentActive={this.props.active}
+ toggleMinimized={emptyFunction}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction} />
+ );
}
- else if (field instanceof AudioField){
- return <AudioBox {...this.props}/>
+ else if (field instanceof List) {
+ return (<div>
+ {field.map(f => f instanceof Doc ? f.title : f.toString()).join(", ")}
+ </div>);
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
// else if (field instanceof HtmlField) {
// return <WebBox {...this.props} />
// }
- else if (field instanceof NumberField) {
- return <p>{field.Data}</p>
+ else if (!(field instanceof Promise)) {
+ return <p>{JSON.stringify(field)}</p>;
}
- else if (field != FieldWaiting) {
- return <p>{JSON.stringify(field.GetValue())}</p>
+ else {
+ return <p> {"Waiting for server..."} </p>;
}
- else
- return <p> {"Waiting for server..."} </p>
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index ab5849f09..4a29c1949 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -1,38 +1,60 @@
+@import "../globalCssVariables";
.ProseMirror {
- width: 100%;
- height: auto;
- min-height: 100%
+ width: 100%;
+ height: auto;
+ min-height: 100%;
+ font-family: $serif;
}
.ProseMirror:focus {
- outline: none !important
+ outline: none !important;
}
-.formattedTextBox-cont {
- background: white;
- padding: 1;
- border-width: 1px;
- border-radius: 2px;
- border-color:black;
- box-sizing: border-box;
- background: white;
- border-style:solid;
- overflow-y: scroll;
- overflow-x: hidden;
- color: initial;
- height: 100%;
+.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $intermediate-color;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: initial;
+ height: 100%;
+ pointer-events: all;
+}
+
+.formattedTextBox-cont-hidden {
+ pointer-events: none;
+}
+.formattedTextBox-inner-rounded {
+ height: calc(100% - 25px);
+ width: calc(100% - 40px);
+ position: absolute;
+ overflow: auto;
+ top: 15;
+ left: 20;
}
.menuicon {
- display: inline-block;
- border-right: 1px solid rgba(0, 0, 0, 0.2);
- color: #888;
- line-height: 1;
- padding: 0 7px;
- margin: 1px;
- cursor: pointer;
- text-align: center;
- min-width: 1.4em;
- }
- .strong, .heading { font-weight: bold; }
- .em { font-style: italic; } \ No newline at end of file
+ display: inline-block;
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+ color: #888;
+ line-height: 1;
+ padding: 0 7px;
+ margin: 1px;
+ cursor: pointer;
+ text-align: center;
+ min-width: 1.4em;
+}
+
+.strong,
+.heading {
+ font-weight: bold;
+}
+
+.em {
+ font-style: italic;
+}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 648037e31..98abde89e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,30 +1,41 @@
-import { action, IReactionDisposer, reaction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
+import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { schema } from "../../util/RichTextSchema";
-import { EditorState, Transaction, } from "prosemirror-state";
+import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
-import { Opt, FieldWaiting } from "../../../fields/Field";
-import "./FormattedTextBox.scss";
-import React = require("react")
-import { RichTextField } from "../../../fields/RichTextField";
-import { FieldViewProps, FieldView } from "./FieldView";
-import { Plugin } from 'prosemirror-state'
-import { Decoration, DecorationSet } from 'prosemirror-view'
-import { TooltipTextMenu } from "../../util/TooltipTextMenu"
+import { Doc, Field, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { DocServer } from "../../DocServer";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragManager } from "../../util/DragManager";
+import buildKeymap from "../../util/ProsemirrorKeymap";
+import { inpRules } from "../../util/RichTextRules";
+import { ImageResizeView, schema } from "../../util/RichTextSchema";
+import { SelectionManager } from "../../util/SelectionManager";
+import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
+import { TooltipTextMenu } from "../../util/TooltipTextMenu";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { ContextMenu } from "../../views/ContextMenu";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit } from '@fortawesome/free-solid-svg-icons';
-import { faSmile } from '@fortawesome/free-solid-svg-icons';
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { FieldView, FieldViewProps } from "./FieldView";
+import "./FormattedTextBox.scss";
+import React = require("react");
library.add(faEdit);
library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
-// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name + "Key"}
-//
+// HTML Markup: <FormattedTextBox Doc={Document's ID} FieldKey={Key's name}
+//
// In Code, the node's HTML is specified in the document's parameterized structure as:
// document.SetField(KeyStore.Layout, "<FormattedTextBox doc={doc} fieldKey={<KEYNAME>Key} />");
// and the node's binding to the specified document KEYNAME as:
@@ -33,67 +44,161 @@ library.add(faSmile);
// 'fieldKey' property to the Key stored in LayoutKeys
// and 'doc' property to the document that is being rendered
//
-// When rendered() by React, this extracts the TextController from the Document stored at the
-// specified Key and assigns it to an HTML input node. When changes are made tot his node,
+// When rendered() by React, this extracts the TextController from the Document stored at the
+// specified Key and assigns it to an HTML input node. When changes are made to this node,
// this will edit the document and assign the new value to that field.
//]
-export class FormattedTextBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr) }
+export interface FormattedTextBoxOverlay {
+ isOverlay?: boolean;
+}
+
+const richTextSchema = createSchema({
+ documentText: "string"
+});
+
+type RichTextDocument = makeInterface<[typeof richTextSchema]>;
+const RichTextDocument = makeInterface(richTextSchema);
+
+@observer
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string = "data") {
+ return FieldView.LayoutString(FormattedTextBox, fieldStr);
+ }
private _ref: React.RefObject<HTMLDivElement>;
+ private _proseRef: React.RefObject<HTMLDivElement>;
private _editorView: Opt<EditorView>;
+ private _gotDown: boolean = false;
+ private _dropDisposer?: DragManager.DragDropDisposer;
private _reactionDisposer: Opt<IReactionDisposer>;
+ private _inputReactionDisposer: Opt<IReactionDisposer>;
+ private _proxyReactionDisposer: Opt<IReactionDisposer>;
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
+ @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
+ public static InputBoxOverlayScroll: number = 0;
constructor(props: FieldViewProps) {
super(props);
this._ref = React.createRef();
- this.onChange = this.onChange.bind(this);
+ this._proseRef = React.createRef();
+ if (this.props.isOverlay) {
+ DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
+ }
}
+ _applyingChange: boolean = false;
+
+ _lastState: any = undefined;
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
- const state = this._editorView.state.apply(tx);
+ const state = this._lastState = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- this.props.doc.SetData(this.props.fieldKey, JSON.stringify(state.toJSON()), RichTextField);
+ this._applyingChange = true;
+ Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
+ Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ this._applyingChange = false;
+ let title = StrCast(this.props.Document.title);
+ if (title && title.startsWith("-") && this._editorView) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ }
+ }
+ }
+
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.LinkDragData) {
+ let sourceDoc = de.data.linkSourceDocument;
+ let destDoc = this.props.Document;
+
+ Doc.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ e.stopPropagation();
}
}
componentDidMount() {
- let state: EditorState;
+ if (this._ref.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
+ }
const config = {
schema,
- plugins: [
+ inpRules, //these currently don't do anything, but could eventually be helpful
+ plugins: this.props.isOverlay ? [
+ this.tooltipTextMenuPlugin(),
history(),
- keymap({ "Mod-z": undo, "Mod-y": redo }),
+ keymap(buildKeymap(schema)),
keymap(baseKeymap),
- this.tooltipMenuPlugin()
- ]
+ // this.tooltipLinkingMenuPlugin(),
+ new Plugin({
+ props: {
+ attributes: { class: "ProseMirror-example-setup-style" }
+ }
+ })
+ ] : [
+ history(),
+ keymap(buildKeymap(schema)),
+ keymap(baseKeymap),
+ ]
};
- let field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
- if (field && field != FieldWaiting && field.Data) {
- state = EditorState.fromJSON(config, JSON.parse(field.Data));
+ if (this.props.isOverlay) {
+ this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
+ () => {
+ if (this._editorView) {
+ this._editorView.destroy();
+ }
+ this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox
+ }
+ );
} else {
- state = EditorState.create(config);
+ this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
+ () => {
+ if (this.props.isSelected()) {
+ FormattedTextBox.InputBoxOverlay = this;
+ FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
+ }
+ });
}
- if (this._ref.current) {
- this._editorView = new EditorView(this._ref.current, {
- state,
- dispatchTransaction: this.dispatchTransaction
+
+
+ this._reactionDisposer = reaction(
+ () => {
+ const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
+ return field ? field.Data : undefined;
+ },
+ field => field && this._editorView && !this._applyingChange &&
+ this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
+ );
+ this.setupEditor(config, this.props.Document);
+ }
+
+ private setupEditor(config: any, doc?: Doc) {
+ let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined;
+ if (this._proseRef.current) {
+ this._editorView = new EditorView(this._proseRef.current, {
+ state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
+ dispatchTransaction: this.dispatchTransaction,
+ nodeViews: {
+ image(node, view, getPos) { return new ImageResizeView(node, view, getPos); }
+ }
});
+ let text = StrCast(this.props.Document.documentText);
+ if (text.startsWith("@@@")) {
+ this.props.Document.proto!.documentText = undefined;
+ this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", "")));
+ }
}
- this._reactionDisposer = reaction(() => {
- const field = this.props.doc.GetT(this.props.fieldKey, RichTextField);
- return field && field != FieldWaiting ? field.Data : undefined;
- }, (field) => {
- if (field && this._editorView) {
- this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)));
- }
- })
if (this.props.selectOnLoad) {
- this.props.select();
+ this.props.select(false);
this._editorView!.focus();
}
}
@@ -105,60 +210,175 @@ export class FormattedTextBox extends React.Component<FieldViewProps> {
if (this._reactionDisposer) {
this._reactionDisposer();
}
+ if (this._inputReactionDisposer) {
+ this._inputReactionDisposer();
+ }
+ if (this._proxyReactionDisposer) {
+ this._proxyReactionDisposer();
+ }
+ if (this._dropDisposer) {
+ this._dropDisposer();
+ }
}
- shouldComponentUpdate() {
- return false;
- }
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ e.stopPropagation();
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
+ this._toolTipTextMenu.tooltip.style.opacity = "0";
+ }
+ }
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) {
+ if (e.target && (e.target as any).href) {
+ let href = (e.target as any).href;
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+ if (f instanceof Doc) {
+ if (DocumentManager.Instance.getDocumentView(f)) {
+ DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+ } else {
+ CollectionDockingView.Instance.AddRightSplit(f);
+ }
+ }
+ }));
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
- @action
- onChange(e: React.ChangeEvent<HTMLInputElement>) {
- this.props.doc.SetData(this.props.fieldKey, e.target.value, RichTextField);
+ }
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
+ this._gotDown = true;
+ e.preventDefault();
+ }
}
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && this.props.isSelected()) {
+ onPointerUp = (e: React.PointerEvent): void => {
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
+ this._toolTipTextMenu.tooltip.style.opacity = "1";
+ }
+ if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => {
+ @action
+ onFocused = (e: React.FocusEvent): void => {
+ if (!this.props.isOverlay) {
+ FormattedTextBox.InputBoxOverlay = this;
+ } else {
+ if (this._ref.current) {
+ this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
+ }
+ }
}
- @action
+ //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
+ freezeNativeDimensions = (e: React.MouseEvent): void => {
+ if (NumCast(this.props.Document.nativeWidth)) {
+ this.props.Document.proto!.nativeWidth = undefined;
+ this.props.Document.proto!.nativeHeight = undefined;
+
+ } else {
+ this.props.Document.proto!.nativeWidth = this.props.Document[WidthSym]();
+ this.props.Document.proto!.nativeHeight = this.props.Document[HeightSym]();
+ }
+ }
specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Text Capability", event: this.textCapability, icon: "edit" });
+ if (!this._gotDown) {
+ e.preventDefault();
+ return;
+ }
ContextMenu.Instance.addItem({
- description: "Sub", subitems: [
- { description: "Subitem 1", event: this.textCapability, icon: "smile" },
- { description: "Subitem 2", event: this.textCapability, icon: "smile" },
- { description: "Subitem 3", event: this.textCapability, icon: "smile" },
- { description: "Submenu", subitems: [{ description: "Inner Subitem", event: this.textCapability, icon: "smile" }] }
- ]
+ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
+ event: this.freezeNativeDimensions,
+ icon: "edit"
});
}
onPointerWheel = (e: React.WheelEvent): void => {
- e.stopPropagation();
+ if (this.props.isSelected()) {
+ e.stopPropagation();
+ }
}
- tooltipMenuPlugin() {
+ onClick = (e: React.MouseEvent): void => {
+ this._proseRef.current!.focus();
+ }
+ onMouseDown = (e: React.MouseEvent): void => {
+ if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
+ e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ }
+ }
+
+ tooltipTextMenuPlugin() {
+ let myprops = this.props;
+ let self = this;
return new Plugin({
view(_editorView) {
- return new TooltipTextMenu(_editorView)
+ return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
- })
+ });
}
- onKeyPress(e: React.KeyboardEvent) {
+ _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ tooltipLinkingMenuPlugin() {
+ let myprops = this.props;
+ return new Plugin({
+ view(_editorView) {
+ return new TooltipLinkingMenu(_editorView, myprops);
+ }
+ });
+ }
+ onBlur = (e: any) => {
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ }
+ public _undoTyping?: UndoManager.Batch;
+ onKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Escape") {
+ SelectionManager.DeselectAll();
+ }
e.stopPropagation();
+ if (e.key === "Tab") e.preventDefault();
+ // stop propagation doesn't seem to stop propagation of native keyboard events.
+ // so we set a flag on the native event that marks that the event's been handled.
+ (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
+ if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+ let str = this._editorView.state.doc.textContent;
+ let titlestr = str.substr(0, Math.min(40, str.length));
+ let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+ target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ }
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
}
render() {
- return (<div className="formattedTextBox-cont"
- onKeyPress={this.onKeyPress}
- onPointerDown={this.onPointerDown}
- onContextMenu={this.specificContextMenu}
- onWheel={this.onPointerWheel}
- ref={this._ref} />)
- }
-} \ No newline at end of file
+ let style = this.props.isOverlay ? "scroll" : "hidden";
+ let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
+ let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
+ return (
+ <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
+ style={{
+ pointerEvents: interactive ? "all" : "none",
+ }}
+ onKeyDown={this.onKeyPress}
+ onKeyPress={this.onKeyPress}
+ onFocus={this.onFocused}
+ onClick={this.onClick}
+ onBlur={this.onBlur}
+ onPointerUp={this.onPointerUp}
+ onPointerDown={this.onPointerDown}
+ onMouseDown={this.onMouseDown}
+ onContextMenu={this.specificContextMenu}
+ // tfs: do we need this event handler
+ onWheel={this.onPointerWheel}
+ >
+ <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} />
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss
new file mode 100644
index 000000000..893dc2d36
--- /dev/null
+++ b/src/client/views/nodes/IconBox.scss
@@ -0,0 +1,22 @@
+
+@import "../globalCssVariables";
+.iconBox-container {
+ position: inherit;
+ left:0;
+ top:0;
+ height: auto;
+ width: max-content;
+ // overflow: hidden;
+ pointer-events: all;
+ svg {
+ width: $MINIMIZED_ICON_SIZE !important;
+ height: auto;
+ background: white;
+ }
+ .iconBox-label {
+ position: absolute;
+ width:max-content;
+ font-size: 14px;
+ margin-top: 3px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
new file mode 100644
index 000000000..b42eb44a5
--- /dev/null
+++ b/src/client/views/nodes/IconBox.tsx
@@ -0,0 +1,84 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./IconBox.scss";
+import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { IconField } from "../../../new_fields/IconField";
+import { ContextMenu } from "../ContextMenu";
+import Measure from "react-measure";
+import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
+import { listSpec } from "../../../new_fields/Schema";
+
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+@observer
+export class IconBox extends React.Component<FieldViewProps> {
+ public static LayoutString() { return FieldView.LayoutString(IconBox); }
+
+ @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
+ @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
+
+ public static DocumentIcon(layout: string) {
+ let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+ layout.indexOf("ImageBox") !== -1 ? faImage :
+ layout.indexOf("Formatted") !== -1 ? faStickyNote :
+ layout.indexOf("Video") !== -1 ? faFilm :
+ layout.indexOf("Collection") !== -1 ? faObjectGroup :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
+ }
+
+ setLabelField = (e: React.MouseEvent): void => {
+ this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
+ }
+ setUseOwnTitleField = (e: React.MouseEvent): void => {
+ this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
+ event: this.setLabelField
+ });
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
+ event: this.setUseOwnTitleField
+ });
+ }
+ }
+ @observable _panelWidth: number = 0;
+ @observable _panelHeight: number = 0;
+ render() {
+ let labelField = StrCast(this.props.Document.labelField);
+ let hideLabel = BoolCast(this.props.Document.hideLabel);
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
+ let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title);
+ return (
+ <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
+ {this.minimizedIcon}
+ <Measure offset onResize={(r) => runInAction(() => {
+ if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) {
+ this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
+ if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth;
+ }
+ })}>
+ {({ measureRef }) =>
+ <span ref={measureRef} className="iconBox-label">{label}</span>
+ }
+ </Measure>
+ </div>);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index ea459b911..f1b73a676 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,22 +1,41 @@
-
.imageBox-cont {
- padding: 0vw;
- position: relative;
- text-align: center;
- width: 100%;
- height: auto;
- max-width: 100%;
- max-height: 100%
+ padding: 0vw;
+ position: relative;
+ text-align: center;
+ width: 100%;
+ height: auto;
+ max-width: 100%;
+ max-height: 100%;
+ pointer-events: none;
+}
+.imageBox-cont-interactive {
+ pointer-events: all;
+ width:100%;
+ height:auto;
+}
+
+.imageBox-dot {
+ position:absolute;
+ bottom: 10;
+ left: 0;
+ border-radius: 10px;
+ width:20px;
+ height:20px;
+ background:gray;
}
.imageBox-cont img {
- object-fit: contain;
- height: 100%;
+ height: auto;
+ width:100%;
+}
+.imageBox-cont-interactive img {
+ height: auto;
+ width:100%;
}
.imageBox-button {
- padding : 0vw;
- border: none;
- width : 100%;
- height: 100%;
+ padding: 0vw;
+ border: none;
+ width: 100%;
+ height: 100%;
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7532e23c8..828ac9bc8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -2,54 +2,93 @@ import { action, observable } from 'mobx';
import { observer } from "mobx-react";
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { FieldWaiting } from '../../../fields/Field';
-import { ImageField } from '../../../fields/ImageField';
-import { KeyStore } from '../../../fields/KeyStore';
+import { Utils } from '../../../Utils';
+import { DragManager } from '../../util/DragManager';
+import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
-import React = require("react")
+import React = require("react");
+import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
+import { DocComponent } from '../DocComponent';
+import { positionSchema } from './DocumentView';
+import { FieldValue, Cast, StrCast } from '../../../new_fields/Types';
+import { ImageField } from '../../../new_fields/URLField';
+import { List } from '../../../new_fields/List';
+import { InkingControl } from '../InkingControl';
+import { Doc } from '../../../new_fields/Doc';
+
+export const pageSchema = createSchema({
+ curPage: "number"
+});
+
+type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
+const ImageDocument = makeInterface(pageSchema, positionSchema);
@observer
-export class ImageBox extends React.Component<FieldViewProps> {
+export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
- public static LayoutString() { return FieldView.LayoutString(ImageBox) }
- private _ref: React.RefObject<HTMLDivElement>;
- private _imgRef: React.RefObject<HTMLImageElement>;
+ public static LayoutString() { return FieldView.LayoutString(ImageBox); }
+ private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _downX: number = 0;
private _downY: number = 0;
private _lastTap: number = 0;
@observable private _photoIndex: number = 0;
@observable private _isOpen: boolean = false;
-
- constructor(props: FieldViewProps) {
- super(props);
-
- this._ref = React.createRef();
- this._imgRef = React.createRef();
- this.state = {
- photoIndex: 0,
- isOpen: false,
- };
- }
+ private dropDisposer?: DragManager.DragDropDisposer;
@action
onLoad = (target: any) => {
var h = this._imgRef.current!.naturalHeight;
var w = this._imgRef.current!.naturalWidth;
- this.props.doc.SetNumber(KeyStore.NativeHeight, this.props.doc.GetNumber(KeyStore.NativeWidth, 0) * h / w)
+ if (this._photoIndex === 0 && (this.props as any).id !== "isExpander") {
+ Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w);
+ this.Document.height = FieldValue(this.Document.width, 0) * h / w;
+ }
}
- componentDidMount() {
+
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ if (this.dropDisposer) {
+ this.dropDisposer();
+ }
+ if (ele) {
+ this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+ }
}
+ onDrop = (e: React.DragEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ console.log("IMPLEMENT ME PLEASE");
+ }
+
- componentWillUnmount() {
+ @undoBatch
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ de.data.droppedDocuments.forEach(action((drop: Doc) => {
+ let layout = StrCast(drop.backgroundLayout);
+ if (layout.indexOf(ImageBox.name) !== -1) {
+ let imgData = this.props.Document[this.props.fieldKey];
+ if (imgData instanceof ImageField) {
+ Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
+ }
+ let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]);
+ if (imgList) {
+ let field = drop.data;
+ if (field instanceof ImageField) imgList.push(field);
+ else if (field instanceof List) imgList.concat(field);
+ }
+ e.stopPropagation();
+ }
+ }));
+ // de.data.removeDocument() bcz: need to implement
+ }
}
onPointerDown = (e: React.PointerEvent): void => {
if (Date.now() - this._lastTap < 300) {
- if (e.buttons === 1 && this.props.isSelected()) {
- e.stopPropagation();
+ if (e.buttons === 1) {
this._downX = e.clientX;
this._downY = e.clientY;
document.removeEventListener("pointerup", this.onPointerUp);
@@ -68,9 +107,8 @@ export class ImageBox extends React.Component<FieldViewProps> {
e.stopPropagation();
}
- lightbox = (path: string) => {
- const images = [path, "http://www.cs.brown.edu/~bcz/face.gif"];
- if (this._isOpen && this.props.isSelected()) {
+ lightbox = (images: string[]) => {
+ if (this._isOpen) {
return (<Lightbox
mainSrc={images[this._photoIndex]}
nextSrc={images[(this._photoIndex + 1) % images.length]}
@@ -84,27 +122,58 @@ export class ImageBox extends React.Component<FieldViewProps> {
onMoveNextRequest={action(() =>
this._photoIndex = (this._photoIndex + 1) % images.length
)}
- />)
+ />);
}
}
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- imageCapability = (e: React.MouseEvent): void => {
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let field = Cast(this.Document[this.props.fieldKey], ImageField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => {
+ Utils.CopyText(url);
+ }, icon: "expand-arrows-alt"
+ });
+ }
}
- specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Image Capability", event: this.imageCapability, icon: "smile" });
+ @action
+ onDotDown(index: number) {
+ this._photoIndex = index;
+ this.Document.curPage = index;
+ }
+
+ dots(paths: string[]) {
+ let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
+ let dist = Math.min(nativeWidth / paths.length, 40);
+ let left = (nativeWidth - paths.length * dist) / 2;
+ return paths.map((p, i) =>
+ <div className="imageBox-placer" key={i} >
+ <div className="imageBox-dot" style={{ background: (i === this._photoIndex ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
+ </div>
+ );
}
render() {
- let field = this.props.doc.Get(this.props.fieldKey);
- let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- field instanceof ImageField ? field.Data.href : "http://www.cs.brown.edu/~bcz/face.gif";
- let nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 1);
+ let field = this.Document[this.props.fieldKey];
+ let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"];
+ if (field instanceof ImageField) paths = [field.url.href];
+ else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href);
+ let nativeWidth = FieldValue(this.Document.nativeWidth, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50);
+ let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
+ let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
return (
- <div className="imageBox-cont" onPointerDown={this.onPointerDown} ref={this._ref} onContextMenu={this.specificContextMenu}>
- <img src={path} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} />
- {this.lightbox(path)}
- </div>)
+ <div id={id} className={`imageBox-cont${interactive}`}
+ // onPointerDown={this.onPointerDown}
+ onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <img id={id} src={paths[Math.min(paths.length, this._photoIndex)]}
+ style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
+ width={nativeWidth}
+ ref={this._imgRef}
+ onLoad={this.onLoad} />
+ {paths.length > 1 ? this.dots(paths) : (null)}
+ {this.lightbox(paths)}
+ </div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 1295266e5..20cae03d4 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -1,31 +1,123 @@
+@import "../globalCssVariables";
.keyValueBox-cont {
- overflow-y:scroll;
+ overflow-y: scroll;
+ width:100%;
height: 100%;
- border: black;
- border-width: 1px;
- border-style: solid;
+ background-color: $light-color;
+ border: 1px solid $intermediate-color;
+ border-radius: $border-radius;
box-sizing: border-box;
display: inline-block;
+ pointer-events: all;
.imageBox-cont img {
- max-height:45px;
- height: auto;
+ width: auto;
}
}
+$header-height: 30px;
+.keyValueBox-tbody {
+ width:100%;
+ height:100%;
+ position: absolute;
+ overflow-y: scroll;
+}
+.keyValueBox-key {
+ display: inline-block;
+ height:100%;
+ width:50%;
+ text-align: center;
+}
+.keyValueBox-fields {
+ display: inline-block;
+ height:100%;
+ width:50%;
+ text-align: center;
+}
+
.keyValueBox-table {
- position: relative;
+ position: absolute;
+ width:100%;
+ height:100%;
+ border-collapse: collapse;
+}
+.keyValueBox-td-key {
+ display:inline-block;
+ height:30px;
+}
+.keyValueBox-td-value {
+ display:inline-block;
+ height:30px;
+}
+.keyValueBox-valueRow {
+ width:100%;
+ height:30px;
+ display: inline-block;
}
.keyValueBox-header {
- background:gray;
+ width:100%;
+ position: relative;
+ display: inline-block;
+ background: $intermediate-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 12px;
+ height: $header-height;
+ padding-top: 4px;
+ th {
+ font-weight: normal;
+ &:first-child {
+ border-right: 1px solid $light-color;
+ }
+ }
}
+
.keyValueBox-evenRow {
- background: white;
+ position: relative;
+ display: inline-block;
+ width:100%;
+ height:$header-height;
+ background: $light-color;
.formattedTextBox-cont {
- background: white;
+ background: $light-color;
+ }
+}
+.keyValueBox-cont {
+ .collectionfreeformview-overlay {
+ position: relative;
}
}
+.keyValueBox-dividerDraggerThumb{
+ position: relative;
+ width: 4px;
+ float: left;
+ height: 30px;
+ width: 10px;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ border-radius: 10px;
+ background: gray;
+ pointer-events: all;
+}
+.keyValueBox-dividerDragger{
+ position: relative;
+ width: 100%;
+ float: left;
+ height: 37px;
+ z-index: 20;
+ right: 0;
+ top: 0;
+ background: transparent;
+ pointer-events: none;
+}
+
.keyValueBox-oddRow {
- background: lightGray;
+ position: relative;
+ display: inline-block;
+ width:100%;
+ height:30px;
+ background: $light-color-secondary;
.formattedTextBox-cont {
- background: lightgray;
+ background: $light-color-secondary;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index ac8c949a9..86437a6c1 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,18 +1,54 @@
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { Document } from '../../../fields/Document';
-import { FieldWaiting } from '../../../fields/Field';
-import { KeyStore } from '../../../fields/KeyStore';
+import { CompileScript } from "../../util/Scripting";
import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
-import React = require("react")
+import React = require("react");
+import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
+import { Doc, IsField } from "../../../new_fields/Doc";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
+ private _mainCont = React.createRef<HTMLDivElement>();
- public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(KeyValueBox, fieldStr) }
+ public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ @observable private _keyInput: string = "";
+ @observable private _valueInput: string = "";
+ @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
+ get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; }
+
+ constructor(props: FieldViewProps) {
+ super(props);
+ }
+
+ @action
+ onEnterKey = (e: React.KeyboardEvent): void => {
+ if (e.key === 'Enter') {
+ if (this._keyInput && this._valueInput) {
+ let doc = this.fieldDocToLayout;
+ if (!doc) {
+ return;
+ }
+ let realDoc = doc;
+
+ let script = CompileScript(this._valueInput, { addReturn: true });
+ if (!script.compiled) {
+ return;
+ }
+ let res = script.run();
+ if (!res.success) return;
+ const field = res.result;
+ if (IsField(field)) {
+ realDoc[this._keyInput] = field;
+ }
+ this._keyInput = "";
+ this._valueInput = "";
+ }
+ }
+ }
onPointerDown = (e: React.PointerEvent): void => {
if (e.buttons === 1 && this.props.isSelected()) {
@@ -24,43 +60,87 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
createTable = () => {
- let doc = this.props.doc.GetT(KeyStore.Data, Document);
- if (!doc || doc == FieldWaiting) {
- return <tr><td>Loading...</td></tr>
+ let doc = this.fieldDocToLayout;
+ if (!doc) {
+ return <tr><td>Loading...</td></tr>;
}
let realDoc = doc;
let ids: { [key: string]: string } = {};
- let protos = doc.GetAllPrototypes();
+ let protos = Doc.GetAllPrototypes(doc);
for (const proto of protos) {
- proto._proxies.forEach((val, key) => {
+ Object.keys(proto).forEach(key => {
if (!(key in ids)) {
ids[key] = key;
}
- })
+ });
}
let rows: JSX.Element[] = [];
let i = 0;
for (let key in ids) {
- rows.push(<KeyValuePair doc={realDoc} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} fieldId={key} key={key} />)
+ rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
+ @action
+ keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._keyInput = e.currentTarget.value;
+ }
+
+ @action
+ valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._valueInput = e.currentTarget.value;
+ }
+
+ newKeyValue = () =>
+ (
+ <tr className="keyValueBox-valueRow">
+ <td className="keyValueBox-td-key" style={{ width: `${100 - this.splitPercentage}%` }}>
+ <input style={{ width: "100%" }} type="text" value={this._keyInput} placeholder="Key" onChange={this.keyChanged} />
+ </td>
+ <td className="keyValueBox-td-value" style={{ width: `${this.splitPercentage}%` }}>
+ <input style={{ width: "100%" }} type="text" value={this._valueInput} placeholder="Value" onChange={this.valueChanged} onKeyPress={this.onEnterKey} />
+ </td>
+ </tr>
+ )
+
+ @action
+ onDividerMove = (e: PointerEvent): void => {
+ let nativeWidth = this._mainCont.current!.getBoundingClientRect();
+ this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round((e.clientX - nativeWidth.left) / nativeWidth.width * 100));
+ }
+ @action
+ onDividerUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onDividerMove);
+ document.removeEventListener('pointerup', this.onDividerUp);
+ }
+ onDividerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ document.addEventListener("pointermove", this.onDividerMove);
+ document.addEventListener('pointerup', this.onDividerUp);
+ }
render() {
+ let dividerDragger = this.splitPercentage === 0 ? (null) :
+ <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
+ <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
+ </div>;
- return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel}>
+ return (<div className="keyValueBox-cont" onWheel={this.onPointerWheel} ref={this._mainCont}>
<table className="keyValueBox-table">
- <tbody>
+ <tbody className="keyValueBox-tbody">
<tr className="keyValueBox-header">
- <th>Key</th>
- <th>Fields</th>
+ <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }}>Key</th>
+ <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>Fields</th>
</tr>
{this.createTable()}
+ {this.newKeyValue()}
</tbody>
</table>
- </div>)
+ {dividerDragger}
+ </div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
new file mode 100644
index 000000000..a1c5d5537
--- /dev/null
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -0,0 +1,37 @@
+@import "../globalCssVariables";
+
+
+.keyValuePair-td-key {
+ display:inline-block;
+ .keyValuePair-td-key-container{
+ width:100%;
+ height:100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ .keyValuePair-td-key-delete{
+ position: relative;
+ background-color: transparent;
+ color:red;
+ }
+ .keyValuePair-keyField {
+ width:100%;
+ text-align: center;
+ position: relative;
+ overflow: auto;
+ }
+ }
+}
+.keyValuePair-td-value {
+ display:inline-block;
+ overflow: scroll;
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont{
+ width: auto;
+ max-height: 36px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index a97e98313..4f7919f50 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,58 +1,88 @@
+import { action, observable } from 'mobx';
+import { observer } from "mobx-react";
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
+import { CompileScript } from "../../util/Scripting";
+import { Transform } from '../../util/Transform';
+import { EditableView } from "../EditableView";
+import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
-import React = require("react")
-import { FieldViewProps, FieldView } from './FieldView';
-import { Opt, Field } from '../../../fields/Field';
-import { observer } from "mobx-react"
-import { observable, action } from 'mobx';
-import { Document } from '../../../fields/Document';
-import { Key } from '../../../fields/Key';
-import { Server } from "../../Server"
+import "./KeyValuePair.scss";
+import React = require("react");
+import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { FieldValue } from '../../../new_fields/Types';
// Represents one row in a key value plane
export interface KeyValuePairProps {
rowStyle: string;
- fieldId: string;
- doc: Document;
+ keyName: string;
+ doc: Doc;
+ keyWidth: number;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
- @observable
- private key: Opt<Key>
-
- constructor(props: KeyValuePairProps) {
- super(props);
- Server.GetField(this.props.fieldId,
- action((field: Opt<Field>) => {
- if (field) {
- this.key = field as Key;
- }
- }));
-
- }
-
-
render() {
- if (!this.key) {
- return <tr><td>error</td><td></td></tr>
-
- }
let props: FieldViewProps = {
- doc: this.props.doc,
- fieldKey: this.key,
- isSelected: () => false,
- select: () => { },
+ Document: this.props.doc,
+ ContainingCollectionView: undefined,
+ fieldKey: this.props.keyName,
+ isSelected: returnFalse,
+ select: emptyFunction,
isTopMost: false,
- bindings: {},
selectOnLoad: false,
- }
+ active: returnFalse,
+ whenActiveChanged: emptyFunction,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ PanelWidth: returnZero,
+ PanelHeight: returnZero,
+ };
+ let contents = <FieldView {...props} />;
+ let fieldKey = Object.keys(props.Document).indexOf(props.fieldKey) !== -1 ? props.fieldKey : "(" + props.fieldKey + ")";
return (
<tr className={this.props.rowStyle}>
- <td>{this.key.Name}</td>
- <td><FieldView {...props} /></td>
+ <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
+ <div className="keyValuePair-td-key-container">
+ <button className="keyValuePair-td-key-delete" onClick={() => {
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
+ props.Document[props.fieldKey] = undefined;
+ }
+ else props.Document.proto![props.fieldKey] = undefined;
+ }}>
+ X
+ </button>
+ <div className="keyValuePair-keyField">{fieldKey}</div>
+ </div>
+ </td>
+ <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }}>
+ <EditableView contents={contents} height={36} GetValue={() => {
+
+ let field = FieldValue(props.Document[props.fieldKey]);
+ if (field) {
+ //TODO Types
+ return String(field);
+ // return field.ToScriptString();
+ }
+ return "";
+ }}
+ SetValue={(value: string) => {
+ let script = CompileScript(value, { addReturn: true });
+ if (!script.compiled) {
+ return false;
+ }
+ let res = script.run();
+ if (!res.success) return false;
+ const field = res.result;
+ if (IsField(field)) {
+ props.Document[props.fieldKey] = field;
+ return true;
+ }
+ return false;
+ }}>
+ </EditableView></td>
</tr>
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 00e5ebb3d..639f83b38 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,13 +1,14 @@
+@import "../globalCssVariables";
.link-container {
width: 100%;
- height: 30px;
+ height: 50px;
display: flex;
flex-direction: row;
border-top: 0.5px solid #bababa;
}
.info-container {
- width: 60%;
+ width: 65%;
padding-top: 5px;
padding-left: 5px;
display: flex;
@@ -23,17 +24,43 @@
}
.button-container {
- width: 40%;
+ width: 35%;
+ padding-top: 8px;
display: flex;
flex-direction: row;
}
.button {
- height: 15px;
- width: 15px;
- margin: 8px 5px;
+ height: 20px;
+ width: 20px;
+ margin: 8px 4px;
border-radius: 50%;
- opacity: 0.6;
+ opacity: 0.9;
pointer-events: auto;
- background-color: #2B6091;
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 60%;
+ transition: transform 0.2s;
+}
+
+.button:hover {
+ background: $main-accent;
+ cursor: pointer;
+}
+
+// .fa-icon-view {
+// margin-left: 3px;
+// margin-top: 5px;
+// }
+
+.fa-icon-edit {
+ margin-left: 6px;
+ margin-top: 6px;
+}
+
+.fa-icon-delete {
+ margin-left: 7px;
+ margin-top: 6px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 69df676ff..68b692aad 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,67 +1,61 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { observer } from "mobx-react";
-import './LinkBox.scss'
-import { KeyStore } from '../../../fields/KeyStore'
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
import { DocumentManager } from "../../util/DocumentManager";
-import { LinkEditor } from "./LinkEditor";
+import { undoBatch } from "../../util/UndoManager";
import { CollectionDockingView } from "../collections/CollectionDockingView";
+import './LinkBox.scss';
+import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+import { Cast, NumCast } from '../../../new_fields/Types';
+import { listSpec } from '../../../new_fields/Schema';
+import { action } from 'mobx';
+
+
+library.add(faEye);
+library.add(faEdit);
+library.add(faTimes);
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
linkName: String;
- pairedDoc: Document;
+ pairedDoc: Doc;
type: String;
- showEditor: () => void
+ showEditor: () => void;
}
@observer
export class LinkBox extends React.Component<Props> {
- onViewButtonPressed = (e: React.PointerEvent): void => {
- console.log("view down");
+ @undoBatch
+ onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
- if (docView) {
- docView.props.focus(this.props.pairedDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc)
- }
+ DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
}
onEditButtonPressed = (e: React.PointerEvent): void => {
- console.log("edit down");
e.stopPropagation();
this.props.showEditor();
}
- onDeleteButtonPressed = (e: React.PointerEvent): void => {
- console.log("delete down");
+ @action
+ onDeleteButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- this.props.linkDoc.GetTAsync(KeyStore.LinkedFromDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedToDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- })
+ const [linkedFrom, linkedTo] = await Promise.all([Cast(this.props.linkDoc.linkedFrom, Doc), Cast(this.props.linkDoc.linkedTo, Doc)]);
+ if (linkedFrom) {
+ const linkedToDocs = Cast(linkedFrom.linkedToDocs, listSpec(Doc));
+ if (linkedToDocs) {
+ linkedToDocs.splice(linkedToDocs.indexOf(this.props.linkDoc), 1);
}
- });
- this.props.linkDoc.GetTAsync(KeyStore.LinkedToDocs, Document, field => {
- if (field) {
- field.GetTAsync<ListField<Document>>(KeyStore.LinkedFromDocs, ListField, field => {
- if (field) {
- field.Data.splice(field.Data.indexOf(this.props.linkDoc));
- }
- })
+ }
+ if (linkedTo) {
+ const linkedFromDocs = Cast(linkedTo.linkedFromDocs, listSpec(Doc));
+ if (linkedFromDocs) {
+ linkedFromDocs.splice(linkedFromDocs.indexOf(this.props.linkDoc), 1);
}
- });
+ }
}
render() {
@@ -79,11 +73,14 @@ export class LinkBox extends React.Component<Props> {
</div>
<div className="button-container">
- <div className="button" onPointerDown={this.onViewButtonPressed}></div>
- <div className="button" onPointerDown={this.onEditButtonPressed}></div>
- <div className="button" onPointerDown={this.onDeleteButtonPressed}></div>
+ {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
+ <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
+ <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
+ <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
</div>
</div>
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.scss b/src/client/views/nodes/LinkEditor.scss
index cb191dc8c..9629585d7 100644
--- a/src/client/views/nodes/LinkEditor.scss
+++ b/src/client/views/nodes/LinkEditor.scss
@@ -1,3 +1,4 @@
+@import "../globalCssVariables";
.edit-container {
width: 100%;
height: auto;
@@ -9,21 +10,33 @@
margin-bottom: 10px;
padding: 5px;
font-size: 12px;
+ border: 1px solid #bababa;
}
.description-input {
- font-size: 12px;
+ font-size: 11px;
padding: 5px;
margin-bottom: 10px;
+ border: 1px solid #bababa;
}
.save-button {
width: 50px;
- height: 20px;
- background-color: #2B6091;
+ height: 22px;
+ pointer-events: auto;
+ background-color: $dark-color;
+ color: $light-color;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ padding: 2px;
+ font-size: 10px;
margin: 0 auto;
- color: white;
+ transition: transform 0.2s;
text-align: center;
line-height: 20px;
- font-size: 12px;
+}
+
+.save-button:hover {
+ background: $main-accent;
+ cursor: pointer;
} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 3f7b4bf2d..71a423338 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -2,32 +2,31 @@ import { observable, computed, action } from "mobx";
import React = require("react");
import { SelectionManager } from "../../util/SelectionManager";
import { observer } from "mobx-react";
-import './LinkEditor.scss'
-import { KeyStore } from '../../../fields/KeyStore'
+import './LinkEditor.scss';
import { props } from "bluebird";
import { DocumentView } from "./DocumentView";
-import { Document } from "../../../fields/Document";
-import { TextField } from "../../../fields/TextField";
import { link } from "fs";
+import { StrCast } from "../../../new_fields/Types";
+import { Doc } from "../../../new_fields/Doc";
interface Props {
- linkDoc: Document;
+ linkDoc: Doc;
showLinks: () => void;
}
@observer
export class LinkEditor extends React.Component<Props> {
- @observable private _nameInput: string = this.props.linkDoc.GetText(KeyStore.Title, "");
- @observable private _descriptionInput: string = this.props.linkDoc.GetText(KeyStore.LinkDescription, "");
+ @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
+ @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
onSaveButtonPressed = (e: React.PointerEvent): void => {
- console.log("view down");
e.stopPropagation();
- this.props.linkDoc.SetData(KeyStore.Title, this._nameInput, TextField);
- this.props.linkDoc.SetData(KeyStore.LinkDescription, this._descriptionInput, TextField);
+ let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+ linkDoc.title = this._nameInput;
+ linkDoc.linkDescription = this._descriptionInput;
this.props.showLinks();
}
@@ -43,7 +42,7 @@ export class LinkEditor extends React.Component<Props> {
<div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
</div>
- )
+ );
}
@action
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index a120ab2a7..dedcce6ef 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -10,6 +10,7 @@
padding: 5px;
margin-bottom: 10px;
font-size: 12px;
+ border: 1px solid #bababa;
}
#linkMenu-list {
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 5c6b06d00..4cf798249 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,53 +1,51 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting } from "../../../fields/Field";
-import { Key } from "../../../fields/Key";
-import { KeyStore } from '../../../fields/KeyStore';
-import { ListField } from "../../../fields/ListField";
import { DocumentView } from "./DocumentView";
import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/RefField";
interface Props {
docView: DocumentView;
- changeFlyout: () => void
+ changeFlyout: () => void;
}
@observer
export class LinkMenu extends React.Component<Props> {
- @observable private _editingLink?: Document;
+ @observable private _editingLink?: Doc;
- renderLinkItems(links: Document[], key: Key, type: string) {
+ renderLinkItems(links: Doc[], key: string, type: string) {
return links.map(link => {
- let doc = link.GetT(key, Document);
- if (doc && doc != FieldWaiting) {
- return <LinkBox key={doc.Id} linkDoc={link} linkName={link.Title} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />
+ let doc = FieldValue(Cast(link[key], Doc));
+ if (doc) {
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
}
- })
+ });
}
render() {
//get list of links from document
- let linkFrom: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedFromDocs, ListField, []);
- let linkTo: Document[] = this.props.docView.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []);
+ let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
+ let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
if (this._editingLink === undefined) {
return (
<div id="linkMenu-container">
- <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input>
+ {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
<div id="linkMenu-list">
- {this.renderLinkItems(linkTo, KeyStore.LinkedToDocs, "Source: ")}
- {this.renderLinkItems(linkFrom, KeyStore.LinkedFromDocs, "Destination: ")}
+ {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
+ {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
</div>
</div>
- )
+ );
} else {
return (
<LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
- )
+ );
}
}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 9f92410d4..3760e378a 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,15 +1,37 @@
.react-pdf__Page {
transform-origin: left top;
position: absolute;
+ top: 0;
+ left:0;
+}
+.react-pdf__Page__textContent span {
+ user-select: text;
}
.react-pdf__Document {
position: absolute;
}
.pdfBox-buttonTray {
position:absolute;
+ top: 0;
+ left:0;
z-index: 25;
+ pointer-events: all;
+}
+.pdfButton {
+ pointer-events: all;
+ width: 100px;
+ height:100px;
+}
+.pdfBox-cont {
+ pointer-events: none ;
+ span {
+ pointer-events: none !important;
+ }
+}
+.pdfBox-cont-interactive {
+ pointer-events: all;
}
.pdfBox-contentContainer {
position: absolute;
- transform-origin: "left top";
+ transform-origin: left top;
} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 70a70c7c8..e71ac4924 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,22 +1,26 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, observable, reaction, IReactionDisposer } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace, runInAction } from 'mobx';
import { observer } from "mobx-react";
import 'react-image-lightbox/style.css';
import Measure from "react-measure";
//@ts-ignore
import { Document, Page } from "react-pdf";
import 'react-pdf/dist/Page/AnnotationLayer.css';
-import { FieldWaiting, Opt } from '../../../fields/Field';
-import { ImageField } from '../../../fields/ImageField';
-import { KeyStore } from '../../../fields/KeyStore';
-import { PDFField } from '../../../fields/PDFField';
+import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
import { Annotation } from './Annotation';
import { FieldView, FieldViewProps } from './FieldView';
-import "./ImageBox.scss";
import "./PDFBox.scss";
-import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here
-import React = require("react")
+import React = require("react");
+import { SelectionManager } from "../../util/SelectionManager";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Opt } from "../../../new_fields/Doc";
+import { DocComponent } from "../DocComponent";
+import { makeInterface } from "../../../new_fields/Schema";
+import { positionSchema } from "./DocumentView";
+import { pageSchema } from "./ImageBox";
+import { ImageField, PdfField } from "../../../new_fields/URLField";
+import { InkingControl } from "../InkingControl";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -38,121 +42,58 @@ import React = require("react")
* 4) another method: click on highlight first and then drag on your desired text
* 5) To make another highlight, you need to reclick on the button
*
- * Draw:
- * 1) click draw and select color. then just draw like there's no tomorrow.
- * 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session.
- *
- * Pagination:
- * 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't.
- * 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved.
- *
- *
* written by: Andrew Kim
*/
-@observer
-export class PDFBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(PDFBox); }
- private _mainDiv = React.createRef<HTMLDivElement>()
- private _pdf = React.createRef<HTMLCanvasElement>();
+type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(positionSchema, pageSchema);
- //very useful for keeping track of X and y position throughout the PDF Canvas
- private initX: number = 0;
- private initY: number = 0;
- private initPage: boolean = false;
-
- //checks if tool is on
- private _toolOn: boolean = false; //checks if tool is on
- private _pdfContext: any = null; //gets pdf context
- private bool: Boolean = false; //general boolean debounce
- private currSpan: any;//keeps track of current span (for highlighting)
+@observer
+export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+ public static LayoutString() { return FieldView.LayoutString(PDFBox); }
- private _currTool: any; //keeps track of current tool button reference
- private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool
- private _drawTool = React.createRef<HTMLButtonElement>()//drawing tool button reference
+ private _mainDiv = React.createRef<HTMLDivElement>();
+ private renderHeight = 2400;
- private _colorTool = React.createRef<HTMLButtonElement>(); //color button reference
- private _currColor: string = "black"; //current color that user selected (for ink/pen)
+ @observable private _renderAsSvg = true;
+ @observable private _alt = false;
- private _highlightTool = React.createRef<HTMLButtonElement>(); //highlighter button reference
- private _highlightToolOn: boolean = false;
- private _pdfCanvas: any;
- private _reactionDisposer: Opt<IReactionDisposer>;
+ private _reactionDisposer?: IReactionDisposer;
@observable private _perPageInfo: Object[] = []; //stores pageInfo
@observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
- @observable private _currAnno: any = []
+ @observable private _currAnno: any = [];
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, 0); }
+ @computed private get curPage() { return NumCast(this.Document.curPage, 1); }
+ @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
componentDidMount() {
+ let wasSelected = false;
this._reactionDisposer = reaction(
- () => this.curPage,
+ () => this.props.isSelected(),
() => {
- if (this.curPage && this.initPage) {
+ if (this.curPage > 0 && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) {
this.saveThumbnail();
- this._interactive = true;
- } else {
- if (this.curPage)
- this.initPage = true;
}
+ wasSelected = this._interactive = this.props.isSelected();
},
{ fireImmediately: true });
}
componentWillUnmount() {
- if (this._reactionDisposer) {
- this._reactionDisposer();
- }
- }
-
- /**
- * selection tool used for area highlighting (stickies). Kinda temporary
- */
- selectionTool = () => {
- this._toolOn = true;
- }
- /**
- * when user draws on the canvas. When mouse pointer is down
- */
- drawDown = (e: PointerEvent) => {
- this.initX = e.offsetX;
- this.initY = e.offsetY;
- this._pdfContext.beginPath();
- this._pdfContext.lineTo(this.initX, this.initY);
- this._pdfContext.strokeStyle = this._currColor;
- this._pdfCanvas.addEventListener("pointermove", this.drawMove);
- this._pdfCanvas.addEventListener("pointerup", this.drawUp);
-
- }
- //when user drags
- drawMove = (e: PointerEvent): void => {
- //x and y mouse movement
- let x = this.initX += e.movementX,
- y = this.initY += e.movementY;
- //connects the point
- this._pdfContext.lineTo(x, y);
- this._pdfContext.stroke();
+ if (this._reactionDisposer) this._reactionDisposer();
}
- drawUp = (e: PointerEvent) => {
- this._pdfContext.closePath();
- this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
- this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
- this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
- }
-
-
/**
* highlighting helper function
*/
makeEditableAndHighlight = (colour: string) => {
var range, sel = window.getSelection();
- if (sel.rangeCount && sel.getRangeAt) {
+ if (sel && sel.rangeCount && sel.getRangeAt) {
range = sel.getRangeAt(0);
}
document.designMode = "on";
@@ -160,31 +101,31 @@ export class PDFBox extends React.Component<FieldViewProps> {
document.execCommand("HiliteColor", false, colour);
}
- if (range) {
+ if (range && sel) {
sel.removeAllRanges();
sel.addRange(range);
let obj: Object = { parentDivs: [], spans: [] };
//@ts-ignore
- if (range.commonAncestorContainer.className == 'react-pdf__Page__textContent') { //multiline highlighting case
- obj = this.highlightNodes(range.commonAncestorContainer.childNodes)
+ if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case
+ obj = this.highlightNodes(range.commonAncestorContainer.childNodes);
} else { //single line highlighting case
- let parentDiv = range.commonAncestorContainer.parentElement
+ let parentDiv = range.commonAncestorContainer.parentElement;
if (parentDiv) {
- if (parentDiv.className == 'react-pdf__Page__textContent') { //when highlight is overwritten
- obj = this.highlightNodes(parentDiv.childNodes)
+ if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten
+ obj = this.highlightNodes(parentDiv.childNodes);
} else {
parentDiv.childNodes.forEach((child) => {
- if (child.nodeName == 'SPAN') {
+ if (child.nodeName === 'SPAN') {
//@ts-ignore
- obj.parentDivs.push(parentDiv)
+ obj.parentDivs.push(parentDiv);
//@ts-ignore
- child.id = "highlighted"
+ child.id = "highlighted";
//@ts-ignore
- obj.spans.push(child)
- child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
+ obj.spans.push(child);
+ // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
}
- })
+ });
}
}
}
@@ -195,21 +136,21 @@ export class PDFBox extends React.Component<FieldViewProps> {
}
highlightNodes = (nodes: NodeListOf<ChildNode>) => {
- let temp = { parentDivs: [], spans: [] }
+ let temp = { parentDivs: [], spans: [] };
nodes.forEach((div) => {
div.childNodes.forEach((child) => {
- if (child.nodeName == 'SPAN') {
+ if (child.nodeName === 'SPAN') {
//@ts-ignore
- temp.parentDivs.push(div)
+ temp.parentDivs.push(div);
//@ts-ignore
- child.id = "highlighted"
+ child.id = "highlighted";
//@ts-ignore
- temp.spans.push(child)
- child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
+ temp.spans.push(child);
+ // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler
}
- })
+ });
- })
+ });
return temp;
}
@@ -222,29 +163,27 @@ export class PDFBox extends React.Component<FieldViewProps> {
let index: any;
this._pageInfo.divs.forEach((obj: any) => {
obj.spans.forEach((element: any) => {
- if (element == span) {
- if (!index) {
- index = this._pageInfo.divs.indexOf(obj);
- }
+ if (element === span && !index) {
+ index = this._pageInfo.divs.indexOf(obj);
}
- })
- })
+ });
+ });
if (this._pageInfo.anno.length >= index + 1) {
- if (this._currAnno.length == 0) {
+ if (this._currAnno.length === 0) {
this._currAnno.push(this._pageInfo.anno[index]);
}
} else {
- if (this._currAnno.length == 0) { //if there are no current annotation
+ if (this._currAnno.length === 0) { //if there are no current annotation
let div = span.offsetParent;
//@ts-ignore
- let divX = div.style.left
+ let divX = div.style.left;
//@ts-ignore
- let divY = div.style.top
+ let divY = div.style.top;
//slicing "px" from the end
divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span)
divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span)
- let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />
+ let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />;
this._pageInfo.anno.push(annotation);
this._currAnno.push(annotation);
}
@@ -262,7 +201,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
this.makeEditableAndHighlight(color);
}
} catch (ex) {
- this.makeEditableAndHighlight(color)
+ this.makeEditableAndHighlight(color);
}
}
}
@@ -271,11 +210,21 @@ export class PDFBox extends React.Component<FieldViewProps> {
* controls the area highlighting (stickies) Kinda temporary
*/
onPointerDown = (e: React.PointerEvent) => {
- if (this._toolOn) {
- let mouse = e.nativeEvent;
- this.initX = mouse.offsetX;
- this.initY = mouse.offsetY;
-
+ if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) {
+ if (e.altKey) {
+ this._alt = true;
+ } else {
+ if (e.metaKey) {
+ e.stopPropagation();
+ }
+ }
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+ if (this.props.isSelected() && e.buttons === 2) {
+ runInAction(() => this._alt = true);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
}
}
@@ -283,135 +232,40 @@ export class PDFBox extends React.Component<FieldViewProps> {
* controls area highlighting and partially highlighting. Kinda temporary
*/
@action
- onPointerUp = (e: React.PointerEvent) => {
- if (this._highlightToolOn) {
+ onPointerUp = (e: PointerEvent) => {
+ this._alt = false;
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (this.props.isSelected()) {
this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color.
- this._highlightToolOn = false;
- }
- if (this._toolOn) {
- let mouse = e.nativeEvent;
- let finalX = mouse.offsetX;
- let finalY = mouse.offsetY;
- let width = Math.abs(finalX - this.initX); //width
- let height = Math.abs(finalY - this.initY); //height
-
- //these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky
- if (finalX < this.initX) {
- this.initX = finalX;
- }
- if (finalY < this.initY) {
- this.initY = finalY;
- }
-
- if (this._mainDiv.current) {
- let sticky = <Sticky key={Utils.GenerateGuid()} Height={height} Width={width} X={this.initX} Y={this.initY} />
- this._pageInfo.area.push(sticky);
- }
- this._toolOn = false;
}
this._interactive = true;
}
- /**
- * starts drawing the line when user presses down.
- */
- onDraw = () => {
- if (this._currTool != null) {
- this._currTool.style.backgroundColor = "grey";
- }
-
- if (this._drawTool.current) {
- this._currTool = this._drawTool.current;
- if (this._drawToolOn) {
- this._drawToolOn = false;
- this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
- this._pdfCanvas.removeEventListener("pointerup", this.drawUp);
- this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
- this._drawTool.current.style.backgroundColor = "grey";
- } else {
- this._drawToolOn = true;
- this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
- this._drawTool.current.style.backgroundColor = "cyan";
- }
- }
- }
-
-
- /**
- * for changing color (for ink/pen)
- */
- onColorChange = (e: React.PointerEvent) => {
- if (e.currentTarget.innerHTML == "Red") {
- this._currColor = "red";
- } else if (e.currentTarget.innerHTML == "Blue") {
- this._currColor = "blue";
- } else if (e.currentTarget.innerHTML == "Green") {
- this._currColor = "green";
- } else if (e.currentTarget.innerHTML == "Black") {
- this._currColor = "black";
- }
-
- }
-
-
- /**
- * For highlighting (text drag highlighting)
- */
- onHighlight = () => {
- this._drawToolOn = false;
- if (this._currTool != null) {
- this._currTool.style.backgroundColor = "grey";
- }
- if (this._highlightTool.current) {
- this._currTool = this._drawTool.current;
- if (this._highlightToolOn) {
- this._highlightToolOn = false;
- this._highlightTool.current.style.backgroundColor = "grey";
- } else {
- this._highlightToolOn = true;
- this._highlightTool.current.style.backgroundColor = "orange";
- }
- }
- }
-
@action
saveThumbnail = () => {
+ this._renderAsSvg = false;
setTimeout(() => {
- var me = this;
- htmlToImage.toPng(this._mainDiv.current!,
- { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 })
- .then(function (dataUrl: string) {
- me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
- })
+ let nwidth = FieldValue(this.Document.nativeWidth, 0);
+ let nheight = FieldValue(this.Document.nativeHeight, 0);
+ htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
+ .then(action((dataUrl: string) => {
+ this.props.Document.thumbnail = new ImageField(new URL(dataUrl));
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
+ this._renderAsSvg = true;
+ }))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
- }, 1000);
+ }, 1250);
}
@action
onLoaded = (page: any) => {
- if (this._mainDiv.current) {
- this._mainDiv.current.childNodes.forEach((element) => {
- if (element.nodeName == "DIV") {
- element.childNodes[0].childNodes.forEach((e) => {
-
- if (e instanceof HTMLCanvasElement) {
- this._pdfCanvas = e;
- this._pdfContext = e.getContext("2d")
-
- }
-
- })
- }
- })
- }
-
// bcz: the number of pages should really be set when the document is imported.
- this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages);
- if (this._perPageInfo.length == 0) { //Makes sure it only runs once
- this._perPageInfo = [...Array(page._transport.numPages)]
+ this.props.Document.numPages = page._transport.numPages;
+ if (this._perPageInfo.length === 0) { //Makes sure it only runs once
+ this._perPageInfo = [...Array(page._transport.numPages)];
}
this._loaded = true;
}
@@ -419,34 +273,40 @@ export class PDFBox extends React.Component<FieldViewProps> {
@action
setScaling = (r: any) => {
// bcz: the nativeHeight should really be set when the document is imported.
- // also, the native dimensions could be different for different pages of the PDF
+ // also, the native dimensions could be different for different pages of the canvas
// so this design is flawed.
- var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0);
- if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) {
- this.props.doc.SetNumber(KeyStore.NativeHeight, nativeWidth * r.entry.height / r.entry.width);
- }
- if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) {
- this.saveThumbnail();
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ if (!FieldValue(this.Document.nativeHeight, 0)) {
+ var nativeHeight = nativeWidth * r.offset.height / r.offset.width;
+ this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.props.Document.nativeHeight = nativeHeight;
}
}
-
+ @computed
+ get pdfPage() {
+ return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
+ }
@computed
get pdfContent() {
- let page = this.curPage;
- if (page == 0)
- page = 1;
- const renderHeight = 2400;
- let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
- let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight;
+ trace();
+ let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
+ if (!pdfUrl) {
+ return <p>No pdf url to render</p>;
+ }
+ let pdfpage = this.pdfPage;
+ let body = this.Document.nativeHeight ?
+ pdfpage :
+ <Measure offset onResize={this.setScaling}>
+ {({ measureRef }) =>
+ <div className="pdfBox-page" ref={measureRef}>
+ {pdfpage}
+ </div>
+ }
+ </Measure>;
+ let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}>
- <Measure onResize={this.setScaling}>
- {({ measureRef }) =>
- <div className="pdfBox-page" ref={measureRef}>
- <Page height={renderHeight} pageNumber={page} onLoadSuccess={this.onLoaded} />
- </div>
- }
- </Measure>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}>
+ {body}
</Document>
</div >;
}
@@ -454,34 +314,35 @@ export class PDFBox extends React.Component<FieldViewProps> {
@computed
get pdfRenderer() {
let proxy = this._loaded ? (null) : this.imageProxyRenderer;
- let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
- if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) {
+ let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
+ if ((!this._interactive && proxy) || !pdfUrl) {
return proxy;
}
return [
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
- <div key="pdfBox-contentShell">
- {this.pdfContent}
- {proxy}
- </div>
+ this.pdfContent,
+ proxy
];
}
@computed
get imageProxyRenderer() {
- let field = this.props.doc.Get(KeyStore.Thumbnail);
- if (field) {
- let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ let thumbField = this.props.Document.thumbnail;
+ if (thumbField) {
+ let path = this.thumbnailPage !== this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
+ thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
return <img src={path} width="100%" />;
}
return (null);
}
-
+ @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
+ @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
render() {
+ trace();
+ let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div className="pdfBox-cont" ref={this._mainDiv} onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} >
+ <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} >
{this.pdfRenderer}
</div >
);
diff --git a/src/client/views/nodes/Sticky.tsx b/src/client/views/nodes/Sticky.tsx
deleted file mode 100644
index d57dd5c0b..000000000
--- a/src/client/views/nodes/Sticky.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import React = require("react")
-import { observer } from "mobx-react"
-import 'react-pdf/dist/Page/AnnotationLayer.css'
-
-interface IProps {
- Height: number;
- Width: number;
- X: number;
- Y: number;
-}
-
-/**
- * Sticky, also known as area highlighting, is used to highlight large selection of the PDF file.
- * Improvements that could be made: maybe store line array and store that somewhere for future rerendering.
- *
- * Written By: Andrew Kim
- */
-@observer
-export class Sticky extends React.Component<IProps> {
-
- private initX: number = 0;
- private initY: number = 0;
-
- private _ref = React.createRef<HTMLCanvasElement>();
- private ctx: any; //context that keeps track of sticky canvas
-
- /**
- * drawing. Registers the first point that user clicks when mouse button is pressed down on canvas
- */
- drawDown = (e: React.PointerEvent) => {
- if (this._ref.current) {
- this.ctx = this._ref.current.getContext("2d");
- let mouse = e.nativeEvent;
- this.initX = mouse.offsetX;
- this.initY = mouse.offsetY;
- this.ctx.beginPath();
- this.ctx.lineTo(this.initX, this.initY);
- this.ctx.strokeStyle = "black";
- document.addEventListener("pointermove", this.drawMove);
- document.addEventListener("pointerup", this.drawUp);
- }
- }
-
- //when user drags
- drawMove = (e: PointerEvent): void => {
- //x and y mouse movement
- let x = this.initX += e.movementX,
- y = this.initY += e.movementY;
- //connects the point
- this.ctx.lineTo(x, y);
- this.ctx.stroke();
-
- }
-
- /**
- * when user lifts the mouse, the drawing ends
- */
- drawUp = (e: PointerEvent) => {
- this.ctx.closePath();
- console.log(this.ctx);
- document.removeEventListener("pointermove", this.drawMove);
- }
-
- render() {
- return (
- <div onPointerDown={this.drawDown}>
- <canvas ref={this._ref} height={this.props.Height} width={this.props.Width}
- style={{
- position: "absolute",
- top: "20px",
- left: "0px",
- zIndex: 1,
- background: "yellow",
- transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
- opacity: 0.4
- }}
- />
-
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 7306450d9..35db64cf4 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,8 @@
-.videobox-cont{
+.videoBox-cont, .videoBox-cont-fullScreen{
width: 100%;
- height: 100%;
+ height: Auto;
+}
+
+.videoBox-cont-fullScreen {
+ pointer-events: all;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 22ff5c5ad..81c429a02 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,43 +1,163 @@
-import React = require("react")
-import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
-import { observer } from "mobx-react"
-import { VideoField } from '../../../fields/VideoField';
-import "./VideoBox.scss"
-import { ContextMenu } from "../../views/ContextMenu";
-import { observable, action } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
+import React = require("react");
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import * as rp from "request-promise";
+import "./VideoBox.scss";
+import { action, IReactionDisposer, reaction, observable } from "mobx";
+import { DocComponent } from "../DocComponent";
+import { positionSchema } from "./DocumentView";
+import { makeInterface } from "../../../new_fields/Schema";
+import { pageSchema } from "./ImageBox";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import Measure from "react-measure";
+import "./VideoBox.scss";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+
+type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
+const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
-export class VideoBox extends React.Component<FieldViewProps> {
+export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+ private _reactionDisposer?: IReactionDisposer;
+ private _videoRef: HTMLVideoElement | null = null;
+ private _loaded: boolean = false;
+ @observable _playTimer?: NodeJS.Timeout = undefined;
+ @observable _fullScreen = false;
+ @observable public Playing: boolean = false;
+ public static LayoutString() { return FieldView.LayoutString(VideoBox); }
+
+ public get player(): HTMLVideoElement | undefined {
+ if (this._videoRef) {
+ return this._videoRef;
+ }
+ }
+ @action
+ setScaling = (r: any) => {
+ if (this._loaded) {
+ // bcz: the nativeHeight should really be set when the document is imported.
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ var newNativeHeight = nativeWidth * r.offset.height / r.offset.width;
+ if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) {
+ this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0);
+ this.Document.nativeHeight = newNativeHeight;
+ }
+ } else {
+ this._loaded = true;
+ }
+ }
- public static LayoutString() { return FieldView.LayoutString(VideoBox) }
+ @action public Play() {
+ this.Playing = true;
+ if (this.player) this.player.play();
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000);
+ }
+
+ @action public Pause() {
+ this.Playing = false;
+ if (this.player) this.player.pause();
+ if (this._playTimer) {
+ clearInterval(this._playTimer);
+ this._playTimer = undefined;
+ }
+ }
- constructor(props: FieldViewProps) {
- super(props);
+ @action public FullScreen() {
+ this._fullScreen = true;
+ this.player && this.player.requestFullscreen();
}
-
+ @action
+ updateTimecode = () => this.player && (this.props.Document.curPage = this.player.currentTime)
componentDidMount() {
+ if (this.props.setVideoBox) this.props.setVideoBox(this);
}
-
componentWillUnmount() {
+ this.Pause();
+ if (this._reactionDisposer) this._reactionDisposer();
+ }
+
+ @action
+ setVideoRef = (vref: HTMLVideoElement | null) => {
+ this._videoRef = vref;
+ if (vref) {
+ vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+ if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
+ vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true });
+ }
+ }
+ videoContent(path: string) {
+ let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
+ return <video className={`${style}`} ref={this.setVideoRef} onPointerDown={this.onPointerDown}>
+ <source src={path} type="video/mp4" />
+ Not supported.
+ </video>;
+ }
+
+ getMp4ForVideo(videoId: string = "JN5beCVArMs") {
+ return new Promise(async (resolve, reject) => {
+ const videoInfoRequestConfig = {
+ headers: {
+ connection: 'keep-alive',
+ "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0',
+ },
+
+ };
+ try {
+ let responseSchema: any = {};
+ const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
+ const dataHtml = videoInfoResponse;
+ const start = dataHtml.indexOf('ytplayer.config = ') + 18;
+ const end = dataHtml.indexOf(';ytplayer.load');
+ const subString = dataHtml.substring(start, end);
+ const subJson = JSON.parse(subString);
+ const stringSub = subJson.args.player_response;
+ const stringSubJson = JSON.parse(stringSub);
+ const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats;
+ const videoDetails = stringSubJson.videoDetails;
+ responseSchema.adaptiveFormats = adaptiveFormats;
+ responseSchema.videoDetails = videoDetails;
+ resolve(responseSchema);
+ }
+ catch (err) {
+ console.log(`
+ --- Youtube ---
+ Function: getMp4ForVideo
+ Error: `, err);
+ reject(err);
+ }
+ });
+ }
+ onPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
}
-
render() {
- let field = this.props.doc.Get(this.props.fieldKey)
- let path = field == FieldWaiting ? "http://techslides.com/demos/sample-videos/small.mp4":
- field instanceof VideoField ? field.Data.href : "http://techslides.com/demos/sample-videos/small.mp4";
-
- return (
- <div>
- <video width = {200} height = {200} controls className = "videobox-cont">
- <source src = {path} type = "video/mp4"/>
- Not supported.
- </video>
- </div>
- )
+ let field = Cast(this.Document[this.props.fieldKey], VideoField);
+ if (!field) {
+ return <div>Loading</div>;
+ }
+
+ // this.getMp4ForVideo().then((mp4) => {
+ // console.log(mp4);
+ // }).catch(e => {
+ // console.log("")
+ // });
+ // //
+ let content = this.videoContent(field.url.href);
+ return NumCast(this.props.Document.nativeHeight) ?
+ content :
+ <Measure offset onResize={this.setScaling}>
+ {({ measureRef }) =>
+ <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
+ {content}
+ </div>
+ }
+ </Measure>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a535b2638..eb09b0693 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,10 +1,31 @@
-.webBox-cont {
+.webBox-cont, .webBox-cont-interactive{
padding: 0vw;
position: absolute;
+ top: 0;
+ left:0;
width: 100%;
height: 100%;
- overflow: scroll;
+ overflow: auto;
+ pointer-events: none ;
+}
+.webBox-cont-interactive {
+ pointer-events: all;
+ span {
+ user-select: text !important;
+ }
+}
+
+#webBox-htmlSpan {
+ position: absolute;
+ top:0;
+ left:0;
+}
+
+.webBox-overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
}
.webBox-button {
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2ca8d49ce..2239a8e38 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,38 +1,59 @@
import "./WebBox.scss";
-import React = require("react")
-import { WebField } from '../../../fields/WebField';
+import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
-import { FieldWaiting } from '../../../fields/Field';
-import { observer } from "mobx-react"
-import { computed } from 'mobx';
-import { KeyStore } from '../../../fields/KeyStore';
+import { HtmlField } from "../../../new_fields/HtmlField";
+import { WebField } from "../../../new_fields/URLField";
+import { observer } from "mobx-react";
+import { computed, reaction, IReactionDisposer } from 'mobx';
+import { DocumentDecorations } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
@observer
export class WebBox extends React.Component<FieldViewProps> {
public static LayoutString() { return FieldView.LayoutString(WebBox); }
- constructor(props: FieldViewProps) {
- super(props);
+ _ignore = 0;
+ onPreWheel = (e: React.WheelEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPrePointer = (e: React.PointerEvent) => {
+ this._ignore = e.timeStamp;
+ }
+ onPostPointer = (e: React.PointerEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
+ }
+ onPostWheel = (e: React.WheelEvent) => {
+ if (this._ignore !== e.timeStamp) {
+ e.stopPropagation();
+ }
}
-
- @computed get html(): string { return this.props.doc.GetHtml(KeyStore.Data, ""); }
-
render() {
- let field = this.props.doc.Get(this.props.fieldKey);
- let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
- field instanceof WebField ? field.Data.href : "https://crossorigin.me/" + "https://cs.brown.edu";
-
- let content = this.html ?
- <span dangerouslySetInnerHTML={{ __html: this.html }}></span> :
- <div style={{ width: "100%", height: "100%", position: "absolute" }}>
- <iframe src={path} style={{ position: "absolute", width: "100%", height: "100%" }}></iframe>
- {this.props.isSelected() ? (null) : <div style={{ width: "100%", height: "100%", position: "absolute" }} />}
+ let field = this.props.Document[this.props.fieldKey];
+ let view;
+ if (field instanceof HtmlField) {
+ view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
+ } else if (field instanceof WebField) {
+ view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ } else {
+ view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />;
+ }
+ let content =
+ <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
+ {view}
</div>;
+ let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+
+ let classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
return (
- <div className="webBox-cont" >
- {content}
- </div>)
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ </>);
}
} \ No newline at end of file