diff options
| author | bob <bcz@cs.brown.edu> | 2019-06-11 10:53:36 -0400 |
|---|---|---|
| committer | bob <bcz@cs.brown.edu> | 2019-06-11 10:53:36 -0400 |
| commit | 95674ee7f68782d1ce85858120efea956825bcb9 (patch) | |
| tree | 0d2ddc9838b45278d6533099f7030a9713d6413f /src/client/views/nodes | |
| parent | 4dacd1220e6a0ef73167f187f52f3b4c222c2586 (diff) | |
| parent | 06d5bb5c65da6f4a115ebf8118989115420bccef (diff) | |
merged embedded linking with master
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 198 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 26 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 278 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 34 | ||||
| -rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 197 | ||||
| -rw-r--r-- | src/client/views/nodes/IconBox.scss | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/IconBox.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.scss | 6 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 145 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 55 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValuePair.tsx | 34 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkMenu.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.scss | 4 | ||||
| -rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 133 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 102 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 9 |
17 files changed, 707 insertions, 525 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 631bf1ba8..499b83c0f 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,26 +1,24 @@ -import { action, computed, IReactionDisposer, reaction } from "mobx"; +import { computed, IReactionDisposer, reaction, action } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +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, Utils } from "../../../Utils"; -import { DocumentManager } from "../../util/DocumentManager"; -import { SelectionManager } from "../../util/SelectionManager"; +import { OmitKeys } from "../../../Utils"; import { Transform } from "../../util/Transform"; -import { UndoManager } from "../../util/UndoManager"; -import { CollectionDockingView } from "../collections/CollectionDockingView"; import { DocComponent } from "../DocComponent"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); +import { UndoManager } from "../../util/UndoManager"; +import { SelectionManager } from "../../util/SelectionManager"; 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 @@ -30,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() { @@ -43,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; @@ -59,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 = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs)); getTransform = (): Transform => this.props.ScreenToLocalTransform() .translate(-this.X, -this.Y) .scale(1 / this.contentScaling()).scale(1 / this.zoom) @@ -74,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} />; } @@ -87,7 +77,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this.props.bringToFront(this.props.Document); if (values instanceof List) { let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]); - this.animateBetweenIcon(true, scrpt, [values[2], values[3]], values[4], values[5], values[6], this.props.Document, values[7] ? true : false); + this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0], + this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false); } }, { fireImmediately: true }); } @@ -96,68 +87,24 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF if (this._bringToFrontDisposer) this._bringToFrontDisposer(); } - animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) { - if (first) { - if (maximizing) target.width = target.height = 1; - } - 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]; - if (now < stime + 200) { - this.animateBetweenIcon(false, icon, targ, width, height, stime, target, maximizing); - } - else { - if (!maximizing) { - target.isMinimized = true; - target.x = targ[0]; - target.y = targ[1]; - target.width = width; - target.height = height; - } - target.isIconAnimating = undefined; - } - }, - 2); - } + static _undoBatch?: UndoManager.Batch = undefined; @action - public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => { + public collapseToPoint = async (scrpt: number[], expandedDocs: Doc[] | undefined): Promise<void> => { SelectionManager.DeselectAll(); - let isMinimized: boolean | undefined; - let minimizedDoc: Doc | undefined = this.props.Document; - if (!maximizedDocs) { - minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (minimizedDoc) maximizedDocs = await DocListCastAsync(minimizedDoc.maximizedDocs); - } - if (minimizedDoc && maximizedDocs) { - let minimizedTarget = minimizedDoc; + if (expandedDocs) { if (!CollectionFreeFormDocumentView._undoBatch) { CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); } - maximizedDocs.map(maximizedDoc => { + let isMinimized: boolean | undefined; + expandedDocs.map(d => Doc.GetProto(d)).map(maximizedDoc => { let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); - if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { + if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) { if (isMinimized === undefined) { isMinimized = BoolCast(maximizedDoc.isMinimized, false); } - let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) * this.getTransform().Scale * this.contentScaling() / 2; - let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) * this.getTransform().Scale * this.contentScaling() / 2; - let maxx = NumCast(maximizedDoc.x, undefined); - let maxy = NumCast(maximizedDoc.y, undefined); - let maxw = NumCast(maximizedDoc.width, undefined); - let maxh = NumCast(maximizedDoc.height, undefined); - if (minx !== undefined && miny !== undefined && maxx !== undefined && maxy !== undefined && - maxw !== undefined && maxh !== undefined) { - let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny); - if (isMinimized) maximizedDoc.isMinimized = false; - maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], maxx, maxy, maxw, maxh, Date.now(), isMinimized ? 1 : 0]) - } + maximizedDoc.willMaximize = isMinimized; + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); } }); setTimeout(() => { @@ -166,66 +113,39 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF }, 500); } } - static _undoBatch?: UndoManager.Batch = undefined; - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - if (e.button === 0 && e.altKey) { - e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started - } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document - } - onClick = async (e: React.MouseEvent) => { - e.stopPropagation(); - let altKey = e.altKey; - if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && - Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { - let isExpander = (e.target as any).id === "isExpander"; - if (BoolCast(this.props.Document.isButton, false) || isExpander) { - let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); - let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); - let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); - let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []); - let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []); - let expandedDocs: Doc[] = []; - expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; - expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; - expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; - // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), - // ...(maximizedDocs ? maximizedDocs : []), - // ...(summarizedDocs ? summarizedDocs : []),]; - if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents - let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0].proto!); - if (!hasView && ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight))) { - let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); - if (dataDocs) { - SelectionManager.DeselectAll(); - expandedDocs.forEach(maxDoc => { - maxDoc.isMinimized = false; - if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { - CollectionDockingView.Instance.AddRightSplit(maxDoc.proto ? Doc.MakeDelegate(maxDoc.proto) : maxDoc); - } - }); - } - } else { - this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); - this.toggleIcon(expandedDocs); - } - } - else if (linkedToDocs.length || linkedFromDocs.length) { - let linkedFwdDocs = [ - linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], - linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; - if (linkedFwdDocs) { - DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0]); - } + + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) { + + setTimeout(() => { + let now = Date.now(); + let progress = Math.min(1, (now - stime) / 200); + let pval = maximizing ? + [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : + [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; + this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + this.props.Document.x = pval[0]; + this.props.Document.y = pval[1]; + if (first) { + this.props.Document.proto!.willMaximize = false; + } + if (now < stime + 200) { + this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); + } + else { + if (!maximizing) { + this.props.Document.proto!.isMinimized = true; + this.props.Document.x = targ[0]; + this.props.Document.y = targ[1]; + this.props.Document.width = width; + this.props.Document.height = height; } + this.props.Document.proto!.isIconAnimating = undefined; } - } + }, + 2); } - onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; - onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; - borderRounding = () => { let br = NumCast(this.props.Document.borderRounding); return br >= 0 ? br : @@ -238,27 +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 ? Math.sqrt(Math.sqrt(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} onPointerOver={this.onPointerEnter} - onClick={this.onClick} style={{ - outlineColor: "black", - outlineStyle: "dashed", - outlineWidth: - BoolCast(this.props.Document.protoBrush, false) ? `${1 / this.contentScaling()}px` : - BoolCast(this.props.Document.libraryBrush, false) ? `${0.5 / this.contentScaling()}px` : "0px", opacity: zoomFade, borderRadius: `${this.borderRounding()}px`, transformOrigin: "left top", @@ -267,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 f3d76c49b..02396c3af 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -21,7 +21,7 @@ import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox"; import React = require("react"); import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; -import { Cast, StrCast } from "../../../new_fields/Types"; +import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -44,12 +44,19 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { isSelected: () => boolean, select: (ctrl: boolean) => void, layoutKey: string, + hideOnLeave?: boolean }> { @computed get layout(): string { - return StrCast(this.props.Document[this.props.layoutKey], - this.props.Document.data ? + 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" : "")); + KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : ""); + } else if (typeof layout === "string") { + return layout; + } else { + return "<p>Loading layout</p>"; + } } CreateBindings(): JsxBindings { @@ -76,12 +83,17 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & { (this.props.layoutKey === "layout" && StrCast(this.props.Document.layout).indexOf("CollectionView") === -1)) { this.templates.forEach(template => { let self = this; + // this scales constants in the markup by the scaling applied to the document, but caps the constants to be smaller + // than the width/height of the containing document function convertConstantsToNative(match: string, offset: number, x: string) { let px = Number(match.replace("px", "")); - return `${px * self.props.ScreenToLocalTransform().Scale}px`; + return `${Math.min(NumCast(self.props.Document.height, 0), + Math.min(NumCast(self.props.Document.width, 0), + px * self.props.ScreenToLocalTransform().Scale))}px`; } - let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative); - layout = nativizedTemplate.replace("{layout}", base); + // let nativizedTemplate = template.replace(/([0-9]+)px/g, convertConstantsToNative); + // layout = nativizedTemplate.replace("{layout}", base); + layout = template.replace("{layout}", base); base = layout; }); } diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 260630216..19f5c7d36 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,9 +1,19 @@ -import { action, computed, runInAction, reaction, IReactionDisposer } 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, trace, observable } 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, PromiseValue } 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,21 +22,30 @@ import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; import { CollectionView } from "../collections/CollectionView"; import { ContextMenu } from "../ContextMenu"; +import { DocComponent } from "../DocComponent"; +import { PresentationView } from "../PresentationView"; import { Template } from "./../Templates"; import { DocumentContentsView } from "./DocumentContentsView"; import "./DocumentView.scss"; import React = require("react"); -import { Opt, Doc, WidthSym, HeightSym, DocListCastAsync, DocListCast } from "../../../new_fields/Doc"; -import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { FieldValue, StrCast, BoolCast, Cast } from "../../../new_fields/Types"; -import { List } from "../../../new_fields/List"; -import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; -import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { DocServer } from "../../DocServer"; -import { Id } from "../../../new_fields/RefField"; -import { PresentationView } from "../PresentationView"; -import { SearchUtil } from "../../util/SearchUtil"; +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", @@ -42,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({ @@ -84,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; @@ -100,6 +123,12 @@ 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(); + constructor(props: DocumentViewProps) { + super(props); + this.selectOnLoad = props.selectOnLoad; + } + + _reactionDisposer?: IReactionDisposer; @action componentDidMount() { @@ -109,7 +138,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes - this._reactionDisposer = reaction(() => [this.props.Document.maximizedDocs, this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""], + 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) { @@ -162,31 +192,100 @@ 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().scale(this.props.ContentScaling()).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().scale(this.props.ContentScaling()).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 linkedFwdContextDocs = [ + linkedToDocs.length ? await (linkedToDocs[0].linkedToContext) as Doc : linkedFromDocs.length ? await PromiseValue(linkedFromDocs[0].linkedFromContext) as Doc : undefined, + linkedFromDocs.length ? await (linkedFromDocs[0].linkedFromContext) as Doc : linkedToDocs.length ? await PromiseValue(linkedToDocs[0].linkedToContext) as Doc : undefined]; + + 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], linkedFwdContextDocs[altKey ? 1 : 0]); + } + } + } } } - _hitExpander = false; onPointerDown = (e: React.PointerEvent): void => { this._downX = e.clientX; this._downY = e.clientY; - if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { - return; - } this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0; - if (e.shiftKey && e.buttons === 1) { - if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey ? "alias" : undefined, this._hitExpander); - } else if (this.props.Document) { - CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); - } + if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) { + CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e); e.stopPropagation(); - } else if (this.active) { - //e.stopPropagation(); // bcz: doing this will block click events from CollectionFreeFormDocumentView which are needed for iconifying,etc + } 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); @@ -194,11 +293,11 @@ 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)) { + if (!e.altKey && !this.topMost && e.buttons === 1) { this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander); } } @@ -209,29 +308,26 @@ 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, { title: this.props.Document.title + ".kvp", width: 300, height: 300 }); - CollectionDockingView.Instance.AddRightSplit(kvp); - } - makeButton = (e: React.MouseEvent): void => { - let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + 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 (doc.isButton && !doc.nativeWidth) { - doc.nativeWidth = this.props.Document[WidthSym](); - doc.nativeHeight = this.props.Document[HeightSym](); + 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(); } @@ -242,20 +338,22 @@ 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; - if (de.mods == "Control") { + e.stopPropagation(); + 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; + dst.data = (src.data! as ObjectField)[Copy](); dst.nativeWidth = src.nativeWidth; dst.nativeHeight = src.nativeHeight; } else { - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); + // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true); + // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView); + DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined); de.data.droppedDocuments.push(destDoc); } - e.stopPropagation(); } } @@ -288,6 +386,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu this.templates = this.templates; } + freezeNativeDimensions = (e: React.MouseEvent): void => { + let proto = Doc.GetProto(this.props.Document); + if (proto.ignoreAspect === undefined && !proto.nativeWidth) { + proto.nativeWidth = this.props.PanelWidth(); + proto.nativeHeight = this.props.PanelHeight(); + proto.ignoreAspect = true; + } + proto.ignoreAspect = !BoolCast(proto.ignoreAspect, false); + } + @action onContextMenu = (e: React.MouseEvent): void => { e.stopPropagation(); @@ -299,21 +407,27 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu e.preventDefault(); const cm = ContextMenu.Instance; - cm.addItem({ description: "Full Screen", event: this.fullScreenClicked }); - cm.addItem({ description: this.props.Document.isButton ? "Remove Button" : "Make Button", event: this.makeButton }); - cm.addItem({ description: "Fields", event: this.fieldsClicked }); - cm.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); - cm.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); + 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: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", 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); - CollectionDockingView.Instance.AddRightSplit(Docs.SchemaDocument(["title"], aliases, {})); - } + this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight"); + }, icon: "search" }); - cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])) }); - cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) }); - cm.addItem({ description: "Pin to Presentation", event: () => PresentationView.Instance.PinDoc(this.props.Document) }); - cm.addItem({ description: "Delete", event: this.deleteClicked }); + 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(); @@ -324,22 +438,34 @@ 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); + @action select = (ctrlPressed: boolean) => { this.selectOnLoad = false; SelectionManager.SelectDoc(this, ctrlPressed); } + @observable selectOnLoad: boolean = false; @computed get nativeWidth() { return this.Document.nativeWidth || 0; } @computed get nativeHeight() { return this.Document.nativeHeight || 0; } - @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); } + @computed get contents() { + return ( + <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.selectOnLoad} layoutKey={"layout"} />); + } render() { var scaling = this.props.ContentScaling(); - var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : (StrCast(this.props.Document.layout).indexOf("IconBox") === -1 ? "100%" : "auto"); + 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, @@ -347,6 +473,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 34b6c5e70..5a83de8e3 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -1,24 +1,23 @@ import React = require("react"); +import { computed } from "mobx"; import { observer } from "mobx-react"; -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 { DateField } from "../../../new_fields/DateField"; +import { Doc, FieldResult, Opt } from "../../../new_fields/Doc"; +import { IconField } from "../../../new_fields/IconField"; +import { List } from "../../../new_fields/List"; +import { RichTextField } from "../../../new_fields/RichTextField"; +import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField"; +import { emptyFunction, returnFalse, returnOne } from "../../../Utils"; import { Transform } from "../../util/Transform"; -import { returnFalse, emptyFunction, returnOne } from "../../../Utils"; -import { CollectionView } from "../collections/CollectionView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; +import { CollectionView } from "../collections/CollectionView"; +import { AudioBox } from "./AudioBox"; +import { DocumentContentsView } from "./DocumentContentsView"; +import { FormattedTextBox } from "./FormattedTextBox"; import { IconBox } from "./IconBox"; -import { Opt, Doc, FieldResult } from "../../../new_fields/Doc"; -import { List } from "../../../new_fields/List"; -import { ImageField, VideoField, AudioField } from "../../../new_fields/URLField"; -import { IconField } from "../../../new_fields/IconField"; -import { RichTextField } from "../../../new_fields/RichTextField"; -import { DateField } from "../../../new_fields/DateField"; -import { NumCast } from "../../../new_fields/Types"; +import { ImageBox } from "./ImageBox"; +import { VideoBox } from "./VideoBox"; // @@ -35,6 +34,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; @@ -87,6 +87,7 @@ export class FieldView extends React.Component<FieldViewProps> { return ( <DocumentContentsView Document={field} addDocument={undefined} + addDocTab={this.props.addDocTab} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} ContentScaling={returnOne} @@ -100,7 +101,6 @@ export class FieldView extends React.Component<FieldViewProps> { layoutKey={"layout"} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} - toggleMinimized={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} bringToFront={emptyFunction} /> ); @@ -115,7 +115,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.tsx b/src/client/views/nodes/FormattedTextBox.tsx index c2aed652d..7dcacd19b 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,3 +1,5 @@ +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons'; import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; @@ -5,11 +7,12 @@ import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; import { EditorState, Plugin, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; -import { Doc, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; +import { Doc, Opt } 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 { DocUtils, Docs } from '../../documents/Documents'; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; @@ -19,15 +22,17 @@ 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 { 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 { View } from "@react-pdf/renderer"; -import { NodeType } from "prosemirror-model"; +import { Id } from '../../../new_fields/FieldSymbols'; +import { MainOverlayTextBox } from '../MainOverlayTextBox'; +import { NodeType } from 'prosemirror-model'; + +library.add(faEdit); +library.add(faSmile); // FormattedTextBox: Displays an editable plain text node that maps to a specified Key of a Document // @@ -46,8 +51,11 @@ import { NodeType } from "prosemirror-model"; // this will edit the document and assign the new value to that field. //] -export interface FormattedTextBoxOverlay { +export interface FormattedTextBoxProps { isOverlay?: boolean; + hideOnLeave?: boolean; + height?: string; + color?: string; } const richTextSchema = createSchema({ @@ -58,36 +66,58 @@ 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); } private _ref: React.RefObject<HTMLDivElement>; - private _proseRef: React.RefObject<HTMLDivElement>; + private _proseRef?: HTMLDivElement; private _editorView: Opt<EditorView>; - private _gotDown: boolean = false; - private _dropDisposer?: DragManager.DragDropDisposer; + private _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + private _lastState: any = undefined; + private _applyingChange: boolean = false; + private _linkClicked = ""; private _reactionDisposer: Opt<IReactionDisposer>; private _inputReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; + private dropDisposer?: DragManager.DragDropDisposer; public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } + @observable _entered = false; @observable public static InputBoxOverlay?: FormattedTextBox = undefined; public static InputBoxOverlayScroll: number = 0; + public static IsFragment(html: string) { + return html.indexOf("data-pm-slice") !== -1; + } + public static GetHref(html: string): string { + let parser = new DOMParser(); + let parsedHtml = parser.parseFromString(html, 'text/html'); + if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && + (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { + return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; + } + return ""; + } + public static GetDocFromUrl(url: string) { + if (url.startsWith(document.location.origin)) { + let start = url.indexOf(window.location.origin); + let path = url.substr(start, url.length - start); + let docid = path.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + return docid; + } + return ""; + } 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; - _lastState: any = undefined; dispatchTransaction = (tx: Transaction) => { if (this._editorView) { const state = this._lastState = this._editorView.state.apply(tx); @@ -106,33 +136,30 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + protected createDropTarget = (ele: HTMLDivElement) => { + this._proseRef = ele; + if (this.dropDisposer) { + this.dropDisposer(); + } + if (ele) { + this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }); + } + } + @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { // We're dealing with a link to a document - if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc = de.data.linkSourceDocument; - let destDoc = this.props.Document; - - const protoDest = destDoc.proto; - const protoSrc = sourceDoc.proto; - Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); - de.data.droppedDocuments.push(destDoc); - e.stopPropagation(); - } else if (de.data instanceof DragManager.EmbedDragData) { + if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) { // We're dealing with an internal document drop let url = de.data.urlField.url.href; let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image; this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url }))); + 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 @@ -160,7 +187,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._editorView) { this._editorView.destroy(); } - this.setupEditor(config, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox + this.setupEditor(config, // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox + MainOverlayTextBox.Instance.TextDoc ? MainOverlayTextBox.Instance.TextDoc : this.props.Document, + MainOverlayTextBox.Instance.TextFieldKey ? MainOverlayTextBox.Instance.TextFieldKey : this.props.fieldKey); } ); } else { @@ -182,27 +211,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe field => field && this._editorView && !this._applyingChange && this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); - this.setupEditor(config, this.props.Document); + this.setupEditor(config, this.props.Document, this.props.fieldKey); } - private setupEditor(config: any, doc?: Doc) { - let field = doc ? Cast(doc[this.props.fieldKey], RichTextField) : undefined; - if (this._proseRef.current) { - this._editorView = new EditorView(this._proseRef.current, { + private setupEditor(config: any, doc: Doc, fieldKey: string) { + let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; + let startup = StrCast(doc.documentText); + startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; + if (!startup && !field && doc) { + startup = StrCast(doc[fieldKey]); + } + if (this._proseRef) { + this._editorView = new EditorView(this._proseRef, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), dispatchTransaction: this.dispatchTransaction, nodeViews: { image(node, view, getPos) { return new ImageResizeView(node, view, getPos); } } }); - let text = StrCast(this.props.Document.documentText); - if (text.startsWith("@@@")) { - this.props.Document.proto!.documentText = undefined; - this._editorView.dispatch(this._editorView.state.tr.insertText(text.replace("@@@", ""))); + if (startup) { + Doc.GetProto(doc).documentText = undefined; + this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); } } if (this.props.selectOnLoad) { + console.log("Sel on load " + this.props.Document.title + " " + doc!.title); this.props.select(false); this._editorView!.focus(); } @@ -221,9 +255,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._proxyReactionDisposer) { this._proxyReactionDisposer(); } - if (this._dropDisposer) { - this._dropDisposer(); - } } onPointerDown = (e: React.PointerEvent): void => { @@ -233,27 +264,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._toolTipTextMenu.tooltip.style.opacity = "0"; } } - if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey) { - if (e.target && (e.target as any).href) { - let href = (e.target as any).href; + this._linkClicked = ""; + 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(action((f: Opt<Field>) => { - if (f instanceof Doc) { - if (DocumentManager.Instance.getDocumentView(f)) { - DocumentManager.Instance.getDocumentView(f)!.props.focus(f); - } - else CollectionDockingView.Instance.AddRightSplit(f); - } - })); + this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0]; + } else { + let webDoc = Docs.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) }); + this.props.addDocument && this.props.addDocument(webDoc); + this._linkClicked = webDoc[Id]; } e.stopPropagation(); e.preventDefault(); } - } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { - this._gotDown = true; e.preventDefault(); } } @@ -261,6 +290,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._toolTipTextMenu && this._toolTipTextMenu.tooltip) { this._toolTipTextMenu.tooltip.style.opacity = "1"; } + let ctrlKey = e.ctrlKey; + if (this._linkClicked) { + DocServer.GetRefField(this._linkClicked).then(f => { + (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab")); + }); + e.stopPropagation(); + e.preventDefault(); + } if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { e.stopPropagation(); } @@ -276,29 +313,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } } - - //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 - }); - } - onPointerWheel = (e: React.WheelEvent): void => { if (this.props.isSelected()) { e.stopPropagation(); @@ -306,7 +320,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } onClick = (e: React.MouseEvent): void => { - this._proseRef.current!.focus(); + this._proseRef!.focus(); + if (this._linkClicked) { + e.preventDefault(); + e.stopPropagation(); + } } onMouseDown = (e: React.MouseEvent): void => { if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself @@ -324,7 +342,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } - _toolTipTextMenu: TooltipTextMenu | undefined = undefined; tooltipLinkingMenuPlugin() { let myprops = this.props; return new Plugin({ @@ -359,6 +376,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe this._undoTyping = UndoManager.StartBatch("undoTyping"); } } + + @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" : ""; @@ -366,9 +392,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe return ( <div className={`formattedTextBox-cont-${style}`} ref={this._ref} style={{ + height: this.props.height ? this.props.height : undefined, + 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.color ? this.props.color : this.props.hideOnLeave ? "white" : "initial", pointerEvents: interactive ? "all" : "none", }} - onKeyDown={this.onKeyPress} + // onKeyDown={this.onKeyPress} onKeyPress={this.onKeyPress} onFocus={this.onFocused} onClick={this.onClick} @@ -376,11 +406,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe 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 && !this.props.isSelected() ? "none" : "all" }} ref={this._proseRef} /> + <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap", pointerEvents: this.props.Document.isButton && !this.props.isSelected() ? "none" : "all" }} /> </div> ); } diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss index 893dc2d36..488681027 100644 --- a/src/client/views/nodes/IconBox.scss +++ b/src/client/views/nodes/IconBox.scss @@ -10,6 +10,7 @@ pointer-events: all; svg { width: $MINIMIZED_ICON_SIZE !important; + height: $MINIMIZED_ICON_SIZE !important; height: auto; background: white; } diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx index b42eb44a5..00021bc78 100644 --- a/src/client/views/nodes/IconBox.tsx +++ b/src/client/views/nodes/IconBox.tsx @@ -12,7 +12,6 @@ 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); 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 6472ae711..0d19508fa 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,23 +1,30 @@ - -import { action, observable, trace } from 'mobx'; +import { library } from '@fortawesome/fontawesome-svg-core'; +import { faImage } from '@fortawesome/free-solid-svg-icons'; +import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import Lightbox from 'react-image-lightbox'; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app +import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc'; +import { List } from '../../../new_fields/List'; +import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema'; +import { Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types'; +import { ImageField } from '../../../new_fields/URLField'; import { Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { undoBatch } from '../../util/UndoManager'; import { ContextMenu } from "../../views/ContextMenu"; +import { ContextMenuProps } from '../ContextMenuItem'; +import { DocComponent } from '../DocComponent'; +import { InkingControl } from '../InkingControl'; +import { positionSchema } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; -import { DocComponent } from '../DocComponent'; -import { positionSchema } from './DocumentView'; -import { FieldValue, Cast, StrCast } from '../../../new_fields/Types'; -import { ImageField } from '../../../new_fields/URLField'; -import { List } from '../../../new_fields/List'; -import { InkingControl } from '../InkingControl'; -import { Doc } from '../../../new_fields/Doc'; +var path = require('path'); + + +library.add(faImage); + export const pageSchema = createSchema({ curPage: "number" @@ -38,16 +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; - console.log("title: " + this.Document.title); - if (this._photoIndex === 0) { - Doc.SetOnPrototype(this.Document, "nativeHeight", FieldValue(this.Document.nativeWidth, 0) * h / w); - this.Document.height = FieldValue(this.Document.width, 0) * h / w; - } - } protected createDropTarget = (ele: HTMLDivElement) => { @@ -89,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 => { @@ -132,11 +132,23 @@ 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(() => { + let proto = Doc.GetProto(this.props.Document); + let nw = this.props.Document.nativeWidth; + let nh = this.props.Document.nativeHeight; + let w = this.props.Document.width; + let h = this.props.Document.height; + proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360; + proto.nativeWidth = nh; + proto.nativeHeight = nw; + this.props.Document.width = h; + this.props.Document.height = w; + }), icon: "expand-arrows-alt" }); + ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: subitems }); } } @@ -157,24 +169,67 @@ 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() { - trace(); + // 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 nativeHeight = FieldValue(this.Document.nativeHeight, 0); + 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, (this.props.PanelWidth as any) as string ? Number((this.props.PanelWidth as any) as string) : 50); + // 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 id = this.props.id; // bcz: used to set id = "isExpander" in templates.tsx + 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) ? (nativeHeight - nativeWidth / aspect) / 2 : 0; return ( - <div id={id} className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> - <img id={id} src={paths[Math.min(paths.length, this._photoIndex)]} - style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} + <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} - onLoad={this.onLoad} /> + 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 c9d665ceb..917be734d 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -2,13 +2,14 @@ import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { CompileScript } from "../../util/Scripting"; +import { CompileScript, ScriptOptions } from "../../util/Scripting"; import { FieldView, FieldViewProps } from './FieldView'; 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"; +import { ComputedField } from "../../../fields/ScriptField"; @observer export class KeyValueBox extends React.Component<FieldViewProps> { @@ -18,7 +19,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 } + get fieldDocToLayout() { return this.props.fieldKey ? FieldValue(Cast(this.props.Document[this.props.fieldKey], Doc)) : this.props.Document; } constructor(props: FieldViewProps) { super(props); @@ -27,28 +28,38 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @action onEnterKey = (e: React.KeyboardEvent): void => { if (e.key === 'Enter') { - if (this._keyInput && this._valueInput) { - let doc = this.fieldDocToLayout; - if (!doc) { - return; + if (this._keyInput && this._valueInput && this.fieldDocToLayout) { + if (KeyValueBox.SetField(this.fieldDocToLayout, this._keyInput, this._valueInput)) { + this._keyInput = ""; + this._valueInput = ""; } - let realDoc = doc; - - let script = CompileScript(this._valueInput, { addReturn: true }); - if (!script.compiled) { - return; - } - let res = script.run(); - if (!res.success) return; - const field = res.result; - if (IsField(field)) { - realDoc[this._keyInput] = field; - } - this._keyInput = ""; - this._valueInput = ""; } } } + public static SetField(doc: Doc, key: string, value: string) { + let eq = value.startsWith("="); + value = eq ? value.substr(1) : value; + let dubEq = value.startsWith(":="); + value = dubEq ? value.substr(2) : value; + let options: ScriptOptions = { addReturn: true }; + if (dubEq) options.typecheck = false; + let script = CompileScript(value, options); + if (!script.compiled) { + return false; + } + let field = new ComputedField(script); + if (!dubEq) { + let res = script.run(); + if (!res.success) return false; + field = res.result; + } + if (Field.IsField(field, true)) { + let target = !eq ? doc : Doc.GetProto(doc); + target[key] = field; + return true; + } + return false; + } onPointerDown = (e: React.PointerEvent): void => { if (e.buttons === 1 && this.props.isSelected()) { @@ -78,7 +89,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.tsx b/src/client/views/nodes/KeyValuePair.tsx index 5de660d57..e8bc17532 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -1,16 +1,18 @@ import { action, observable } from 'mobx'; import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app -import { emptyFunction, returnFalse, returnZero } from '../../../Utils'; -import { CompileScript } from "../../util/Scripting"; +import { emptyFunction, returnFalse, returnZero, returnTrue } from '../../../Utils'; +import { CompileScript, CompiledScript, ScriptOptions } from "../../util/Scripting"; import { Transform } from '../../util/Transform'; import { EditableView } from "../EditableView"; 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'; +import { ComputedField } from '../../../fields/ScriptField'; +import { KeyValueBox } from './KeyValueBox'; // Represents one row in a key value plane @@ -38,6 +40,7 @@ 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 + ")"; @@ -46,8 +49,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}> <div className="keyValuePair-td-key-container"> <button className="keyValuePair-td-key-delete" onClick={() => { - if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) + if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { props.Document[props.fieldKey] = undefined; + } else props.Document.proto![props.fieldKey] = undefined; }}> X @@ -59,27 +63,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { <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 ""; }} - SetValue={(value: string) => { - let script = CompileScript(value, { addReturn: true }); - if (!script.compiled) { - return false; - } - let res = script.run(); - if (!res.success) return false; - const field = res.result; - if (IsField(field)) { - props.Document[props.fieldKey] = field; - return true; - } - return false; - }}> + SetValue={(value: string) => + KeyValueBox.SetField(props.Document, props.fieldKey, value)}> </EditableView></td> </tr> ); diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 611cb66b6..68b692aad 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -31,7 +31,7 @@ export class LinkBox extends React.Component<Props> { @undoBatch onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => { e.stopPropagation(); - DocumentManager.Instance.jumpToDocument(this.props.pairedDoc); + DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey); } onEditButtonPressed = (e: React.PointerEvent): void => { diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx index 11117122d..3f09d6214 100644 --- a/src/client/views/nodes/LinkMenu.tsx +++ b/src/client/views/nodes/LinkMenu.tsx @@ -6,9 +6,8 @@ import { LinkEditor } from "./LinkEditor"; import './LinkMenu.scss'; import React = require("react"); import { Doc, DocListCast } from "../../../new_fields/Doc"; -import { Cast, FieldValue } from "../../../new_fields/Types"; -import { listSpec } from "../../../new_fields/Schema"; -import { Id } from "../../../new_fields/RefField"; +import { Cast, FieldValue, StrCast } from "../../../new_fields/Types"; +import { Id } from "../../../new_fields/FieldSymbols"; interface Props { docView: DocumentView; @@ -24,7 +23,7 @@ export class LinkMenu extends React.Component<Props> { return links.map(link => { let doc = FieldValue(Cast(link[key], Doc)); if (doc) { - return <LinkBox key={doc[Id]} linkDoc={link} linkName={Cast(link.title, "string", "")} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; + return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />; } }); } 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 14fe2df80..855c744e6 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, runInAction } 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, @@ -65,19 +68,21 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen @observable private _currAnno: any = []; @observable private _interactive: boolean = false; - @observable private _loaded: boolean = false; @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.active(); this._reactionDisposer = reaction( - () => [SelectionManager.SelectedDocuments().slice()], + () => [this.props.active(), this.curPage], () => { - if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage !== this.thumbnailPage && !this.props.isSelected()) { - this.saveThumbnail(); - this._interactive = true; - } + setTimeout(action(() => { // this forces the active() check to happen after all changes in a transaction have occurred. + if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.active()) { + this.saveThumbnail(); + } + wasSelected = this._interactive = this.props.active(); + }), 0); }, { fireImmediately: true }); @@ -243,20 +248,26 @@ 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 @@ -266,7 +277,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen if (this._perPageInfo.length === 0) { //Makes sure it only runs once this._perPageInfo = [...Array(page._transport.numPages)]; } - this._loaded = true; } @action @@ -283,7 +293,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen } @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() { @@ -291,18 +301,19 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen if (!pdfUrl) { return <p>No pdf url to render</p>; } - let body = NumCast(this.props.Document.nativeHeight) ? - this.pdfPage : + let pdfpage = this.pdfPage; + let body = this.Document.nativeHeight ? + pdfpage : <Measure offset onResize={this.setScaling}> {({ measureRef }) => <div className="pdfBox-page" ref={measureRef}> - {this.pdfPage} + {pdfpage} </div> } </Measure>; - let xf = NumCast(this.Document.nativeHeight) / this.renderHeight; + let xf = (this.Document.nativeHeight || 0) / this.renderHeight; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg ? "svg" : "canvas"}> + <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}> {body} </Document> </div >; @@ -310,35 +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); } @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() { - let classname = "pdfBox-cont"; // + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); + 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.tsx b/src/client/views/nodes/VideoBox.tsx index 96dc884c8..1239b498f 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -1,19 +1,17 @@ import React = require("react"); +import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { FieldView, FieldViewProps } from './FieldView'; import * as rp from "request-promise"; -import "./VideoBox.scss"; -import { action, IReactionDisposer, reaction, observable } from "mobx"; -import { DocComponent } from "../DocComponent"; -import { positionSchema } from "./DocumentView"; import { makeInterface } from "../../../new_fields/Schema"; -import { pageSchema } from "./ImageBox"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Cast, FieldValue } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; -import Measure from "react-measure"; -import "./VideoBox.scss"; import { RouteStore } from "../../../server/RouteStore"; import { DocServer } from "../../DocServer"; +import { DocComponent } from "../DocComponent"; +import { positionSchema } from "./DocumentView"; +import { FieldView, FieldViewProps } from './FieldView'; +import { pageSchema } from "./ImageBox"; +import "./VideoBox.scss"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); @@ -22,37 +20,30 @@ const VideoDocument = makeInterface(positionSchema, pageSchema); export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) { private _reactionDisposer?: IReactionDisposer; private _videoRef: HTMLVideoElement | null = null; - private _loaded: boolean = false; @observable _playTimer?: NodeJS.Timeout = undefined; @observable _fullScreen = false; @observable public Playing: boolean = false; public static LayoutString() { return FieldView.LayoutString(VideoBox); } - public get player(): HTMLVideoElement | undefined { - if (this._videoRef) { - return this._videoRef; - } + public get player(): HTMLVideoElement | null { + return this._videoRef; } - @action - setScaling = (r: any) => { - if (this._loaded) { - // bcz: the nativeHeight should really be set when the document is imported. - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - var nativeHeight = FieldValue(this.Document.nativeHeight, 0); - var newNativeHeight = nativeWidth * r.offset.height / r.offset.width; - if (!nativeHeight && newNativeHeight !== nativeHeight && !isNaN(newNativeHeight)) { - this.Document.height = newNativeHeight / nativeWidth * FieldValue(this.Document.width, 0); - this.Document.nativeHeight = newNativeHeight; - } - } else { - this._loaded = true; + + 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 public Play() { this.Playing = true; if (this.player) this.player.play(); - if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 1000); + if (!this._playTimer) this._playTimer = setInterval(this.updateTimecode, 500); } @action public Pause() { @@ -70,7 +61,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD } @action - updateTimecode = () => this.player && (this.props.Document.curPage = this.player.currentTime); + updateTimecode = () => { + this.player && (this.props.Document.curPage = this.player.currentTime); + } componentDidMount() { if (this.props.setVideoBox) this.props.setVideoBox(this); @@ -87,16 +80,10 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); if (this._reactionDisposer) this._reactionDisposer(); this._reactionDisposer = reaction(() => this.props.Document.curPage, () => - vref.currentTime = NumCast(this.props.Document.curPage, 0), { fireImmediately: true }); + !this.Playing && (vref.currentTime = this.Document.curPage || 0) + , { fireImmediately: true }); } } - videoContent(path: string) { - let style = "videoBox-cont" + (this._fullScreen ? "-fullScreen" : ""); - return <video className={`${style}`} ref={this.setVideoRef} onPointerDown={this.onPointerDown}> - <source src={path} type="video/mp4" /> - Not supported. - </video>; - } getMp4ForVideo(videoId: string = "JN5beCVArMs") { return new Promise(async (resolve, reject) => { @@ -105,32 +92,31 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD 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 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 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) + const videoDetails = stringSubJson.videoDetails; + responseSchema.adaptiveFormats = adaptiveFormats; + responseSchema.videoDetails = videoDetails; + resolve(responseSchema); } catch (err) { console.log(` --- Youtube --- Function: getMp4ForVideo - Error: `, err) - reject(err) + Error: `, err); + reject(err); } - }) + }); } onPointerDown = (e: React.PointerEvent) => { e.preventDefault(); @@ -139,9 +125,6 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD render() { let field = Cast(this.Document[this.props.fieldKey], VideoField); - if (!field) { - return <div>Loading</div>; - } // this.getMp4ForVideo().then((mp4) => { // console.log(mp4); @@ -149,15 +132,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD // console.log("") // }); // // - let content = this.videoContent(field.url.href); - return NumCast(this.props.Document.nativeHeight) ? - content : - <Measure offset onResize={this.setScaling}> - {({ measureRef }) => - <div style={{ width: "100%", height: "auto" }} ref={measureRef}> - {content} - </div> - } - </Measure>; + + 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 diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 2239a8e38..98c57fc75 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,12 +1,11 @@ -import "./WebBox.scss"; -import React = require("react"); -import { FieldViewProps, FieldView } from './FieldView'; +import { observer } from "mobx-react"; import { HtmlField } from "../../../new_fields/HtmlField"; import { WebField } from "../../../new_fields/URLField"; -import { observer } from "mobx-react"; -import { computed, reaction, IReactionDisposer } from 'mobx'; import { DocumentDecorations } from "../DocumentDecorations"; import { InkingControl } from "../InkingControl"; +import { FieldView, FieldViewProps } from './FieldView'; +import "./WebBox.scss"; +import React = require("react"); @observer export class WebBox extends React.Component<FieldViewProps> { |
