aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx166
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx52
-rw-r--r--src/client/views/nodes/DocumentView.tsx301
-rw-r--r--src/client/views/nodes/FieldView.tsx23
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss6
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx191
-rw-r--r--src/client/views/nodes/IconBox.scss9
-rw-r--r--src/client/views/nodes/IconBox.tsx30
-rw-r--r--src/client/views/nodes/ImageBox.scss6
-rw-r--r--src/client/views/nodes/ImageBox.tsx126
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx12
-rw-r--r--src/client/views/nodes/KeyValuePair.scss8
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx23
-rw-r--r--src/client/views/nodes/LinkBox.tsx23
-rw-r--r--src/client/views/nodes/LinkEditor.tsx5
-rw-r--r--src/client/views/nodes/LinkMenu.tsx15
-rw-r--r--src/client/views/nodes/PDFBox.scss4
-rw-r--r--src/client/views/nodes/PDFBox.tsx170
-rw-r--r--src/client/views/nodes/VideoBox.scss6
-rw-r--r--src/client/views/nodes/VideoBox.tsx149
20 files changed, 859 insertions, 466 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2ba0458f5..499b83c0f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,23 +1,24 @@
-import { computed, trace, action, reaction, IReactionDisposer } from "mobx";
+import { computed, IReactionDisposer, reaction, action } from "mobx";
import { observer } from "mobx-react";
+import { Doc } 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 } from "../../../Utils";
import { Transform } from "../../util/Transform";
+import { DocComponent } from "../DocComponent";
import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
import "./DocumentView.scss";
import React = require("react");
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
-import { OmitKeys, Utils } from "../../../Utils";
+import { UndoManager } from "../../util/UndoManager";
import { SelectionManager } from "../../util/SelectionManager";
-import { Doc } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
}
const schema = createSchema({
zoomBasis: "number",
- zIndex: "number"
+ zIndex: "number",
});
//TODO Types: The import order is wrong, so positionSchema is undefined
@@ -27,8 +28,6 @@ const FreeformDocument = makeInterface(schema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
private _mainCont = React.createRef<HTMLDivElement>();
- private _downX: number = 0;
- private _downY: number = 0;
_bringToFrontDisposer?: IReactionDisposer;
@computed get transform() {
@@ -40,9 +39,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); }
@computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
@computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
- @computed get width(): number { return FieldValue(this.Document.width, 0); }
- @computed get height(): number { return FieldValue(this.Document.height, 0); }
- @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); }
+ @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); }
+ @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); }
set width(w: number) {
this.Document.width = w;
@@ -56,14 +54,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.Document.width = this.nativeWidth / this.nativeHeight * h;
}
}
- set zIndex(h: number) {
- this.Document.zIndex = h;
- }
-
contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1;
panelWidth = () => this.props.PanelWidth();
panelHeight = () => this.props.PanelHeight();
- toggleMinimized = () => this.toggleIcon();
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling()).scale(1 / this.zoom)
@@ -71,11 +64,11 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@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}
+ collapseToPoint={this.collapseToPoint}
/>;
}
@@ -84,99 +77,74 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this.props.bringToFront(this.props.Document);
if (values instanceof List) {
let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]);
- this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false);
+ this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0],
+ this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false);
}
- });
+ }, { fireImmediately: true });
}
componentWillUnmount() {
if (this._bringToFrontDisposer) this._bringToFrontDisposer();
}
- animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) {
+ static _undoBatch?: UndoManager.Batch = undefined;
+ @action
+ public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => {
+ SelectionManager.DeselectAll();
+ if (expandedDocs) {
+ if (!CollectionFreeFormDocumentView._undoBatch) {
+ CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
+ }
+ let isMinimized: boolean | undefined;
+ expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => {
+ let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
+ if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
+ if (isMinimized === undefined) {
+ isMinimized = BoolCast(maximizedDoc.isMinimized, false);
+ }
+ 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);
+ }
+ }
+
+ animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) {
+
setTimeout(() => {
let now = Date.now();
let progress = Math.min(1, (now - stime) / 200);
let pval = maximizing ?
[icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] :
[targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress];
- target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
- target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
- target.x = pval[0];
- target.y = pval[1];
+ this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
+ this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
+ this.props.Document.x = pval[0];
+ this.props.Document.y = pval[1];
+ if (first) {
+ this.props.Document.proto!.willMaximize = false;
+ }
if (now < stime + 200) {
- this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing);
+ this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing);
}
else {
if (!maximizing) {
- target.isMinimized = true;
- target.x = targ[0];
- target.y = targ[1];
- target.width = width;
- target.height = height;
+ this.props.Document.proto!.isMinimized = true;
+ this.props.Document.x = targ[0];
+ this.props.Document.y = targ[1];
+ this.props.Document.width = width;
+ this.props.Document.height = height;
}
- target.isIconAnimating = undefined;
+ this.props.Document.proto!.isIconAnimating = undefined;
}
},
2);
}
- @action
- public toggleIcon = async (): Promise<void> => {
- SelectionManager.DeselectAll();
- let isMinimized: boolean | undefined;
- let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc));
- let minimizedDoc: Doc | undefined = this.props.Document;
- if (!maximizedDocs) {
- minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
- if (minimizedDoc) maximizedDocs = await Cast(minimizedDoc.maximizedDocs, listSpec(Doc));
- }
- if (minimizedDoc && maximizedDocs && maximizedDocs instanceof List) {
- let minimizedTarget = minimizedDoc;
- 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;
- let maxx = NumCast(maximizedDoc.x, undefined);
- let maxy = NumCast(maximizedDoc.y, undefined);
- let maxw = NumCast(maximizedDoc.width, undefined);
- let maxh = NumCast(maximizedDoc.height, undefined);
- if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined &&
- maxw !== undefined && maxh !== undefined) {
- let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny);
- maximizedDoc.isMinimized = false;
- maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0])
- }
- }
- });
- }
- }
- onPointerDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- e.stopPropagation();
- }
- onClick = async (e: React.MouseEvent) => {
- e.stopPropagation();
- let ctrlKey = e.ctrlKey;
- if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
- Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
- if (await BoolCast(this.props.Document.isButton, false)) {
- let maximizedDocs = await Cast(this.props.Document.maximizedDocs, listSpec(Doc));
- if (maximizedDocs) { // bcz: need a better way to associate behaviors with click events on widget-documents
- if (ctrlKey)
- this.props.addDocument && maximizedDocs.filter(d => d instanceof Doc).map(maxDoc => this.props.addDocument!(maxDoc, false));
- this.toggleIcon();
- }
- }
- }
- }
-
- 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);
@@ -190,25 +158,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
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;
+ // 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 ? fadeDown / w : w / fadeUp))) : 1;
+ // 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}
- onPointerDown={this.onPointerDown}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onClick={this.onClick}
style={{
- outlineColor: "black",
- outlineStyle: "dashed",
- outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px",
opacity: zoomFade,
borderRadius: `${this.borderRounding()}px`,
transformOrigin: "left top",
@@ -217,7 +179,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
width: this.width,
height: this.height,
position: "absolute",
- zIndex: this.zIndex,
+ zIndex: this.Document.zIndex || 0,
backgroundColor: "transparent"
}} >
{this.docView}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bbc927b5a..02396c3af 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed } from "mobx";
+import { computed, trace } from "mobx";
import { observer } from "mobx-react";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
@@ -21,7 +21,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
import { FieldViewProps } from "./FieldView";
import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { List } from "../../../new_fields/List";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -43,9 +43,21 @@ const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: () => boolean,
select: (ctrl: boolean) => void,
- layoutKey: string
+ layoutKey: string,
+ hideOnLeave?: boolean
}> {
- @computed get layout(): string { return Cast(this.props.Document[this.props.layoutKey], "string", "<p>Error loading layout data</p>"); }
+ @computed get layout(): string {
+ const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+ if (layout === undefined) {
+ return this.props.Document.data ?
+ "<FieldView {...props} fieldKey='data' />" :
+ KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+ } else if (typeof layout === "string") {
+ return layout;
+ } else {
+ return "<p>Loading layout</p>";
+ }
+ }
CreateBindings(): JsxBindings {
return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
@@ -58,20 +70,38 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
return new List<string>();
}
- set templates(templates: List<string>) { this.props.Document.templates = templates; }
- get finalLayout() {
- const baseLayout = this.layout;
+ @computed get finalLayout() {
+ const baseLayout = this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
let base = baseLayout;
let layout = baseLayout;
- this.templates.forEach(template => {
- layout = template.replace("{layout}", base);
- base = layout;
- });
+ // 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);
+ layout = template.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()}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fd012e7ea..7750b9173 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,9 +1,19 @@
-import { action, computed, runInAction } from "mobx";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faAlignCenter, faCaretSquareRight, faCompressArrowsAlt, faExpandArrowsAlt, faLayerGroup, faSquare, faTrash, faConciergeBell, faFolder, faMapPin, faLink, faFingerprint, faCrosshairs, faDesktop } from '@fortawesome/free-solid-svg-icons';
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { BoolCast, Cast, FieldValue, StrCast, NumCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { emptyFunction, Utils } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
+import { DocServer } from "../../DocServer";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { SearchUtil } from "../../util/SearchUtil";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
@@ -12,20 +22,30 @@ import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
-import { Template, Templates } from "./../Templates";
+import { DocComponent } from "../DocComponent";
+import { PresentationView } from "../PresentationView";
+import { Template } from "./../Templates";
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import React = require("react");
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { FieldValue, StrCast, BoolCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { DocServer } from "../../DocServer";
-import { Id } from "../../../new_fields/RefField";
-import { PresentationView } from "../PresentationView";
+import { Id, Copy } from '../../../new_fields/FieldSymbols';
+import { ContextMenuProps } from '../ContextMenuItem';
+const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
+
+library.add(faTrash);
+library.add(faExpandArrowsAlt);
+library.add(faCompressArrowsAlt);
+library.add(faLayerGroup);
+library.add(faAlignCenter);
+library.add(faCaretSquareRight);
+library.add(faSquare);
+library.add(faConciergeBell);
+library.add(faFolder);
+library.add(faMapPin);
+library.add(faLink);
+library.add(faFingerprint);
+library.add(faCrosshairs);
+library.add(faDesktop);
const linkSchema = createSchema({
title: "string",
@@ -41,27 +61,28 @@ const LinkDoc = makeInterface(linkSchema);
export interface DocumentViewProps {
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;
+ addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ removeDocument?: (doc: Doc) => boolean;
+ moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
isTopMost: boolean;
ContentScaling: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
- focus: (doc: Document) => void;
+ focus: (doc: Doc) => void;
selectOnLoad: boolean;
parentActive: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
- toggleMinimized: () => void;
bringToFront: (doc: Doc) => void;
+ addDocTab: (doc: Doc, where: string) => void;
+ collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
}
const schema = createSchema({
layout: "string",
nativeWidth: "number",
nativeHeight: "number",
- backgroundColor: "string"
+ backgroundColor: "string",
});
export const positionSchema = createSchema({
@@ -83,6 +104,9 @@ const Document = makeInterface(schema);
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
private _downX: number = 0;
private _downY: number = 0;
+ private _lastTap: number = 0;
+ private _doubleTap = false;
+ private _hitExpander = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -99,6 +123,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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) {
@@ -106,6 +131,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
handlers: { drop: this.drop.bind(this) }
});
}
+ // 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(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title),
+ this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
+ () => {
+ 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
@@ -121,9 +159,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
componentWillUnmount() {
- if (this._dropDisposer) {
- this._dropDisposer();
- }
+ if (this._reactionDisposer) this._reactionDisposer();
+ if (this._dropDisposer) this._dropDisposer();
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -131,10 +168,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
}
- startDragging(x: number, y: number, dropAction: dropActionType) {
+ startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
if (this._mainCont.current) {
+ 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([this.props.Document]);
+ 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;
@@ -148,28 +186,95 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
}
+ toggleMinimized = async () => {
+ let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
+ if (minimizedDoc) {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(
+ NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
+ }
+ }
- onClick = (e: React.MouseEvent): void => {
- if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+ onClick = async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ let altKey = e.altKey;
+ let ctrlKey = e.ctrlKey;
+ if (this._doubleTap && !this.props.isTopMost) {
+ this.props.addDocTab(this.props.Document, "inTab");
+ SelectionManager.DeselectAll();
+ this.props.Document.libraryBrush = false;
+ }
+ else 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);
+ let isExpander = (e.target as any).id === "isExpander";
+ if (BoolCast(this.props.Document.isButton, false) || isExpander) {
+ SelectionManager.DeselectAll();
+ let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
+ let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+ let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
+ let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+ let expandedDocs: Doc[] = [];
+ expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+ expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+ // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
+ if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents
+ let expandedProtoDocs = expandedDocs.map(doc => Doc.GetProto(doc));
+ let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
+ let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
+ if (altKey) {
+ maxLocation = this.props.Document.maximizeLocation = (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace");
+ if (!maxLocation || maxLocation === "inPlace") {
+ let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized, false), false);
+ expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
+ let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedProtoDocs[0], this.props.ContainingCollectionView);
+ if (!hasView) {
+ this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
+ }
+ expandedProtoDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
+ }
+ }
+ if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
+ let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
+ if (dataDocs) {
+ expandedDocs.forEach(maxDoc =>
+ (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
+ this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+ }
+ } else {
+ let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
+ }
+ }
+ else if (linkedToDocs.length || linkedFromDocs.length) {
+ let linkedFwdDocs = [
+ linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
+ linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
+
+ let linkedFwdPage = [
+ linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
+ linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
+ if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+ let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
+ DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]);
+ }
+ }
+ }
}
}
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 (e.shiftKey && e.buttons === 1) {
- if (this.props.isTopMost) {
- this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined);
- } else {
- CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
- }
+ this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
+ if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
+ CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
e.stopPropagation();
- } else if (this.active) {
+ } else {
+ if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.
document.removeEventListener("pointermove", this.onPointerMove);
document.addEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -177,12 +282,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble) {
+ if (!e.cancelBubble && this.active) {
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);
+ if (!e.altKey && !this.topMost && e.buttons === 1) {
+ 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
@@ -192,30 +297,29 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerUp = (e: PointerEvent): void => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ this._lastTap = Date.now();
}
- deleteClicked = (): void => {
- this.props.removeDocument && this.props.removeDocument(this.props.Document);
- }
- fieldsClicked = (e: React.MouseEvent): void => {
- let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 });
- CollectionDockingView.Instance.AddRightSplit(kvp);
- }
- makeButton = (e: React.MouseEvent): void => {
- this.props.Document.isButton = !BoolCast(this.props.Document.isButton, false);
- if (this.props.Document.isButton && !this.props.Document.nativeWidth) {
- this.props.Document.nativeWidth = this.props.Document[WidthSym]();
- this.props.Document.nativeHeight = this.props.Document[HeightSym]();
+ deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
+ fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+ makeBtnClicked = (): void => {
+ let doc = Doc.GetProto(this.props.Document);
+ doc.isButton = !BoolCast(doc.isButton, false);
+ if (StrCast(doc.layout).indexOf("Formatted") !== -1) { // only need to freeze the dimensions of text boxes since they don't have a native width and height naturally
+ 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 => {
- const doc = Doc.MakeDelegate(FieldValue(this.Document.proto));
- if (doc) {
- CollectionDockingView.Instance.OpenFullScreen(doc);
- }
- ContextMenu.Instance.clearItems();
+ fullScreenClicked = (): void => {
+ CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
SelectionManager.DeselectAll();
}
+
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
@@ -223,9 +327,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.Document;
- const protoDest = destDoc.proto;
- const protoSrc = sourceDoc.proto;
- Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc);
+ if (de.mods === "AltKey") {
+ const protoDest = destDoc.proto;
+ const protoSrc = sourceDoc.proto;
+ let src = protoSrc ? protoSrc : sourceDoc;
+ let dst = protoDest ? protoDest : destDoc;
+ dst.data = (src.data! as ObjectField)[Copy]();
+ dst.nativeWidth = src.nativeWidth;
+ dst.nativeHeight = src.nativeHeight;
+ }
+ else {
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ }
e.stopPropagation();
}
}
@@ -242,7 +356,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
-
@action
addTemplate = (template: Template) => {
this.templates.push(template.Layout);
@@ -260,6 +373,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.templates = this.templates;
}
+ freezeNativeDimensions = (e: React.MouseEvent): void => {
+ if (NumCast(this.props.Document.nativeWidth)) {
+ let proto = Doc.GetProto(this.props.Document);
+ let nw = proto.nativeWidth;
+ let nh = proto.nativeHeight;
+ proto.nativeWidth = proto.nativeHeight = undefined;
+ this.props.Document.width = this.props.Document.frozenWidth;
+ this.props.Document.height = this.props.Document.frozenHeight;
+ }
+ else {
+ let scale = this.props.ScreenToLocalTransform().Scale * NumCast(this.props.Document.zoomBasis, 1);
+ let scr = this.screenRect();
+ let proto = Doc.GetProto(this.props.Document);
+ this.props.Document.frozenWidth = this.props.Document.width;
+ this.props.Document.frozenHeight = this.props.Document.height;
+ this.props.Document.height = proto.nativeHeight = scr.height * scale;
+ this.props.Document.width = proto.nativeWidth = scr.width * scale;
+ }
+ }
+
@action
onContextMenu = (e: React.MouseEvent): void => {
e.stopPropagation();
@@ -270,16 +403,28 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked });
- ContextMenu.Instance.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton });
- ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked });
- ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])) });
- ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) });
- //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) })
- ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) });
- ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked });
+ const cm = ContextMenu.Instance;
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
+ subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
+ subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
+ cm.addItem({ description: "Open...", subitems: subitems });
+ cm.addItem({ description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze", event: this.freezeNativeDimensions, icon: "edit" });
+ cm.addItem({ description: "Pin to Pres", event: () => PresentationView.Instance.PinDoc(this.props.Document), icon: "map-pin" });
+ cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeBtnClicked, icon: "concierge-bell" });
+ cm.addItem({
+ description: "Find aliases", event: async () => {
+ const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
+ this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
+ }, icon: "search"
+ });
+ cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
+ cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
+ cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+ cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
if (!this.topMost) {
// DocumentViews should stop propagation of this event
e.stopPropagation();
@@ -290,6 +435,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
+ onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
+ onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
isSelected = () => SelectionManager.IsSelected(this);
select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed);
@@ -300,13 +447,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
render() {
var scaling = this.props.ContentScaling();
- var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%";
- var nativeWidth = this.nativeWidth > 0 ? this.nativeWidth.toString() + "px" : "100%";
+ var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
return (
<div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
ref={this._mainCont}
style={{
+ outlineColor: "maroon",
+ outlineStyle: "dashed",
+ outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
+ BoolCast(this.props.Document.protoBrush, false) ?
+ `${1 * this.props.ScreenToLocalTransform().Scale}px`
+ : "0px",
borderRadius: "inherit",
background: this.Document.backgroundColor || "",
width: nativeWidth,
@@ -314,6 +467,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
transform: `scale(${scaling}, ${scaling})`
}}
onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+
+ onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
>
{this.contents}
</div>
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index a1e083b36..7b642b299 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,13 +1,13 @@
import React = require("react");
import { observer } from "mobx-react";
-import { computed } from "mobx";
+import { computed, observable } from "mobx";
import { FormattedTextBox } from "./FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { VideoBox } from "./VideoBox";
import { AudioBox } from "./AudioBox";
import { DocumentContentsView } from "./DocumentContentsView";
import { Transform } from "../../util/Transform";
-import { returnFalse, emptyFunction } from "../../../Utils";
+import { returnFalse, emptyFunction, returnOne } from "../../../Utils";
import { CollectionView } from "../collections/CollectionView";
import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionVideoView } from "../collections/CollectionVideoView";
@@ -17,6 +17,8 @@ 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";
//
@@ -33,6 +35,7 @@ export interface FieldViewProps {
isTopMost: boolean;
selectOnLoad: boolean;
addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocTab: (document: Doc, where: string) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -77,16 +80,20 @@ export class FieldView extends React.Component<FieldViewProps> {
}
else if (field instanceof AudioField) {
return <AudioBox {...this.props} />;
+ } else if (field instanceof DateField) {
+ return <p>{field.date.toLocaleString()}</p>;
}
else if (field instanceof Doc) {
+ let returnHundred = () => 100;
return (
<DocumentContentsView Document={field}
addDocument={undefined}
+ addDocTab={this.props.addDocTab}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
- ContentScaling={() => 1}
- PanelWidth={() => 100}
- PanelHeight={() => 100}
+ ContentScaling={returnOne}
+ PanelWidth={returnHundred}
+ PanelHeight={returnHundred}
isTopMost={true} //TODO Why is this top most?
selectOnLoad={false}
focus={emptyFunction}
@@ -95,8 +102,8 @@ export class FieldView extends React.Component<FieldViewProps> {
layoutKey={"layout"}
ContainingCollectionView={this.props.ContainingCollectionView}
parentActive={this.props.active}
- toggleMinimized={emptyFunction}
- whenActiveChanged={this.props.whenActiveChanged} />
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={emptyFunction} />
);
}
else if (field instanceof List) {
@@ -109,7 +116,7 @@ export class FieldView extends React.Component<FieldViewProps> {
// return <WebBox {...this.props} />
// }
else if (!(field instanceof Promise)) {
- return <p>{JSON.stringify(field)}</p>;
+ return <p>{field.toString()}</p>;
}
else {
return <p> {"Waiting for server..."} </p>;
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index d43aa4e02..4a29c1949 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -11,14 +11,15 @@
}
.formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden {
- background: $light-color-secondary;
+ 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: scroll;
+ overflow-y: auto;
overflow-x: hidden;
color: initial;
height: 100%;
@@ -26,7 +27,6 @@
}
.formattedTextBox-cont-hidden {
- overflow: hidden;
pointer-events: none;
}
.formattedTextBox-inner-rounded {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index eeb60cb3d..5c635cc0c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,30 +1,37 @@
-import { action, IReactionDisposer, reaction, trace, computed, _allowStateChangesInsideComputed } 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 } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { EditorState, Plugin, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
+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 { schema } from "../../util/RichTextSchema";
+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 { MainOverlayTextBox } from "../MainOverlayTextBox";
+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");
-import { SelectionManager } from "../../util/SelectionManager";
-import { DocComponent } from "../DocComponent";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Opt, Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { observer } from "mobx-react";
-import { InkingControl } from "../InkingControl";
-import { StrCast, Cast, NumCast, BoolCast } from "../../../new_fields/Types";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { Id } from "../../../new_fields/RefField";
-const { buildMenuItems } = require("prosemirror-example-setup");
-const { menuBar } = require("prosemirror-menu");
+import { DocUtils } from '../../documents/Documents';
+
+library.add(faEdit);
+library.add(faSmile);
// FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document
//
@@ -43,8 +50,9 @@ const { menuBar } = require("prosemirror-menu");
// this will edit the document and assign the new value to that field.
//]
-export interface FormattedTextBoxOverlay {
+export interface FormattedTextBoxProps {
isOverlay?: boolean;
+ hideOnLeave?: boolean;
}
const richTextSchema = createSchema({
@@ -55,7 +63,7 @@ type RichTextDocument = makeInterface<[typeof richTextSchema]>;
const RichTextDocument = makeInterface(richTextSchema);
@observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) {
+export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
@@ -63,15 +71,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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._proseRef = React.createRef();
+ if (this.props.isOverlay) {
+ DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
+ }
}
_applyingChange: boolean = false;
@@ -91,11 +107,29 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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;
+
+ DocUtils.MakeLink(sourceDoc, destDoc);
+ de.data.droppedDocuments.push(destDoc);
+ e.stopPropagation();
}
}
componentDidMount() {
+ if (this._ref.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._ref.current, {
+ handlers: { drop: this.drop.bind(this) }
+ });
+ }
const config = {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
@@ -118,7 +152,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
};
if (this.props.isOverlay) {
- this._inputReactionDisposer = reaction(() => MainOverlayTextBox.Instance.TextDoc && MainOverlayTextBox.Instance.TextDoc[Id],
+ this._inputReactionDisposer = reaction(() => FormattedTextBox.InputBoxOverlay,
() => {
if (this._editorView) {
this._editorView.destroy();
@@ -128,7 +162,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
);
} else {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => this.props.isSelected() && !BoolCast(this.props.Document.isButton, false) && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform));
+ () => {
+ if (this.props.isSelected()) {
+ FormattedTextBox.InputBoxOverlay = this;
+ FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
+ }
+ });
}
@@ -148,8 +187,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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
+ 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("@@@", "")));
+ }
}
if (this.props.selectOnLoad) {
@@ -171,13 +218,35 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (this._proxyReactionDisposer) {
this._proxyReactionDisposer();
}
+ if (this._dropDisposer) {
+ this._dropDisposer();
+ }
}
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)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "0";
+ }
+ }
+ let ctrlKey = e.ctrlKey;
+ if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
+ let href = (e.target as any).href;
+ for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
+ href = parent.childNodes[0].href;
+ }
+ if (href) {
+ if (href.indexOf(DocServer.prepend("/doc/")) === 0) {
+ let docid = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
+ DocServer.GetRefField(docid).then(f => {
+ (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"))
+ });
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
this._gotDown = true;
@@ -185,58 +254,24 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
onPointerUp = (e: React.PointerEvent): void => {
- if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip)
+ if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) {
this._toolTipTextMenu.tooltip.style.opacity = "1";
+ }
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ @action
onFocused = (e: React.FocusEvent): void => {
if (!this.props.isOverlay) {
- MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform);
+ FormattedTextBox.InputBoxOverlay = this;
} else {
- if (this._proseRef.current) {
- this._proseRef.current.scrollTop = MainOverlayTextBox.Instance.TextScroll;
+ if (this._ref.current) {
+ this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
}
}
}
-
- //REPLACE THIS WITH CAPABILITIES SPECIFIC TO THIS TYPE OF NODE
- textCapability = (e: React.MouseEvent): void => {
- if (NumCast(this.props.Document.nativeWidth)) {
- this.props.Document.nativeWidth = undefined;
- this.props.Document.nativeHeight = undefined;
-
- } else {
- this.props.Document.nativeWidth = this.props.Document[WidthSym]();
- this.props.Document.nativeHeight = this.props.Document[HeightSym]();
- }
- }
- specificContextMenu = (e: React.MouseEvent): void => {
- if (!this._gotDown) {
- e.preventDefault();
- return;
- }
- ContextMenu.Instance.addItem({
- description: NumCast(this.props.Document.nativeWidth) ? "Unfreeze" : "Freeze",
- event: this.textCapability
- });
-
- // ContextMenu.Instance.addItem({
- // description: "Submenu",
- // items: [
- // {
- // description: "item 1", event:
- // },
- // {
- // description: "item 2", event:
- // }
- // ]
- // })
- // e.stopPropagation()
- }
-
onPointerWheel = (e: React.WheelEvent): void => {
if (this.props.isSelected()) {
e.stopPropagation();
@@ -271,7 +306,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
});
}
-
+ 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();
@@ -287,30 +328,48 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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");
+ }
+ }
+
+ @observable
+ _entered = false;
+ @action
+ onPointerEnter = (e: React.PointerEvent) => {
+ this._entered = true;
+ }
+ @action
+ onPointerLeave = (e: React.PointerEvent) => {
+ this._entered = false;
}
render() {
let style = this.props.isOverlay ? "scroll" : "hidden";
let rounded = NumCast(this.props.Document.borderRounding) < 0 ? "-rounded" : "";
- let color = StrCast(this.props.Document.backgroundColor);
let interactive = InkingControl.Instance.selectedTool ? "" : "interactive";
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
+ background: this.props.hideOnLeave ? "rgba(0,0,0,0.4)" : undefined,
+ opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || this.props.Document.libraryBrush ? 1 : 0.1) : 1,
+ color: this.props.hideOnLeave ? "white" : "initial",
pointerEvents: interactive ? "all" : "none",
- background: color,
}}
- onKeyDown={this.onKeyPress}
+ // 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}
+ onPointerEnter={this.onPointerEnter}
+ onPointerLeave={this.onPointerLeave}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ pointerEvents: this.props.Document.isButton ? "none" : "all" }} ref={this._proseRef} />
+ <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", 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
index 85bbdeb59..488681027 100644
--- a/src/client/views/nodes/IconBox.scss
+++ b/src/client/views/nodes/IconBox.scss
@@ -1,20 +1,21 @@
@import "../globalCssVariables";
.iconBox-container {
- position: absolute;
+ position: inherit;
left:0;
top:0;
- height: 100%;
+ height: auto;
width: max-content;
// overflow: hidden;
pointer-events: all;
svg {
width: $MINIMIZED_ICON_SIZE !important;
- height: 100%;
+ height: $MINIMIZED_ICON_SIZE !important;
+ height: auto;
background: white;
}
.iconBox-label {
- position: inherit;
+ position: absolute;
width:max-content;
font-size: 14px;
margin-top: 3px;
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 3fab10df4..00021bc78 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -2,18 +2,16 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { SelectionManager } from "../../util/SelectionManager";
import { FieldView, FieldViewProps } from './FieldView';
import "./IconBox.scss";
import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
import { IconField } from "../../../new_fields/IconField";
import { ContextMenu } from "../ContextMenu";
import Measure from "react-measure";
import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
-import { listSpec } from "../../../new_fields/Schema";
library.add(faCaretUp);
@@ -42,24 +40,40 @@ export class IconBox extends React.Component<FieldViewProps> {
setLabelField = (e: React.MouseEvent): void => {
this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
}
+ setUseOwnTitleField = (e: React.MouseEvent): void => {
+ this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
+ }
specificContextMenu = (e: React.MouseEvent): void => {
ContextMenu.Instance.addItem({
- description: BoolCast(this.props.Document.hideLabel) ? "show label" : "hide label",
+ description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
event: this.setLabelField
});
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
+ ContextMenu.Instance.addItem({
+ description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
+ event: this.setUseOwnTitleField
+ });
+ }
}
@observable _panelWidth: number = 0;
@observable _panelHeight: number = 0;
render() {
let labelField = StrCast(this.props.Document.labelField);
let hideLabel = BoolCast(this.props.Document.hideLabel);
- let maxDoc = Cast(this.props.Document.maximizedDocs, listSpec(Doc), []);
- let label = !hideLabel && maxDoc && labelField ? (maxDoc.length === 1 ? maxDoc[0][labelField] : this.props.Document[labelField]) : "";
+ let maxDocs = DocListCast(this.props.Document.maximizedDocs);
+ let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
+ let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle, false) ? firstDoc[labelField] : this.props.Document.title);
return (
<div className="iconBox-container" onContextMenu={this.specificContextMenu}>
{this.minimizedIcon}
- <Measure onResize={(r) => runInAction(() => { if (r.entry.width || BoolCast(this.props.Document.hideLabel)) this.props.Document.nativeWidth = this.props.Document.width = (r.entry.width + Number(MINIMIZED_ICON_SIZE)); })}>
+ <Measure offset onResize={(r) => runInAction(() => {
+ if (r.offset!.width || BoolCast(this.props.Document.hideLabel)) {
+ this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
+ if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth;
+ }
+ })}>
{({ measureRef }) =>
<span ref={measureRef} className="iconBox-label">{label}</span>
}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 2316a050e..f1b73a676 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -25,7 +25,11 @@
}
.imageBox-cont img {
- height: 100%;
+ height: auto;
+ width:100%;
+}
+.imageBox-cont-interactive img {
+ height: auto;
width:100%;
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0e9e904a8..4c2b73b70 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,4 @@
-
-import { action, observable } from 'mobx';
+import { action, observable, trace } 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
@@ -13,11 +12,19 @@ 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 { FieldValue, Cast, StrCast, PromiseValue, NumCast } 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';
+import { Doc, WidthSym, HeightSym } from '../../../new_fields/Doc';
+import { faImage } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { ContextMenuItemProps, ContextMenuProps } from '../ContextMenuItem';
+var path = require('path');
+
+
+library.add(faImage);
+
export const pageSchema = createSchema({
curPage: "number"
@@ -38,15 +45,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@observable private _isOpen: boolean = false;
private dropDisposer?: DragManager.DragDropDisposer;
- @action
- onLoad = (target: any) => {
- var h = this._imgRef.current!.naturalHeight;
- var w = this._imgRef.current!.naturalWidth;
- if (this._photoIndex === 0) {
- this.Document.nativeHeight = FieldValue(this.Document.nativeWidth, 0) * h / w;
- this.Document.height = FieldValue(this.Document.width, 0) * h / w;
- }
- }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -88,16 +86,19 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
onPointerDown = (e: React.PointerEvent): void => {
- 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();
- }
+ if (e.shiftKey && e.ctrlKey)
+
+ e.stopPropagation();
+ // 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();
+ // }
}
@action
onPointerUp = (e: PointerEvent): void => {
@@ -131,11 +132,20 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
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);
- }
+ let subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: "Rotate", event: action(() => {
+ this.props.Document.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
+ let nw = this.props.Document.nativeWidth;
+ this.props.Document.nativeWidth = this.props.Document.nativeHeight;
+ this.props.Document.nativeHeight = nw;
+ let w = this.props.Document.width;
+ this.props.Document.width = this.props.Document.height;
+ this.props.Document.height = w;
+ }), icon: "expand-arrows-alt"
});
+ ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems });
}
}
@@ -156,18 +166,66 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
);
}
+ choosePath(url: URL) {
+ const lower = url.href.toLowerCase();
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1 || !(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ return url.href;
+ }
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
render() {
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
+ let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
+ let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
+ // this._curSuffix = "";
+ // if (w > 20) {
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, 1);
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) paths = [this.choosePath(field.url)];
+ else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
+ // }
let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
+ let rotation = NumCast(this.props.Document.rotation, 0);
+ let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
+ let shift = (rotation % 180) ? (this.props.Document[HeightSym]() - this.props.Document[WidthSym]() / aspect) / 2 : 0;
return (
- <div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} />
+ <div id={id} className={`imageBox-cont${interactive}`}
+ onPointerDown={this.onPointerDown}
+ onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <img id={id}
+ key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
+ src={paths[Math.min(paths.length, this._photoIndex)]}
+ style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
+ // style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }}
+ width={nativeWidth}
+ ref={this._imgRef}
+ onError={this.onError} />
{paths.length > 1 ? this.dots(paths) : (null)}
- {this.lightbox(paths)}
+ {/* {this.lightbox(paths)} */}
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 876a3c173..849f17aa4 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -8,7 +8,7 @@ import "./KeyValueBox.scss";
import { KeyValuePair } from "./KeyValuePair";
import React = require("react");
import { NumCast, Cast, FieldValue } from "../../../new_fields/Types";
-import { Doc, IsField } from "../../../new_fields/Doc";
+import { Doc, Field } from "../../../new_fields/Doc";
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
@@ -18,7 +18,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
@computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
-
+ get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; }
constructor(props: FieldViewProps) {
super(props);
@@ -28,7 +28,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
onEnterKey = (e: React.KeyboardEvent): void => {
if (e.key === 'Enter') {
if (this._keyInput && this._valueInput) {
- let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ let doc = this.fieldDocToLayout;
if (!doc) {
return;
}
@@ -41,7 +41,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let res = script.run();
if (!res.success) return;
const field = res.result;
- if (IsField(field)) {
+ if (Field.IsField(field)) {
realDoc[this._keyInput] = field;
}
this._keyInput = "";
@@ -60,7 +60,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
}
createTable = () => {
- let doc = FieldValue(Cast(this.props.Document.data, Doc));
+ let doc = this.fieldDocToLayout;
if (!doc) {
return <tr><td>Loading...</td></tr>;
}
@@ -78,7 +78,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let rows: JSX.Element[] = [];
let i = 0;
- for (let key in ids) {
+ for (let key of Object.keys(ids).sort()) {
rows.push(<KeyValuePair doc={realDoc} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index ff6885965..a1c5d5537 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -26,4 +26,12 @@
.keyValuePair-td-value {
display:inline-block;
overflow: scroll;
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont{
+ width: auto;
+ max-height: 36px;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 235792cbe..228d07018 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,7 +1,7 @@
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 { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils';
import { CompileScript } from "../../util/Scripting";
import { Transform } from '../../util/Transform';
import { EditableView } from "../EditableView";
@@ -9,7 +9,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import "./KeyValueBox.scss";
import "./KeyValuePair.scss";
import React = require("react");
-import { Doc, Opt, IsField } from '../../../new_fields/Doc';
+import { Doc, Opt, Field } from '../../../new_fields/Doc';
import { FieldValue } from '../../../new_fields/Types';
// Represents one row in a key value plane
@@ -38,28 +38,31 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
focus: emptyFunction,
PanelWidth: returnZero,
PanelHeight: returnZero,
+ addDocTab: emptyFunction
};
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 className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
<button className="keyValuePair-td-key-delete" onClick={() => {
- let field = FieldValue(props.Document[props.fieldKey]);
- field && (props.Document[props.fieldKey] = undefined);
+ 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">{this.props.keyName}</div>
+ <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();
+ if (Field.IsField(field)) {
+ return Field.toScriptString(field);
}
return "";
}}
@@ -71,7 +74,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
let res = script.run();
if (!res.success) return false;
const field = res.result;
- if (IsField(field)) {
+ if (Field.IsField(field, true)) {
props.Document[props.fieldKey] = field;
return true;
}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 08cfa590b..68b692aad 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -31,28 +31,7 @@ export class LinkBox extends React.Component<Props> {
@undoBatch
onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
e.stopPropagation();
- let docView = DocumentManager.Instance.getDocumentView(this.props.pairedDoc);
- if (docView) {
- docView.props.focus(docView.props.Document);
- } else {
- const contextDoc = await Cast(this.props.pairedDoc.annotationOn, Doc);
- if (!contextDoc) {
- CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(this.props.pairedDoc));
- } else {
- const page = NumCast(this.props.pairedDoc.page, undefined);
- const curPage = NumCast(contextDoc.curPage, undefined);
- if (page !== curPage) {
- contextDoc.curPage = page;
- }
- let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
- if (contextView) {
- contextDoc.panTransformType = "Ease";
- contextView.props.focus(contextDoc);
- } else {
- CollectionDockingView.Instance.AddRightSplit(contextDoc);
- }
- }
- }
+ DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
}
onEditButtonPressed = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index f82c6e9cb..71a423338 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -24,8 +24,9 @@ export class LinkEditor extends React.Component<Props> {
onSaveButtonPressed = (e: React.PointerEvent): void => {
e.stopPropagation();
- this.props.linkDoc.title = this._nameInput;
- this.props.linkDoc.linkDescription = this._descriptionInput;
+ let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+ linkDoc.title = this._nameInput;
+ linkDoc.linkDescription = this._descriptionInput;
this.props.showLinks();
}
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index e21adebbc..3f09d6214 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -5,10 +5,9 @@ import { LinkBox } from "./LinkBox";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
import React = require("react");
-import { Doc } from "../../../new_fields/Doc";
-import { Cast, FieldValue } from "../../../new_fields/Types";
-import { listSpec } from "../../../new_fields/Schema";
-import { Id } from "../../../new_fields/RefField";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
interface Props {
docView: DocumentView;
@@ -24,19 +23,19 @@ export class LinkMenu extends React.Component<Props> {
return links.map(link => {
let doc = FieldValue(Cast(link[key], Doc));
if (doc) {
- return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+ return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
}
});
}
render() {
//get list of links from document
- let linkFrom: Doc[] = Cast(this.props.docView.props.Document.linkedFromDocs, listSpec(Doc), []);
- let linkTo: Doc[] = Cast(this.props.docView.props.Document.linkedToDocs, listSpec(Doc), []);
+ 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, "linkedTo", "Destination: ")}
{this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 3760e378a..449408a61 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -17,6 +17,10 @@
z-index: 25;
pointer-events: all;
}
+.pdfBox-thumbnail {
+ position: absolute;
+ width: 100%;
+}
.pdfButton {
pointer-events: all;
width: 100px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 14cbed04c..aa29a7170 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,26 +1,29 @@
import * as htmlToImage from "html-to-image";
-import { action, computed, IReactionDisposer, observable, reaction, Reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } 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 { Id } from "../../../new_fields/FieldSymbols";
+import { makeInterface } from "../../../new_fields/Schema";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { ImageField, PdfField } from "../../../new_fields/URLField";
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
+import { DocServer } from "../../DocServer";
+import { DocComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { SearchBox } from "../SearchBox";
import { Annotation } from './Annotation';
+import { positionSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
+import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
+var path = require('path');
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";
+import { ContextMenu } from "../ContextMenu";
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
@@ -53,10 +56,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
private _mainDiv = React.createRef<HTMLDivElement>();
+ private renderHeight = 2400;
@observable private _renderAsSvg = true;
+ @observable private _alt = false;
- private _reactionDisposer: Opt<IReactionDisposer>;
+ private _reactionDisposer?: IReactionDisposer;
@observable private _perPageInfo: Object[] = []; //stores pageInfo
@observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno
@@ -65,27 +70,25 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@observable private _interactive: boolean = false;
@observable private _loaded: boolean = false;
- @computed private get curPage() { return FieldValue(this.Document.curPage, 1); }
- @computed private get thumbnailPage() { return Cast(this.props.Document.thumbnailPage, "number", -1); }
+ @computed private get curPage() { return NumCast(this.Document.curPage, 1); }
+ @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); }
componentDidMount() {
+ let wasSelected = this.props.isSelected();
this._reactionDisposer = reaction(
- () => [SelectionManager.SelectedDocuments().slice()],
+ () => [this.props.isSelected(), this.curPage],
() => {
- if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage !== this.thumbnailPage && !this.props.isSelected()) {
+ if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) {
this.saveThumbnail();
- this._interactive = true;
}
+ wasSelected = this._interactive = this.props.isSelected();
},
{ fireImmediately: true });
}
componentWillUnmount() {
- if (this._reactionDisposer) {
- this._reactionDisposer();
- this._reactionDisposer = undefined;
- }
+ if (this._reactionDisposer) this._reactionDisposer();
}
/**
@@ -163,10 +166,8 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
let index: any;
this._pageInfo.divs.forEach((obj: any) => {
obj.spans.forEach((element: any) => {
- if (element === span) {
- if (!index) {
- index = this._pageInfo.divs.indexOf(obj);
- }
+ if (element === span && !index) {
+ index = this._pageInfo.divs.indexOf(obj);
}
});
});
@@ -216,14 +217,15 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
if (e.altKey) {
this._alt = true;
} else {
- if (e.metaKey)
+ if (e.metaKey) {
e.stopPropagation();
+ }
}
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
}
if (this.props.isSelected() && e.buttons === 2) {
- this._alt = true;
+ runInAction(() => this._alt = true);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointerup", this.onPointerUp);
}
@@ -243,23 +245,28 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
}
-
@action
saveThumbnail = () => {
+ this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1);
this._renderAsSvg = false;
setTimeout(() => {
+ runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0);
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 })
+ htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 })
.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;
+ SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => {
+ if (returnedFilename) {
+ let url = DocServer.prepend(returnedFilename);
+ this.props.Document.thumbnail = new ImageField(new URL(url));
+ }
+ runInAction(() => this._renderAsSvg = true);
+ })
}))
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
- }, 250);
+ }, 1250);
}
@action
@@ -279,33 +286,34 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
// so this design is flawed.
var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
if (!FieldValue(this.Document.nativeHeight, 0)) {
- var nativeHeight = nativeWidth * r.entry.height / r.entry.width;
+ 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;
}
}
- renderHeight = 2400;
@computed
get pdfPage() {
- return <Page height={this.renderHeight} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />
+ return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />;
}
@computed
get pdfContent() {
- let page = this.curPage;
- const renderHeight = 2400;
let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- let xf = FieldValue(this.Document.nativeHeight, 0) / renderHeight;
- let body = NumCast(this.props.Document.nativeHeight) ?
- this.pdfPage :
- <Measure onResize={this.setScaling}>
+ 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}>
- {this.pdfPage}
+ {pdfpage}
</div>
}
</Measure>;
+ let xf = (this.Document.nativeHeight || 0) / this.renderHeight;
return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}>
- <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}>
+ <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}>
{body}
</Document>
</div >;
@@ -313,47 +321,81 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
@computed
get pdfRenderer() {
- let proxy = this._loaded ? (null) : this.imageProxyRenderer;
let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField);
- if ((!this._interactive && proxy) || !pdfUrl) {
+ let proxy = this.imageProxyRenderer;
+ if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) {
return proxy;
}
return [
+ proxy,
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
- this.pdfContent,
- proxy
+ this.pdfContent
];
}
+ choosePath(url: URL) {
+ if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1)
+ return url.href;
+ let ext = path.extname(url.href);
+ return url.href.replace(ext, this._curSuffix + ext);
+ }
+ @observable _smallRetryCount = 1;
+ @observable _mediumRetryCount = 1;
+ @observable _largeRetryCount = 1;
+ @action retryPath = () => {
+ if (this._curSuffix === "_s") this._smallRetryCount++;
+ if (this._curSuffix === "_m") this._mediumRetryCount++;
+ if (this._curSuffix === "_l") this._largeRetryCount++;
+ }
+ @action onError = () => {
+ let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount;
+ if (timeout < 10)
+ setTimeout(this.retryPath, Math.min(10000, timeout * 5));
+ }
+ _curSuffix = "_m";
+
@computed
get imageProxyRenderer() {
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%" />;
+ if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) {
+
+ // let transform = this.props.ScreenToLocalTransform().inverse();
+ let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
+ // var [sptX, sptY] = transform.transformPoint(0, 0);
+ // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
+ // let w = bptX - sptX;
+
+ let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+ // this._curSuffix = "";
+ // if (w > 20) {
+ let field = thumbField;
+ // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
+ // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m";
+ // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
+ if (field instanceof ImageField) path = this.choosePath(field.url);
+ // }
+ return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />;
}
return (null);
}
- @observable _alt = false;
- @action
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.key === "Alt") {
- this._alt = true;
- }
- }
- @action
- onKeyUp = (e: React.KeyboardEvent) => {
- if (e.key === "Alt") {
- this._alt = false;
+ @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true);
+ @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false);
+ onContextMenu = (e: React.MouseEvent): void => {
+ let field = Cast(this.Document[this.props.fieldKey], PdfField);
+ if (field) {
+ let url = field.url.href;
+ ContextMenu.Instance.addItem({
+ description: "Copy path", event: () => {
+ Utils.CopyText(url);
+ }, icon: "expand-arrows-alt"
+ });
}
}
render() {
- trace();
let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
return (
- <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} >
+ <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} >
{this.pdfRenderer}
</div >
);
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 76bbeb37c..35db64cf4 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,8 @@
-.videobox-cont{
+.videoBox-cont, .videoBox-cont-fullScreen{
width: 100%;
height: Auto;
+}
+
+.videoBox-cont-fullScreen {
+ pointer-events: all;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 422508f90..35ecf12f6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,82 +1,145 @@
import React = require("react");
import { observer } from "mobx-react";
import { FieldView, FieldViewProps } from './FieldView';
+import * as rp from "request-promise";
import "./VideoBox.scss";
-import { action, computed, trace } from "mobx";
+import { action, IReactionDisposer, reaction, observable } from "mobx";
import { DocComponent } from "../DocComponent";
import { positionSchema } from "./DocumentView";
import { makeInterface } from "../../../new_fields/Schema";
import { pageSchema } from "./ImageBox";
-import { Cast, FieldValue, NumCast, ToConstructor, ListSpec } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import Measure from "react-measure";
import "./VideoBox.scss";
-import { Field, FieldResult, Opt } from "../../../new_fields/Doc";
+import { RouteStore } from "../../../server/RouteStore";
+import { DocServer } from "../../DocServer";
+import { actionFieldDecorator } from "mobx/lib/internal";
type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
const VideoDocument = makeInterface(positionSchema, pageSchema);
@observer
export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
-
+ private _reactionDisposer?: IReactionDisposer;
private _videoRef: HTMLVideoElement | null = null;
- private _loaded: boolean = false;
- private get initialTimecode() { return FieldValue(this.Document.curPage, -1); }
+ @observable _playTimer?: NodeJS.Timeout = undefined;
+ @observable _fullScreen = false;
+ @observable public Playing: boolean = false;
public static LayoutString() { return FieldView.LayoutString(VideoBox); }
- public get player(): HTMLVideoElement | undefined {
- if (this._videoRef) {
- return this._videoRef;
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
+ }
+
+ videoLoad = () => {
+ let aspect = this.player!.videoWidth / this.player!.videoHeight;
+ var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
+ var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ if (!nativeWidth || !nativeHeight) {
+ if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
+ this.Document.nativeHeight = this.Document.nativeWidth / aspect;
+ this.Document.height = FieldValue(this.Document.width, 0) / aspect;
}
}
- @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.entry.height / r.entry.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;
+
+ @action public Play() {
+ this.Playing = true;
+ if (this.player) this.player.play();
+ if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500);
+ }
+
+ @action public Pause() {
+ this.Playing = false;
+ if (this.player) this.player.pause();
+ if (this._playTimer) {
+ clearInterval(this._playTimer);
+ this._playTimer = undefined;
}
}
+ @action public FullScreen() {
+ this._fullScreen = true;
+ this.player && this.player.requestFullscreen();
+ }
+
+ @action
+ updateTimecode = () => {
+ this.player && (this.props.Document.curPage = this.player.currentTime);
+ }
+
componentDidMount() {
if (this.props.setVideoBox) this.props.setVideoBox(this);
}
+ componentWillUnmount() {
+ this.Pause();
+ if (this._reactionDisposer) this._reactionDisposer();
+ }
@action
setVideoRef = (vref: HTMLVideoElement | null) => {
this._videoRef = vref;
- if (this.initialTimecode >= 0 && vref) {
- vref.currentTime = this.initialTimecode;
+ if (vref) {
+ vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+ if (this._reactionDisposer) this._reactionDisposer();
+ this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
+ !this.Playing && (vref.currentTime = this.Document.curPage || 0)
+ , { fireImmediately: true });
}
}
- videoContent(path: string) {
- return <video className="videobox-cont" ref={this.setVideoRef}>
- <source src={path} type="video/mp4" />
- Not supported.
- </video>;
+
+ getMp4ForVideo(videoId: string = "JN5beCVArMs") {
+ return new Promise(async (resolve, reject) => {
+ const videoInfoRequestConfig = {
+ headers: {
+ connection: 'keep-alive',
+ "user-agent": 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:43.0) Gecko/20100101 Firefox/46.0',
+ },
+ };
+ try {
+ let responseSchema: any = {};
+ const videoInfoResponse = await rp.get(DocServer.prepend(RouteStore.corsProxy + "/" + `https://www.youtube.com/watch?v=${videoId}`), videoInfoRequestConfig);
+ const dataHtml = videoInfoResponse;
+ const start = dataHtml.indexOf('ytplayer.config = ') + 18;
+ const end = dataHtml.indexOf(';ytplayer.load');
+ const subString = dataHtml.substring(start, end);
+ const subJson = JSON.parse(subString);
+ const stringSub = subJson.args.player_response;
+ const stringSubJson = JSON.parse(stringSub);
+ const adaptiveFormats = stringSubJson.streamingData.adaptiveFormats;
+ const videoDetails = stringSubJson.videoDetails;
+ responseSchema.adaptiveFormats = adaptiveFormats;
+ responseSchema.videoDetails = videoDetails;
+ resolve(responseSchema);
+ }
+ catch (err) {
+ console.log(`
+ --- Youtube ---
+ Function: getMp4ForVideo
+ Error: `, err);
+ reject(err);
+ }
+ });
+ }
+ onPointerDown = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
}
render() {
let field = Cast(this.Document[this.props.fieldKey], VideoField);
- if (!field) {
- return <div>Loading</div>;
- }
- let content = this.videoContent(field.url.href);
- return NumCast(this.props.Document.nativeHeight) ?
- content :
- <Measure onResize={this.setScaling}>
- {({ measureRef }) =>
- <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
- {content}
- </div>
- }
- </Measure>;
+
+ // this.getMp4ForVideo().then((mp4) => {
+ // console.log(mp4);
+ // }).catch(e => {
+ // console.log("")
+ // });
+ // //
+
+ let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : "");
+ return !field ? <div>Loading</div> :
+ <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} onPointerDown={this.onPointerDown}>
+ <source src={field.url.href} type="video/mp4" />
+ Not supported.
+ </video>;
}
} \ No newline at end of file