diff options
author | Tyler Schicke <tyler_schicke@brown.edu> | 2019-04-25 15:36:32 -0400 |
---|---|---|
committer | Tyler Schicke <tyler_schicke@brown.edu> | 2019-04-25 15:36:32 -0400 |
commit | c318c6cb44b26b780e94e0f0854e5ec4c14634a9 (patch) | |
tree | 5c6bcb413f771988bd89efe3551007bd6d0f160c /src | |
parent | e1a65c5fa52dd094cc48c0c85b3c92ae8789666d (diff) | |
parent | 430dfa3afbfd38fe0db869b962da45903b888264 (diff) |
Merge branch 'master' of github-tsch-brown:browngraphicslab/Dash-Web into newDocs
Diffstat (limited to 'src')
33 files changed, 652 insertions, 536 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 59ff45dc9..c1ad88e2f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -5,6 +5,8 @@ import { Message } from './server/Message'; export class Utils { + public static DRAG_THRESHOLD = 4; + public static GenerateGuid(): string { return v4(); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a5ce7c076..ed76f32ff 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -49,6 +49,8 @@ export interface DocumentOptions { copyDraggedItems?: boolean; backgroundLayout?: string; curPage?: number; + documentText?: string; + borderRounding?: number; // [key: string]: Opt<Field>; } const delegateKeys = ["x", "y", "width", "height", "panX", "panY"]; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 04e2e2917..69964e2c9 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -26,22 +26,23 @@ export class DocumentManager { public getDocumentView(toFind: Doc): DocumentView | null { - let toReturn: DocumentView | null; - toReturn = null; + let toReturn: DocumentView | null = null; //gets document view that is in a freeform canvas collection DocumentManager.Instance.DocumentViews.map(view => { - let doc = view.props.Document; - - if (doc === toFind) { + if (view.props.Document === toFind) { toReturn = view; return; } - let docSrc = FieldValue(doc.proto); - if (docSrc && Object.is(docSrc, toFind)) { - toReturn = view; - } }); + if (!toReturn) { + DocumentManager.Instance.DocumentViews.map(view => { + let doc = view.props.Document.proto; + if (doc && Object.is(doc, toFind)) { + toReturn = view; + } + }); + } return toReturn; } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 46658867b..5aa7ad8e2 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -41,11 +41,11 @@ export function SetupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc: } export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Document) { - let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document); - let draggedDocs = (srcTarg && srcTarg !== FieldWaiting) ? + let srcTarg = sourceDoc.GetPrototype(); + let draggedDocs = srcTarg ? srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc => (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : []; - let draggedFromDocs = (srcTarg && srcTarg !== FieldWaiting) ? + let draggedFromDocs = srcTarg ? srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc => (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : []; draggedDocs.push(...draggedFromDocs); @@ -158,11 +158,13 @@ export namespace DragManager { } export class LinkDragData { - constructor(linkSourceDoc: Document) { + constructor(linkSourceDoc: Document, blacklist: Document[] = []) { this.linkSourceDocument = linkSourceDoc; + this.blacklist = blacklist; } droppedDocuments: Document[] = []; linkSourceDocument: Document; + blacklist: Document[]; [id: string]: any; } @@ -174,6 +176,7 @@ export namespace DragManager { if (!dragDiv) { dragDiv = document.createElement("div"); dragDiv.className = "dragManager-dragDiv"; + dragDiv.style.pointerEvents = "none"; DragManager.Root().appendChild(dragDiv); } MainOverlayTextBox.Instance.SetTextDoc(); diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index a1a22f732..fe5acf4b4 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,9 +1,7 @@ import { observable, action } from "mobx"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { Main } from "../views/Main"; -import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; -import { DragManager } from "./DragManager"; import { Doc } from "../../new_fields/Doc"; +import { MainOverlayTextBox } from "../views/MainOverlayTextBox"; +import { DocumentView } from "../views/nodes/DocumentView"; export namespace SelectionManager { class Manager { @@ -65,7 +63,7 @@ export namespace SelectionManager { export function ReselectAll() { let sdocs = manager.ReselectAll(); - manager.ReselectAll2(sdocs); + setTimeout(() => manager.ReselectAll2(sdocs), 0); } export function SelectedDocuments(): Array<DocumentView> { return manager.SelectedDocuments; diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index f91ca2e06..f7c3e5a7b 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -140,6 +140,7 @@ export namespace UndoManager { } }); + //TODO Make this return the return value export function RunInBatch(fn: () => void, batchName: string) { let batch = StartBatch(batchName); try { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 3b6381ec2..6e361d76a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -1,6 +1,6 @@ -import { action, computed, observable } from "mobx"; +import { action, computed, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { emptyFunction } from "../../Utils"; +import { emptyFunction, Utils } from "../../Utils"; import { DragLinksAsDocuments, DragManager } from "../util/DragManager"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; @@ -11,9 +11,11 @@ import { LinkMenu } from "./nodes/LinkMenu"; import React = require("react"); import { CompileScript } from "../util/Scripting"; import { IconBox } from "./nodes/IconBox"; -import { Cast, FieldValue } from "../../new_fields/Types"; -import { Doc } from "../../new_fields/Doc"; +import { Cast, FieldValue, NumCast, StrCast } from "../../new_fields/Types"; +import { Doc, FieldResult } from "../../new_fields/Doc"; import { listSpec } from "../../new_fields/Schema"; +import { Docs } from "../documents/Documents"; +import { List } from "../../new_fields/List"; const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -21,8 +23,8 @@ export const Flyout = higflyout.default; @observer export class DocumentDecorations extends React.Component<{}, { value: string }> { static Instance: DocumentDecorations; - private _resizer = ""; private _isPointerDown = false; + private _resizing = ""; private keyinput: React.RefObject<HTMLInputElement>; private _documents: DocumentView[] = SelectionManager.SelectedDocuments(); private _resizeBorderWidth = 16; @@ -30,6 +32,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> private _titleHeight = 20; private _linkButton = React.createRef<HTMLDivElement>(); private _linkerButton = React.createRef<HTMLDivElement>(); + private _downX = 0; + private _downY = 0; + @observable private _minimizedX = 0; + @observable private _minimizedY = 0; //@observable private _title: string = this._documents[0].props.Document.Title; @observable private _title: string = this._documents.length > 0 ? Cast(this._documents[0].props.Document.title, "string", "") : ""; @observable private _fieldKey: string = "title"; @@ -37,6 +43,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @observable private _opacity = 1; @observable private _dragging = false; @observable private _iconifying = false; + @observable public Interacting = false; constructor(props: Readonly<{}>) { @@ -125,19 +132,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> @action onBackgroundMove = (e: PointerEvent): void => { let dragDocView = SelectionManager.SelectedDocuments()[0]; - const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document)); + const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top); + dragData.xOffset = xoff; + dragData.yOffset = yoff; dragData.aliasOnDrop = false; - dragData.xOffset = e.x - left; - dragData.yOffset = e.y - top; let move = SelectionManager.SelectedDocuments()[0].props.moveDocument; dragData.moveDocument = move; - this._dragging = true; + this.Interacting = this._dragging = true; document.removeEventListener("pointermove", this.onBackgroundMove); document.removeEventListener("pointerup", this.onBackgroundUp); DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => docView.ContentDiv!), dragData, e.x, e.y, { handlers: { - dragComplete: action(() => this._dragging = false), + dragComplete: action(() => this.Interacting = this._dragging = false), }, hideSource: true }); @@ -176,13 +184,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> document.removeEventListener("pointerup", this.onCloseUp); } } - _downX = 0; - _downY = 0; + @action onMinimizeDown = (e: React.PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { this._downX = e.pageX; this._downY = e.pageY; + let selDoc = SelectionManager.SelectedDocuments()[0]; + let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); + this._minimizedX = selDocPos[0] + 12; + this._minimizedY = selDocPos[1] + 12; document.removeEventListener("pointermove", this.onMinimizeMove); document.addEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); @@ -190,57 +201,99 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } } - @observable _minimizedX = 0; - @observable _minimizedY = 0; @action onMinimizeMove = (e: PointerEvent): void => { e.stopPropagation(); - let dx = e.pageX - this._downX; - let dy = e.pageY - this._downY; - if (Math.abs(dx) > 4 || Math.abs(dy) > 4) { - this._iconifying = true; - let xf = SelectionManager.SelectedDocuments()[0].props.ScreenToLocalTransform().scale(SelectionManager.SelectedDocuments()[0].props.ContentScaling()).inverse().transformPoint(0, 0); - let dx = e.pageX - xf[0]; - let dy = e.pageY - xf[1]; - this._minimizedX = e.clientX; - this._minimizedY = e.clientY; - if (Math.abs(dx) < 20 && Math.abs(dy) < 20) { - this._minimizedX = xf[0]; - this._minimizedY = xf[1]; - } - SelectionManager.SelectedDocuments().map(dv => { - let minDoc = FieldValue(Cast(dv.props.Document.minimizedDoc, Doc)); - if (minDoc) { - let where = (dv.props.ScreenToLocalTransform()).scale(dv.props.ContentScaling()).transformPoint(this._minimizedX, this._minimizedY); - minDoc.x = where[0] + Cast(dv.props.Document.x, "number", 0); - minDoc.y = where[1] + Cast(dv.props.Document.y, "number", 0); - } - }); + if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) { + let selDoc = SelectionManager.SelectedDocuments()[0]; + let selDocPos = selDoc.props.ScreenToLocalTransform().scale(selDoc.props.ContentScaling()).inverse().transformPoint(0, 0); + let snapped = Math.abs(e.pageX - selDocPos[0]) < 20 && Math.abs(e.pageY - selDocPos[1]) < 20; + this._minimizedX = snapped ? selDocPos[0] + 4 : e.clientX; + this._minimizedY = snapped ? selDocPos[1] - 18 : e.clientY; + let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); + Promise.all(selectedDocs.map(selDoc => this.getIconDoc(selDoc))).then(minDocSet => + this.moveIconDocs(SelectionManager.SelectedDocuments()) + ); + this._iconifying = snapped; } } + + + @action createIcon = (docView: DocumentView, layoutString: string): Doc => { + let doc = docView.props.Document; + let iconDoc = Docs.IconDocument(layoutString); + iconDoc.title = "ICON" + StrCast(doc.title); + iconDoc.isMinimized = false; + iconDoc.nativeWidth = 0; + iconDoc.nativeHeight = 0; + iconDoc.x = NumCast(doc.x); + iconDoc.y = NumCast(doc.y) - 24; + iconDoc.proto = doc; + iconDoc.maximizedDoc = doc; + doc.minimizedDoc = iconDoc; + docView.props.addDocument && docView.props.addDocument(iconDoc, false); + return iconDoc; + } + @action + public getIconDoc = async (docView: DocumentView): Promise<Doc | undefined> => { + let doc = docView.props.Document; + + const minDoc = await Cast(doc.minimizedDoc, Doc); + if (minDoc) return minDoc; + + const field = StrCast(doc.backgroundLayout, undefined); + if (field !== undefined) return this.createIcon(docView, field); + + const layout = StrCast(doc.layout, undefined); + if (layout !== undefined) return this.createIcon(docView, field); + + return undefined; + } @action onMinimizeUp = (e: PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { - let dx = e.clientX - this._downX; - let dy = e.clientY - this._downY; - if (Math.abs(dx) < 4 && Math.abs(dy) < 4 && !this._iconifying) { - SelectionManager.SelectedDocuments().map(dv => dv.minimize()); - SelectionManager.DeselectAll(); - } else { - this._minimizedX = this._minimizedY = 0; - } document.removeEventListener("pointermove", this.onMinimizeMove); document.removeEventListener("pointerup", this.onMinimizeUp); + let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd); + Promise.all(selectedDocs.map(selDoc => this.getIconDoc(selDoc))).then(minDocSet => { + let minDocs = minDocSet.filter(minDoc => minDoc instanceof Doc).map(minDoc => minDoc as Doc); + minDocs.map(minDoc => { + minDoc.x = NumCast(minDocs[0].x); + minDoc.y = NumCast(minDocs[0].y); + minDoc.linkTags = new List(minDocs); + if (this._iconifying && selectedDocs[0].props.removeDocument) { + selectedDocs[0].props.removeDocument(minDoc); + (minDoc.maximizedDoc as Doc).minimizedDoc = undefined; + } + }); + runInAction(() => this._minimizedX = this._minimizedY = 0); + if (!this._iconifying) selectedDocs[0].props.toggleMinimized(); + this._iconifying = false; + }); } - this._iconifying = false; + } + moveIconDocs(selViews: DocumentView[], minDocSet?: FieldResult[]) { + selViews.map(selDoc => { + let minDoc = selDoc.props.Document.minimizedDoc; + if (minDoc instanceof Doc) { + let zoom = NumCast(selDoc.props.Document.zoomBasis, 1); + let where = (selDoc.props.ScreenToLocalTransform()).scale(selDoc.props.ContentScaling()).scale(1 / zoom). + transformPoint(this._minimizedX - 12, this._minimizedY - 12); + minDoc.x = where[0] + NumCast(selDoc.props.Document.x); + minDoc.y = where[1] + NumCast(selDoc.props.Document.y); + } + }); } + @action onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); if (e.button === 0) { this._isPointerDown = true; - this._resizer = e.currentTarget.id; + this._resizing = e.currentTarget.id; + this.Interacting = true; document.removeEventListener("pointermove", this.onPointerMove); document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); @@ -265,7 +318,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (this._linkerButton.current !== null) { document.removeEventListener("pointermove", this.onLinkerButtonMoved); document.removeEventListener("pointerup", this.onLinkerButtonUp); - let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0].props.Document); + let selDoc = SelectionManager.SelectedDocuments()[0]; + let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.proto : undefined; + let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []); DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, { handlers: { dragComplete: action(emptyFunction), @@ -309,7 +364,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> let dX = 0, dY = 0, dW = 0, dH = 0; - switch (this._resizer) { + switch (this._resizing) { case "": break; case "documentDecorations-topLeftResizer": @@ -381,8 +436,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> }); } + @action onPointerUp = (e: PointerEvent): void => { e.stopPropagation(); + this._resizing = ""; + this.Interacting = false; + SelectionManager.ReselectAll(); if (e.button === 0) { e.preventDefault(); this._isPointerDown = false; @@ -392,7 +451,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> } getValue = (): string => { - if (this._title === "changed" && this._documents.length > 0) { + if (this._documents.length > 0) { let field = this._documents[0].props.Document[this._fieldKey]; if (typeof field === "string") { return field; @@ -416,17 +475,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }> if (bounds.x === Number.MAX_VALUE || !seldoc) { return (null); } - let selpos = this._minimizedX !== 0 || this._minimizedY !== 0 ? - [this._minimizedX - 12 + (!this._iconifying ? 8 : 0), this._minimizedY - 12 + (!this._iconifying ? 28 : 0)] : - [0, this._iconifying ? -18 : 0]; let minimizeIcon = ( - <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown} - style={{ transform: `translate(${selpos[0]}px,${selpos[1]}px)`, }}> - {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(Cast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "string", "...")) : "..."} + <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}> + {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."} </div>); - if (this._iconifying) { - return (<div className="documentDecorations-container" > {minimizeIcon} </div>); - } if (this.Hidden) { return (null); diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss index 4373534b2..2f899ff28 100644 --- a/src/client/views/Main.scss +++ b/src/client/views/Main.scss @@ -42,7 +42,9 @@ h1 { } .jsx-parser { - width:100% + width:100%; + pointer-events: none; + border-radius: inherit; } p { diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 09ef30f6b..c6b3f06d8 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -17,7 +17,7 @@ import { CurrentUserUtils } from '../../server/authentication/models/current_use import { MessageStore } from '../../server/Message'; import { RouteStore } from '../../server/RouteStore'; import { ServerUtils } from '../../server/ServerUtil'; -import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne } from '../../Utils'; +import { emptyDocFunction, emptyFunction, returnTrue, Utils, returnOne, returnZero } from '../../Utils'; import { Documents } from '../documents/Documents'; import { ColumnAttributeModel } from '../northstar/core/attribute/AttributeModel'; import { AttributeTransformationModel } from '../northstar/core/attribute/AttributeTransformationModel'; @@ -187,6 +187,7 @@ export class Main extends React.Component { <div ref={measureRef} id="mainContent-div"> {!mainCont ? (null) : <DocumentView Document={mainCont} + toggleMinimized={emptyFunction} addDocument={undefined} removeDocument={undefined} ScreenToLocalTransform={Transform.Identity} diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss index 697d68c8c..f6a746e63 100644 --- a/src/client/views/MainOverlayTextBox.scss +++ b/src/client/views/MainOverlayTextBox.scss @@ -7,6 +7,7 @@ overflow: visible; top: 0; left: 0; + pointer-events: none; z-index: $mainTextInput-zindex; .formattedTextBox-cont { background-color: rgba(248, 6, 6, 0.001); diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx index 8cb01117c..be8d67925 100644 --- a/src/client/views/MainOverlayTextBox.tsx +++ b/src/client/views/MainOverlayTextBox.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Document } from '../../fields/Document'; import { Key } from '../../fields/Key'; import { KeyStore } from '../../fields/KeyStore'; -import { emptyDocFunction, emptyFunction, returnTrue } from '../../Utils'; +import { emptyDocFunction, emptyFunction, returnTrue, returnZero } from '../../Utils'; import '../northstar/model/ModelExtensions'; import '../northstar/utils/Extensions'; import { DragManager } from '../util/DragManager'; @@ -21,8 +21,8 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> public static Instance: MainOverlayTextBox; @observable public TextDoc?: Document = undefined; public TextScroll: number = 0; - private _textRect: any; - private _textXf: Transform = Transform.Identity(); + @observable _textRect: any; + @observable _textXf: () => Transform = () => Transform.Identity(); private _textFieldKey: Key = KeyStore.Data; private _textColor: string | null = null; private _textTargetDiv: HTMLDivElement | undefined; @@ -35,14 +35,14 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> } @action - SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: Transform) { + SetTextDoc(textDoc?: Document, textFieldKey?: Key, div?: HTMLDivElement, tx?: () => Transform) { if (this._textTargetDiv) { this._textTargetDiv.style.color = this._textColor; } this.TextDoc = textDoc; this._textFieldKey = textFieldKey!; - this._textXf = tx ? tx : Transform.Identity(); + this._textXf = tx ? tx : () => Transform.Identity(); this._textTargetDiv = div; if (div) { this._textColor = div.style.color; @@ -71,9 +71,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> document.removeEventListener("pointermove", this.textBoxMove); document.removeEventListener('pointerup', this.textBoxUp); let dragData = new DragManager.DocumentDragData([this.TextDoc!]); - const [left, top] = this._textXf - .inverse() - .transformPoint(0, 0); + const [left, top] = this._textXf().inverse().transformPoint(0, 0); dragData.xOffset = e.clientX - left; dragData.yOffset = e.clientY - top; DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, { @@ -91,17 +89,15 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> render() { if (this.TextDoc) { - let x: number = this._textRect.x; - let y: number = this._textRect.y; - let w: number = this._textRect.width; - let h: number = this._textRect.height; - let t = this._textXf.transformPoint(0, 0); - let s = this._textXf.transformPoint(1, 0); - s[0] = Math.sqrt((s[0] - t[0]) * (s[0] - t[0]) + (s[1] - t[1]) * (s[1] - t[1])); - return <div className="mainOverlayTextBox-textInput" style={{ pointerEvents: "none", transform: `translate(${x}px, ${y}px) scale(${1 / s[0]},${1 / s[0]})`, width: "auto", height: "auto" }} > - <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} style={{ pointerEvents: "none", transform: `scale(${1}, ${1})`, width: `${w * s[0]}px`, height: `${h * s[0]}px` }}> + let toScreenXf = this._textXf().inverse(); + let pt = toScreenXf.transformPoint(0, 0); + let s = 1 / this._textXf().Scale; + return <div className="mainOverlayTextBox-textInput" style={{ transform: `translate(${pt[0]}px, ${pt[1]}px) scale(${s},${s})`, width: "auto", height: "auto" }} > + <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll} + style={{ width: `${this.TextDoc.Width()}px`, height: `${this.TextDoc.Height()}px` }}> <FormattedTextBox fieldKey={this._textFieldKey} isOverlay={true} Document={this.TextDoc} isSelected={returnTrue} select={emptyFunction} isTopMost={true} - selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ScreenToLocalTransform={() => this._textXf} focus={emptyDocFunction} /> + selectOnLoad={true} ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} + ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyDocFunction} /> </div> </ div>; } diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index ff8434681..744ec0535 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -7,30 +7,49 @@ import "./PreviewCursor.scss"; @observer export class PreviewCursor extends React.Component<{}> { private _prompt = React.createRef<HTMLDivElement>(); + static _onKeyPress?: (e: KeyboardEvent) => void; + @observable static _clickPoint = [0, 0]; + @observable public static Visible = false; //when focus is lost, this will remove the preview cursor @action onBlur = (): void => { PreviewCursor.Visible = false; - PreviewCursor.hide(); } - @observable static clickPoint = [0, 0]; - @observable public static Visible = false; - @observable public static hide = () => { }; + constructor(props: any) { + super(props); + document.addEventListener("keydown", this.onKeyPress) + } + + @action + onKeyPress = (e: KeyboardEvent) => { + // Mixing events between React and Native is finicky. In FormattedTextBox, we set the + // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore + // the keyPress here. + //if not these keys, make a textbox if preview cursor is active! + if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { + PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); + PreviewCursor.Visible = false; + } else if (e.ctrlKey) { + if (e.key == "v") { + PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e); + } + } + } @action - public static Show(hide: any, x: number, y: number) { - this.clickPoint = [x, y]; - this.hide = hide; + public static Show(x: number, y: number, onKeyPress: (e: KeyboardEvent) => void) { + this._clickPoint = [x, y]; + this._onKeyPress = onKeyPress; setTimeout(action(() => this.Visible = true), (1)); } render() { - if (!PreviewCursor.clickPoint) { + if (!PreviewCursor._clickPoint) { return (null); } if (PreviewCursor.Visible && this._prompt.current) { this._prompt.current.focus(); } return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._prompt} - style={{ transform: `translate(${PreviewCursor.clickPoint[0]}px, ${PreviewCursor.clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}> + style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)`, opacity: PreviewCursor.Visible ? 1 : 0 }}> I </div >; } diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx index 058893198..4807dc40a 100644 --- a/src/client/views/collections/CollectionBaseView.tsx +++ b/src/client/views/collections/CollectionBaseView.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ContextMenu } from '../ContextMenu'; import { FieldViewProps } from '../nodes/FieldView'; -import { Cast, FieldValue, PromiseValue } from '../../../new_fields/Types'; +import { Cast, FieldValue, PromiseValue, NumCast } from '../../../new_fields/Types'; import { Doc, FieldResult, Opt, Id } from '../../../new_fields/Doc'; import { listSpec } from '../../../new_fields/Schema'; import { List } from '../../../new_fields/List'; @@ -87,51 +87,24 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { let props = this.props; var curPage = Cast(props.Document.curPage, "number", -1); Doc.SetOnPrototype(doc, "page", curPage); - if (true || this.isAnnotationOverlay) { - doc.zoom = Cast(this.props.Document.scale, "number", 1); - } if (curPage >= 0) { Doc.SetOnPrototype(doc, "annotationOn", props.Document); } - const data = props.Document[props.fieldKey]; - if (data !== undefined) { + if (!this.createsCycle(doc, props.Document)) { //TODO This won't create the field if it doesn't already exist - const value = Cast(data, listSpec(Doc)); - if (!this.createsCycle(doc, props.Document) && value !== undefined) { + const value = Cast(props.Document[props.fieldKey], listSpec(Doc)); + if (value !== undefined) { if (allowDuplicates || !value.some(v => v.Id === doc.Id)) { value.push(doc); } + } else { + this.props.Document[this.props.fieldKey] = new List([doc]); } - else { - return false; - } - } else { - let proto = FieldValue(props.Document.proto); - if (!proto || !this.createsCycle(proto, doc)) { - const field = new List([doc]); - // const script = CompileScript(` - // if(added) { - // console.log("added " + field.Title + " " + doc.Title); - // } else { - // console.log("removed " + field.Title + " " + doc.Title); - // } - // `, { - // addReturn: false, - // params: { - // field: Document.name, - // added: "boolean" - // }, - // capturedVariables: { - // doc: this.props.Document - // } - // }); - // if (script.compiled) { - // field.addScript(new ScriptField(script)); - // } - Doc.SetOnPrototype(props.Document, props.fieldKey, field); - } - else { - return false; + // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument? + if (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid) { + let zoom = NumCast(this.props.Document.scale, 1); + let screen = this.props.ScreenToLocalTransform().inverse().Scale / (this.props as any).ContentScaling() * zoom; + doc.zoomBasis = screen; } } return true; @@ -186,7 +159,9 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> { }; const viewtype = this.collectionViewType; return ( - <div className={this.props.className || "collectionView-cont"} onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> + <div className={this.props.className || "collectionView-cont"} + style={{ borderRadius: "inherit", pointerEvents: "all" }} + onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}> {viewtype !== undefined ? this.props.children(viewtype, props) : (null)} </div> ); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 86ee7cea9..ec5f823b0 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -5,7 +5,7 @@ import { action, observable, reaction, trace } from "mobx"; import { observer } from "mobx-react"; import * as ReactDOM from 'react-dom'; import Measure from "react-measure"; -import { Utils, returnTrue, emptyFunction, returnOne } from "../../../Utils"; +import { Utils, returnTrue, emptyFunction, returnOne, returnZero } from "../../../Utils"; import { Server } from "../../Server"; import { undoBatch } from "../../util/UndoManager"; import { DocumentView } from "../nodes/DocumentView"; @@ -16,9 +16,10 @@ import { ServerUtils } from "../../../server/ServerUtil"; import { DragManager, DragLinksAsDocuments } from "../../util/DragManager"; import { Transform } from '../../util/Transform'; import { Doc, Id, Opt, Field, FieldId } from "../../../new_fields/Doc"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; import { DocServer } from "../../DocServer"; +import { listSpec } from "../../../new_fields/Schema"; @observer export class CollectionDockingView extends React.Component<SubCollectionViewProps> { @@ -256,8 +257,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (tfield !== undefined) { tab.titleElement[0].textContent = f.Title; } - const lf = await Cast(f.linkedFromDocs, List); - const lt = await Cast(f.linkedToDocs, List); + const lf = await Cast(f.linkedFromDocs, listSpec(Doc)); + const lt = await Cast(f.linkedToDocs, listSpec(Doc)); let count = (lf ? lf.length : 0) + (lt ? lt.length : 0); let counter: any = this.htmlToElement(`<div class="messageCounter">${count}</div>`); tab.element.append(counter); @@ -322,8 +323,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc)); } - nativeWidth = () => Cast(this._document!.nativeWidth, "number", this._panelWidth); - nativeHeight = () => Cast(this._document!.nativeHeight, "number", this._panelHeight); + nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth); + nativeHeight = () => NumCast(this._document!.nativeHeight, this._panelHeight); contentScaling = () => { const nativeH = this.nativeHeight(); const nativeW = this.nativeWidth(); @@ -349,6 +350,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { <div className="collectionDockingView-content" ref={this._mainCont} style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> <DocumentView key={this._document![Id]} Document={this._document!} + toggleMinimized={emptyFunction} addDocument={undefined} removeDocument={undefined} ContentScaling={this.contentScaling} @@ -361,7 +363,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> { whenActiveChanged={emptyFunction} focus={emptyFunction} ContainingCollectionView={undefined} /> - </div>); + </div >); } render() { diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index e556151f9..2e1175f28 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -7,10 +7,9 @@ import { observer } from "mobx-react"; import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table"; import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss' import "react-table/react-table.css"; -import { emptyFunction, returnFalse } from "../../../Utils"; -import { Server } from "../../Server"; +import { emptyFunction, returnFalse, returnZero } from "../../../Utils"; import { SetupDrag } from "../../util/DragManager"; -import { CompileScript, ToField } from "../../util/Scripting"; +import { CompileScript } from "../../util/Scripting"; import { Transform } from "../../util/Transform"; import { COLLECTION_BORDER_WIDTH } from "../../views/globalCssVariables.scss"; import { anchorPoints, Flyout } from "../DocumentDecorations"; @@ -21,7 +20,7 @@ import { FieldView, FieldViewProps } from "../nodes/FieldView"; import "./CollectionSchemaView.scss"; import { CollectionSubView } from "./CollectionSubView"; import { Opt, Field, Doc, Id } from "../../../new_fields/Doc"; -import { Cast, FieldValue } from "../../../new_fields/Types"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; import { listSpec } from "../../../new_fields/Schema"; import { List } from "../../../new_fields/List"; @@ -57,7 +56,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { @observable _keys: string[] = []; @observable _newKeyName: string = ""; - @computed get splitPercentage() { return Cast(this.props.Document.schemaSplitPercentage, "number", 0); } + @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage); } @computed get columns() { return Cast(this.props.Document.columns, listSpec("string"), []); } @computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); } @@ -74,6 +73,8 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { focus: emptyFunction, active: returnFalse, whenActiveChanged: emptyFunction, + PanelHeight: returnZero, + PanelWidth: returnZero, }; let contents = ( <FieldView {...props} /> @@ -256,6 +257,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) { <div className="collectionSchemaView-previewRegion" style={{ width: `${this.previewRegionWidth}px` }}> <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}> <DocumentView Document={this.previewDocument} isTopMost={false} selectOnLoad={false} + toggleMinimized={emptyFunction} addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} ScreenToLocalTransform={this.getPreviewTransform} ContentScaling={this.previewContentScaling} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 642e5aed6..558a8728f 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -4,11 +4,7 @@ import { undoBatch, UndoManager } from "../../util/UndoManager"; import { DragManager } from "../../util/DragManager"; import { Docs, DocumentOptions } from "../../documents/Documents"; import { RouteStore } from "../../../server/RouteStore"; -import { TupleField } from "../../../fields/TupleField"; import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils"; -import { NumberField } from "../../../fields/NumberField"; -import { ServerUtils } from "../../../server/ServerUtil"; -import { Server } from "../../Server"; import { FieldViewProps } from "../nodes/FieldView"; import * as rp from 'request-promise'; import { CollectionView } from "./CollectionView"; @@ -85,7 +81,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (de.data instanceof DragManager.DocumentDragData) { if (de.data.aliasOnDrop || de.data.copyOnDrop) { ["width", "height", "curPage"].map(key => - de.data.draggedDocuments.map((draggedDocument: Document, i: number) => + de.data.draggedDocuments.map((draggedDocument: Doc, i: number) => PromiseValue(Cast(draggedDocument[key], "number")).then(f => f && (de.data.droppedDocuments[i][key] = f)))); } let added = false; @@ -148,7 +144,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { return undefined; } ctor = Docs.WebDocument; - options = { height: options.width, ...options, title: path }; + options = { height: options.width, ...options, title: path, nativeWidth: undefined }; } return ctor ? ctor(path, options) : undefined; } @@ -166,9 +162,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { e.preventDefault(); if (html && html.indexOf("<img") !== 0 && !html.startsWith("<a")) { - console.log("not good"); - let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300 }); - htmlDoc.documentText = text; + let htmlDoc = Docs.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text }); this.props.addDocument(htmlDoc, false); return; } @@ -182,7 +176,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { if (item.kind === "string" && item.type.indexOf("uri") !== -1) { let str: string; let prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve)) - .then(action((s: string) => rp.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + (str = s))))) + .then(action((s: string) => rp.head(DocServer.prepend(RouteStore.corsProxy + "/" + (str = s))))) .then(result => { let type = result.headers["content-type"]; if (type) { @@ -210,15 +204,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) { let path = window.location.origin + file; let docPromise = this.getDocumentFromType(type, path, { ...options, nativeWidth: 600, width: 300, title: dropFileName }); - docPromise.then(async doc => { - let docs = await Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)); - if (!docs) { - this.props.Document[this.props.fieldKey] = docs = new List(); - } - if (doc) { - docs.push(doc); - } - }); + docPromise.then(doc => doc && this.props.addDocument(doc)); })); }); promises.push(prom); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index e0387f4b4..905b48db7 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -136,7 +136,10 @@ export class CollectionTreeView extends CollectionSubView { ); return ( - <div id="body" className="collectionTreeView-dropTarget" onWheel={(e: React.WheelEvent) => e.stopPropagation()} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> + <div id="body" className="collectionTreeView-dropTarget" + style={{ borderRadius: "inherit" }} + onWheel={(e: React.WheelEvent) => e.stopPropagation()} + onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget}> <div className="coll-title"> <EditableView contents={this.props.Document.Title} diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx index 29fb342c6..779dc8fc3 100644 --- a/src/client/views/collections/CollectionVideoView.tsx +++ b/src/client/views/collections/CollectionVideoView.tsx @@ -37,10 +37,13 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { ]); } + _ele: HTMLDivElement | null = null; @action mainCont = (ele: HTMLDivElement | null) => { + this._ele = ele; if (ele) { this._player = ele.getElementsByTagName("video")[0]; + console.log(this._player); if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) { this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1); } @@ -57,9 +60,12 @@ export class CollectionVideoView extends React.Component<FieldViewProps> { @action updateTimecode = () => { + this._player = this._player ? this._player : this._ele ? this._ele.getElementsByTagName("video")[0] : undefined; if (this._player) { - if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero !== -1) { - this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero; + let timecode = (this._player as any).hasOwnProperty("AHackBecauseSomethingResetsTheVideoToZero") ? + (this._player as any).AHackBecauseSomethingResetsTheVideoToZero : -1; + if (timecode !== -1 && Object) { + this._player.currentTime = timecode; (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1; } else { this._currentTimecode = this._player.currentTime; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index cd74d3a84..ebdb0c75c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -18,7 +18,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP componentDidMount() { this._brushReactionDisposer = reaction(() => this.props.Document.GetList(this.props.fieldKey, [] as Document[]).map(doc => doc.GetNumber(KeyStore.X, 0)), () => { - let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]); + let views = this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc.GetText(KeyStore.BackgroundLayout, "").indexOf("istogram") !== -1); for (let i = 0; i < views.length; i++) { for (let j = 0; j < views.length; j++) { let srcDoc = views[j]; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 57706b51e..67a0e532c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -6,6 +6,7 @@ width: 100%; height: 100%; transform-origin: left top; + pointer-events: none; } .collectionfreeformview-container { .collectionfreeformview > .jsx-parser { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 3a9c9780b..80900c450 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,4 +1,4 @@ -import { action, computed, observable, trace } from "mobx"; +import { action, computed } from "mobx"; import { observer } from "mobx-react"; import { emptyFunction, returnFalse, returnOne } from "../../../../Utils"; import { DocumentManager } from "../../../util/DocumentManager"; @@ -8,7 +8,6 @@ import { Transform } from "../../../util/Transform"; import { undoBatch } from "../../../util/UndoManager"; import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"; import { InkingCanvas } from "../../InkingCanvas"; -import { MainOverlayTextBox } from "../../MainOverlayTextBox"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocumentContentsView } from "../../nodes/DocumentContentsView"; import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView"; @@ -21,7 +20,7 @@ import React = require("react"); import v5 = require("uuid/v5"); import { createSchema, makeInterface, listSpec } from "../../../../new_fields/Schema"; import { Doc, Id } from "../../../../new_fields/Doc"; -import { FieldValue, Cast } from "../../../../new_fields/Types"; +import { FieldValue, Cast, NumCast } from "../../../../new_fields/Types"; import { pageSchema } from "../../nodes/ImageBox"; import { List } from "../../../../new_fields/List"; @@ -36,6 +35,7 @@ const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema) @observer export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { + public static RIGHT_BTN_DRAG = false; private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type) private _lastX: number = 0; private _lastY: number = 0; @@ -46,7 +46,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } private get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; } - private childViews = () => this.views; private panX = () => FieldValue(this.Document.panX, 0); private panY = () => FieldValue(this.Document.panY, 0); private zoomScaling = () => FieldValue(this.Document.scale, 1); @@ -60,10 +59,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.addDocument(newBox, false); } private addDocument = (newBox: Doc, allowDuplicates: boolean) => { - newBox.zoom = FieldValue(this.Document.scale, 1); - return this.props.addDocument(this.bringToFront(newBox), false); + this.props.addDocument(newBox, false); + this.bringToFront(newBox); + return true; } - private selectDocuments = (docs: Document[]) => { + private selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll; docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).filter(dv => dv).map(dv => SelectionManager.SelectDoc(dv!, true)); @@ -80,23 +80,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action drop = (e: Event, de: DragManager.DropEvent) => { if (super.drop(e, de) && de.data instanceof DragManager.DocumentDragData) { - const [x, y] = this.getTransform().transformPoint(de.x - de.data.xOffset, de.y - de.data.yOffset); if (de.data.droppedDocuments.length) { let dragDoc = de.data.droppedDocuments[0]; - let dropX = Cast(dragDoc.x, "number", 0); - let dropY = Cast(dragDoc.y, "number", 0); + let zoom = NumCast(dragDoc.zoomBasis, 1); + let [xp, yp] = this.getTransform().transformPoint(de.x, de.y); + let x = xp - de.data.xOffset / zoom; + let y = yp - de.data.yOffset / zoom; + let dropX = NumCast(de.data.droppedDocuments[0].x); + let dropY = NumCast(de.data.droppedDocuments[0].y); de.data.droppedDocuments.map(d => { - d.x = x + Cast(d.x, "number", 0) - dropX; - d.y = y + Cast(d.y, "number", 0) - dropY; - if (!Cast(d.isMinimized, "boolean", false)) { - if (!Cast(d.width, "number", 0)) { - d.width = 300; - } - if (!Cast(d.height, "number", 0)) { - let nw = Cast(d.nativeWidth, "number", 0); - let nh = Cast(d.nativeHeight, "number", 0); - d.height = nw && nh ? nh / nw * Cast(d.width, "number", 0) : 300; - } + d.x = x + NumCast(d.x) - dropX; + d.y = y + NumCast(d.y) - dropY; + if (!NumCast(d.width)) { + d.width = 300; + } + if (!NumCast(d.height)) { + let nw = NumCast(d.nativeWidth); + let nh = NumCast(d.nativeHeight); + d.height = nw && nh ? nh / nw * d.Width() : 300; } this.bringToFront(d); }); @@ -119,20 +120,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { var dv = DocumentManager.Instance.getDocumentView(doc); return childSelected || (dv && SelectionManager.IsSelected(dv) ? true : false); }, false); - if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || (e.button === 0 && e.altKey)) && (childSelected || this.props.active())) { - document.removeEventListener("pointermove", this.onPointerMove); + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && + (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) || + (e.button === 0 && e.altKey)) && (childSelected || this.props.active()))) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && + ((e.button === 0 && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1)) && (childSelected || this.props.active())))) { + this.cleanupInteractions(); document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); document.addEventListener("pointerup", this.onPointerUp); this._lastX = e.pageX; this._lastY = e.pageY; } } - @action onPointerUp = (e: PointerEvent): void => { - e.stopPropagation(); - this.cleanupInteractions(); } @@ -166,7 +167,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { this.setPan(x - dx, y - dy); this._lastX = e.pageX; this._lastY = e.pageY; - e.stopPropagation(); + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); } } @@ -196,10 +197,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { // if (modes[e.deltaMode] === 'pixels') coefficient = 50; // else if (modes[e.deltaMode] === 'lines') coefficient = 1000; // This should correspond to line-height?? let deltaScale = (1 - (e.deltaY / coefficient)); - if (deltaScale < 0) deltaScale = -deltaScale; if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { deltaScale = 1 / this.zoomScaling(); } + if (deltaScale < 0) deltaScale = -deltaScale; let [x, y] = this.getTransform().transformPoint(e.clientX, e.clientY); let localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); @@ -212,7 +213,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { @action setPan(panX: number, panY: number) { - MainOverlayTextBox.Instance.SetTextDoc(); var scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((1 - 1 / scale) * this.nativeHeight, Math.max(0, panY)); @@ -229,13 +229,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { onDragOver = (): void => { } - @action bringToFront(doc: Doc) { - (this.children || []).slice().sort((doc1, doc2) => { + const docs = (this.children || []); + docs.slice().sort((doc1, doc2) => { if (doc1 === doc) return 1; if (doc2 === doc) return -1; - return Cast(doc1.zIndex, "number", 0) - Cast(doc2.zIndex, "number", 0); + return NumCast(doc1.zIndex) - NumCast(doc2.zIndex); }).forEach((doc, index) => doc.zIndex = index + 1); + doc.zIndex = docs.length + 1; return doc; } @@ -249,6 +250,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { getDocumentViewProps(document: Doc): DocumentViewProps { return { Document: document, + toggleMinimized: emptyFunction, addDocument: this.props.addDocument, removeDocument: this.props.removeDocument, moveDocument: this.props.moveDocument, @@ -289,17 +291,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) { super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); } + private childViews = () => [...this.views, <CollectionFreeFormBackgroundView key="backgroundView" {...this.getDocumentViewProps(this.props.Document)} />]; render() { const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`; return ( <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel} + style={{ borderRadius: "inherit" }} onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} > + {/* <svg viewBox="0 0 180 18" style={{ top: "50%", opacity: 0.05, position: "absolute" }}> + <text y="15" > + {this.props.Document.Title} + </text> + </svg> */} <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}> <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> - <CollectionFreeFormBackgroundView {...this.getDocumentViewProps(this.props.Document)} /> <CollectionFreeFormLinksView {...this.props} key="freeformLinks"> <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} > {this.childViews} @@ -356,7 +364,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const panx = -this.props.panX(); const pany = -this.props.panY(); const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire - return <div className="collectionfreeformview" style={{ transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> + return <div className="collectionfreeformview" style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}> {this.props.children} </div>; } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index bf918beba..c6681e014 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -11,9 +11,9 @@ import { undoBatch } from "../../../util/UndoManager"; import { InkingCanvas } from "../../InkingCanvas"; import { PreviewCursor } from "../../PreviewCursor"; import { CollectionFreeFormView } from "./CollectionFreeFormView"; -import { MINIMIZED_ICON_SIZE } from '../../../views/globalCssVariables.scss' import "./MarqueeView.scss"; import React = require("react"); +import { Utils } from "../../../../Utils"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -33,18 +33,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @observable _lastY: number = 0; @observable _downX: number = 0; @observable _downY: number = 0; - @observable _used: boolean = false; @observable _visible: boolean = false; - _showOnUp: boolean = false; - static DRAG_THRESHOLD = 4; @action cleanupInteractions = (all: boolean = false) => { if (all) { document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); - } else { - this._used = true; } document.removeEventListener("keydown", this.marqueeCommand, true); this._visible = false; @@ -52,34 +47,25 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @action onKeyPress = (e: KeyboardEvent) => { - // Mixing events between React and Native is finicky. In FormattedTextBox, we set the - // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore - // the keyPress here. - //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) { - //make textbox and add it to this collection - let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" }); - this.props.addLiveTextDocument(newBox); - PreviewCursor.Visible = false; - e.stopPropagation(); - } - } - hideCursor = () => { - document.removeEventListener("keypress", this.onKeyPress, false); + //make textbox and add it to this collection + let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY); + let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" }); + this.props.addLiveTextDocument(newBox); + e.stopPropagation(); } @action onPointerDown = (e: React.PointerEvent): void => { - if (e.buttons === 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - this._used = false; - this._showOnUp = true; - document.removeEventListener("keypress", this.onKeyPress, false); + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; + PreviewCursor.Visible = false; + if ((CollectionFreeFormView.RIGHT_BTN_DRAG && e.button === 0 && !e.altKey && !e.metaKey && this.props.container.props.active()) || + (!CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && this.props.container.props.active())) { document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); document.addEventListener("keydown", this.marqueeCommand, true); } + if (e.altKey) + e.preventDefault(); } @action @@ -87,33 +73,41 @@ export class MarqueeView extends React.Component<MarqueeViewProps> this._lastX = e.pageX; this._lastY = e.pageY; if (!e.cancelBubble) { - if (Math.abs(this._downX - e.clientX) > 4 || Math.abs(this._downY - e.clientY) > 4) { - this._showOnUp = false; - PreviewCursor.Visible = false; - } - if (!this._used && e.buttons === 1 && !e.altKey && !e.metaKey && - (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) { + if (Math.abs(this._lastX - this._downX) > Utils.DRAG_THRESHOLD || + Math.abs(this._lastY - this._downY) > Utils.DRAG_THRESHOLD) { this._visible = true; + e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } + if (e.altKey) + e.preventDefault(); } @action onPointerUp = (e: PointerEvent): void => { - this.cleanupInteractions(true); - this._visible = false; - if (this._showOnUp) { - PreviewCursor.Show(this.hideCursor, this._downX, this._downY); - document.addEventListener("keypress", this.onKeyPress, false); - } else { + if (this._visible) { let mselect = this.marqueeSelect(); if (!e.shiftKey) { SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document); } this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]); } + this.cleanupInteractions(true); + if (e.altKey) + e.preventDefault(); + } + + @action + onClick = (e: React.MouseEvent): void => { + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress); + // let the DocumentView stopPropagation of this event when it selects this document + } else { // why do we get a click event when the cursor have moved a big distance? + // let's cut it off here so no one else has to deal with it. + e.stopPropagation(); + } } intersectRect(r1: { left: number, top: number, width: number, height: number }, @@ -133,15 +127,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps> @undoBatch @action marqueeCommand = (e: KeyboardEvent) => { - if (e.key === "Backspace" || e.key === "Delete") { + if (e.key === "Backspace" || e.key === "Delete" || e.key == "d") { this.marqueeSelect().map(d => this.props.removeDocument(d)); let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); if (ink && ink !== FieldWaiting) { this.marqueeInkDelete(ink.Data); } - this.cleanupInteractions(); + this.cleanupInteractions(true); + e.stopPropagation(); } - if (e.key === "c") { + if (e.key === "c" || e.key === "r" || e.key === "e") { + e.stopPropagation(); let bounds = this.Bounds; let selected = this.marqueeSelect().map(d => { this.props.removeDocument(d); @@ -152,22 +148,45 @@ export class MarqueeView extends React.Component<MarqueeViewProps> }); let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField); let inkData = ink && ink !== FieldWaiting ? ink.Data : undefined; - //setTimeout(() => { + let zoomBasis = this.props.container.props.Document.GetNumber(KeyStore.Scale, 1); let newCollection = Documents.FreeformDocument(selected, { x: bounds.left, y: bounds.top, panx: 0, pany: 0, - width: bounds.width, - height: bounds.height, + borderRounding: e.key === "e" ? -1 : undefined, + backgroundColor: selected.length ? "white" : "", + scale: zoomBasis, + width: bounds.width * zoomBasis, + height: bounds.height * zoomBasis, ink: inkData ? this.marqueeInkSelect(inkData) : undefined, title: "a nested collection" }); - this.props.addDocument(newCollection, false); + this.marqueeInkDelete(inkData); - // }, 100); - this.cleanupInteractions(); SelectionManager.DeselectAll(); + if (e.key === "r") { + let summary = Documents.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + summary.GetPrototype()!.CreateLink(newCollection.GetPrototype()!); + this.props.addLiveTextDocument(summary); + e.preventDefault(); + } + else { + this.props.addDocument(newCollection, false); + } + this.cleanupInteractions(true); + } + if (e.key === "s") { + e.stopPropagation(); + e.preventDefault(); + let bounds = this.Bounds; + let selected = this.marqueeSelect(); + SelectionManager.DeselectAll(); + let summary = Documents.TextDocument({ x: bounds.left + bounds.width + 25, y: bounds.top, width: 300, height: 100, backgroundColor: "yellow", title: "-summary-" }); + this.props.addLiveTextDocument(summary); + selected.map(select => summary.GetPrototype()!.CreateLink(select.GetPrototype()!)); + + this.cleanupInteractions(true); } } @action @@ -208,7 +227,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> let selRect = this.Bounds; let selection: Document[] = []; this.props.activeDocuments().map(doc => { - var z = doc.GetNumber(KeyStore.Zoom, 1); + var z = doc.GetNumber(KeyStore.ZoomBasis, 1); var x = doc.GetNumber(KeyStore.X, 0); var y = doc.GetNumber(KeyStore.Y, 0); var w = doc.Width() / z; @@ -230,7 +249,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps> } render() { - return <div className="marqueeView" onPointerDown={this.onPointerDown}> + return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}> {this.props.children} {!this._visible ? (null) : this.marqueeDiv} </div>; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 7b7b7e65e..376af0b36 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,13 +1,16 @@ -import { computed, trace } from "mobx"; +import { computed, trace, action } from "mobx"; import { observer } from "mobx-react"; import { Transform } from "../../util/Transform"; import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); -import { OmitKeys } from "../../../Utils"; import { DocComponent } from "../DocComponent"; -import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { FieldValue } from "../../../new_fields/Types"; +import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; +import { FieldValue, Cast, NumCast } from "../../../new_fields/Types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { SelectionManager } from "../../util/SelectionManager"; +import { matchedData } from "express-validator/filter"; +import { Doc } from "../../../new_fields/Doc"; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { } @@ -23,18 +26,21 @@ 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; - @computed - get transform(): string { + @computed get transform() { return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `; } + @computed get X() { return FieldValue(this.Document.x, 0); } + @computed get Y() { return FieldValue(this.Document.y, 0); } @computed get zoom(): number { return 1 / FieldValue(this.Document.zoom, 1); } - @computed get zIndex(): number { return FieldValue(this.Document.zIndex, 0); } - @computed get width(): number { return FieldValue(this.Document.width, 0); } - @computed get height(): number { return FieldValue(this.Document.height, 0); } @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); } set width(w: number) { this.Document.width = w; @@ -42,36 +48,28 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF this.Document.height = this.nativeHeight / this.nativeWidth * w; } } - set height(h: number) { this.Document.height = h; if (this.nativeWidth && this.nativeHeight) { this.Document.width = this.nativeWidth / this.nativeHeight * h; } } - set zIndex(h: number) { this.Document.zIndex = h; } - get X() { - return FieldValue(this.Document.x, 0); - } - get Y() { - return FieldValue(this.Document.y, 0); - } - getTransform = (): Transform => - this.props.ScreenToLocalTransform() - .translate(-this.X, -this.Y) - .scale(1 / this.contentScaling()).scale(1 / this.zoom) - contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; panelWidth = () => this.props.PanelWidth(); panelHeight = () => this.props.PanelHeight(); + toggleMinimized = () => this.toggleIcon(); + getTransform = (): Transform => this.props.ScreenToLocalTransform() + .translate(-this.X, -this.Y) + .scale(1 / this.contentScaling()).scale(1 / this.zoom) @computed get docView() { return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit} + toggleMinimized={this.toggleMinimized} ContentScaling={this.contentScaling} ScreenToLocalTransform={this.getTransform} PanelWidth={this.panelWidth} @@ -79,30 +77,121 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF />; } + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) { + setTimeout(() => { + let now = Date.now(); + let progress = Math.min(1, (now - stime) / 200); + let pval = maximizing ? + [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : + [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; + target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + target.x = pval[0]; + target.y = pval[1]; + if (first) { + target.isMinimized = false; + } + 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 as any).isIconAnimating = false; + } + }, + 2); + } + @action + public toggleIcon = async (): Promise<void> => { + SelectionManager.DeselectAll(); + let isMinimized: boolean | undefined; + let minimizedDocSet = Cast(this.props.Document.linkTags, listSpec(Doc)); + if (!minimizedDocSet) return; + minimizedDocSet.map(async minimizedDoc => { + if (minimizedDoc instanceof Document) { + this.props.addDocument && this.props.addDocument(minimizedDoc, false); + let maximizedDoc = await Cast(minimizedDoc.maximizedDoc, Doc); + if (maximizedDoc && !(maximizedDoc as any).isIconAnimating) { + (maximizedDoc as any).isIconAnimating = true; + if (isMinimized === undefined) { + let maximizedDocMinimizedState = Cast(maximizedDoc.isMinimized, "boolean"); + isMinimized = (maximizedDocMinimizedState) ? true : false; + } + let minx = NumCast(minimizedDoc.x, undefined); + let miny = NumCast(minimizedDoc.y, undefined); + 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) { + this.animateBetweenIcon(true, [minx, miny], [maxx, maxy], maxw, maxh, Date.now(), maximizedDoc, isMinimized); + } + } + + } + }) + } + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + } + onClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + const maxDoc = await Cast(this.props.Document.maximizedDoc, Doc); + if (maxDoc) { // bcz: need a better way to associate behaviors with click events on widget-documents + this.props.addDocument && this.props.addDocument(maxDoc, false); + this.toggleIcon(); + } + } + } + + borderRounding = () => { + let br = NumCast(this.props.Document.borderRounding); + return br >= 0 ? br : + NumCast(this.props.Document.nativeWidth) === 0 ? + Math.min(this.props.PanelWidth(), this.props.PanelHeight()) + : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0); + } + render() { + let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDoc, Doc)); let zoomFade = 1; - //var zoom = doc.GetNumber(KeyStore.Zoom, 1); - // let transform = this.getTransform().scale(this.contentScaling()).inverse(); - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); - // let w = bptX - sptX; - // //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; - // let fadeUp = .75 * 1800; - // let fadeDown = .075 * 1800; - // zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; + //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1); + let transform = this.getTransform().scale(this.contentScaling()).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); + let w = bptX - sptX; + //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; + const screenWidth = 1800; + let fadeUp = .75 * screenWidth; + let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0, Math.min(1, 2 - (w < fadeDown ? fadeDown / w : w / fadeUp))) : 1; return ( - <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ - opacity: zoomFade, - transformOrigin: "left top", - transform: this.transform, - pointerEvents: (zoomFade < 0.09 ? "none" : "all"), - width: this.width, - height: this.height, - position: "absolute", - zIndex: this.zIndex, - backgroundColor: "transparent" - }} > + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} + onPointerDown={this.onPointerDown} + onClick={this.onClick} + style={{ + opacity: zoomFade, + borderRadius: `${this.borderRounding()}px`, + transformOrigin: "left top", + transform: this.transform, + pointerEvents: (zoomFade < 0.09 ? "none" : "all"), + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.zIndex, + backgroundColor: "transparent" + }} > {this.docView} </div> ); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ce338e9a7..aabc1633e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,8 +1,7 @@ import { action, computed, runInAction } from "mobx"; import { observer } from "mobx-react"; -import { ServerUtils } from "../../../server/ServerUtil"; import { emptyFunction, Utils } from "../../../Utils"; -import { Documents } from "../../documents/Documents"; +import { Docs } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { SelectionManager } from "../../util/SelectionManager"; @@ -21,6 +20,10 @@ import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema"; import { FieldValue, Cast, PromiseValue } 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 { MarqueeView } from "../collections/collectionFreeForm/MarqueeView"; +import { DocServer } from "../../DocServer"; const linkSchema = createSchema({ title: "string", @@ -48,6 +51,7 @@ export interface DocumentViewProps { selectOnLoad: boolean; parentActive: () => boolean; whenActiveChanged: (isActive: boolean) => void; + toggleMinimized: () => void; } const schema = createSchema({ @@ -83,39 +87,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); } @computed get topMost(): boolean { return this.props.isTopMost; } - onPointerDown = (e: React.PointerEvent): void => { - this._downX = e.clientX; - this._downY = e.clientY; - if (e.button === 2 && !this.isSelected()) { - return; - } - if (e.shiftKey && e.buttons === 2) { - if (this.props.isTopMost) { - this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); - } else { - CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); - } - e.stopPropagation(); - } else { - if (this.active) { - e.stopPropagation(); - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - } - } - } - + @action componentDidMount() { if (this._mainCont.current) { this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); } - runInAction(() => DocumentManager.Instance.DocumentViews.push(this)); + DocumentManager.Instance.DocumentViews.push(this); } - + @action componentDidUpdate() { if (this._dropDisposer) { this._dropDisposer(); @@ -126,21 +107,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu }); } } - + @action componentWillUnmount() { if (this._dropDisposer) { this._dropDisposer(); } - runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1)); + DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1); + } + + stopPropagation = (e: React.SyntheticEvent) => { + e.stopPropagation(); } startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) { if (this._mainCont.current) { - const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0); let dragData = new DragManager.DocumentDragData([this.props.Document]); + const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top); dragData.aliasOnDrop = dropAliasOfDraggedDoc; - dragData.xOffset = x - left; - dragData.yOffset = y - top; + dragData.xOffset = xoff; + dragData.yOffset = yoff; dragData.moveDocument = this.props.moveDocument; DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { handlers: { @@ -151,49 +137,58 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu } } - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) { + onClick = (e: React.MouseEvent): void => { + if (CurrentUserUtils.MainDocId !== this.props.Document.Id && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + SelectionManager.SelectDoc(this, e.ctrlKey); + } + } + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (CollectionFreeFormView.RIGHT_BTN_DRAG && (e.button === 2 || (e.button === 0 && e.altKey)) && !this.isSelected()) { return; } - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + if (e.shiftKey && e.buttons === 2) { + if (this.props.isTopMost) { + this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey); + } else { + CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e); + } + e.stopPropagation(); + } else if (this.active) { document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - if (!e.altKey && (!this.topMost || e.buttons === 2)) { - this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey); + document.addEventListener("pointerup", this.onPointerUp); + e.preventDefault(); + } + } + onPointerMove = (e: PointerEvent): void => { + if (!e.cancelBubble) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + if (!e.altKey && !this.topMost && (!CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 1) || (CollectionFreeFormView.RIGHT_BTN_DRAG && e.buttons === 2)) { + this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey); + } } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); } onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - e.stopPropagation(); - if (!SelectionManager.IsSelected(this) && e.button !== 2) { - if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientY - this._downY) < 4) { - PromiseValue(Cast(this.props.Document.maximizedDoc, Doc)).then(maxdoc => { - if (maxdoc instanceof Doc) { - this.props.addDocument && this.props.addDocument(maxdoc, false); - this.toggleMinimize(maxdoc, this.props.Document); - } else { - SelectionManager.SelectDoc(this, e.ctrlKey); - } - }); - } - } - } - stopPropagation = (e: React.SyntheticEvent) => { - e.stopPropagation(); } deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); } - fieldsClicked = (e: React.MouseEvent): void => { - if (this.props.addDocument) { - this.props.addDocument(Documents.KVPDocument(this.props.Document, { width: 300, height: 300 }), false); - } + let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); + CollectionDockingView.Instance.AddRightSplit(kvp); } fullScreenClicked = (e: React.MouseEvent): void => { const doc = Doc.MakeDelegate(FieldValue(this.Document.proto)); @@ -204,7 +199,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ContextMenu.Instance.addItem({ description: "Close Full Screen", event: this.closeFullScreenClicked }); ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } - closeFullScreenClicked = (e: React.MouseEvent): void => { CollectionDockingView.Instance.CloseFullScreen(); ContextMenu.Instance.clearItems(); @@ -212,118 +206,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15); } - @action createIcon = (layoutString: string): Doc => { - let iconDoc: Doc = Documents.IconDocument(layoutString); - iconDoc.isMinimized = false; - iconDoc.nativeWidth = 0; - iconDoc.nativeHeight = 0; - iconDoc.proto = this.props.Document; - iconDoc.maximizedDoc = this.props.Document; - this.Document.minimizedDoc = iconDoc; - this.props.addDocument && this.props.addDocument(iconDoc, false); - return iconDoc; - } - - animateTransition(icon: number[], targ: number[], width: number, height: number, stime: number, target: Doc, maximizing: boolean) { - setTimeout(() => { - let now = Date.now(); - let progress = Math.min(1, (now - stime) / 200); - let pval = maximizing ? - [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : - [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; - target.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; - target.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; - target.x = pval[0]; - target.y = pval[1]; - if (now < stime + 200) { - this.animateTransition(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; - } - this._completed = true; - } - }, - 2); - } - - _completed = true; - - @action - public toggleMinimize = (maximized: Doc, minim: Doc): void => { - SelectionManager.DeselectAll(); - if (this._completed) { - this._completed = false; - let minimized = Cast(maximized.isMinimized, "boolean", false); - maximized.isMinimized = false; - this.animateTransition( - [Cast(minim.x, "number", 0), Cast(minim.y, "number", 0)], - [Cast(maximized.x, "number", 0), Cast(maximized.y, "number", 0)], - Cast(maximized.width, "number", 0), Cast(maximized.width, "number", 0), - Date.now(), maximized, minimized); - } - } - - @action - public minimize = async (): Promise<void> => { - const mindoc = await Cast(this.props.Document.minimizedDoc, Doc); - if (mindoc === undefined) { - const background = await Cast(this.props.Document.backgroundLayout, "string"); - if (background === undefined) { - const layout = await Cast(this.props.Document.layout, "string"); - if (layout) { - this.createIcon(layout); - this.toggleMinimize(this.props.Document, this.createIcon(layout)); - } - } else { - this.toggleMinimize(this.props.Document, this.createIcon(background)); - } - } else { - this.props.addDocument && this.props.addDocument(mindoc, false); - this.toggleMinimize(this.props.Document, mindoc); - } - } - @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.LinkDragData) { - let sourceDoc: Doc = de.data.linkSourceDocument; - let destDoc: Doc = this.props.Document; - let linkDoc = LinkDoc(); - - const protoDest = await Cast(destDoc.proto, Doc); - const protoSrc = await Cast(sourceDoc.proto, Doc); - UndoManager.RunInBatch(() => { - linkDoc.title = "New Link"; - linkDoc.linkDescription = ""; - linkDoc.linkTags = "Default"; + let sourceDoc = de.data.linkSourceDocument; + let destDoc = this.props.Document; - let dstTarg = protoDest ? protoDest : destDoc; - let srcTarg = protoSrc ? protoSrc : sourceDoc; - linkDoc.linkedTo = dstTarg; - linkDoc.linkedFrom = srcTarg; - let linkedFrom = Cast(dstTarg.linkedFrom, listSpec(Doc)); - if (!linkedFrom) { - dstTarg.linkedFrom = linkedFrom = new List<Doc>(); - } - linkedFrom.push(linkDoc); - - let linkedTo = Cast(srcTarg.linkedTo, listSpec(Doc)); - if (!linkedTo) { - srcTarg.linkedTo = linkedTo = new List<Doc>(); - } - linkedTo.push(linkDoc); - }, "document view drop"); + const protoDest = destDoc.proto; + const protoSrc = sourceDoc.proto; + Doc.MakeLink(protoSrc ? protoSrc : sourceDoc, protoDest ? protoDest : destDoc); e.stopPropagation(); } } + @action onDrop = (e: React.DragEvent) => { let text = e.dataTransfer.getData("text/plain"); if (!e.isDefaultPrevented() && text && text.startsWith("<div")) { @@ -349,7 +246,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu ContextMenu.Instance.addItem({ description: "Fields", event: this.fieldsClicked }); ContextMenu.Instance.addItem({ description: "Center", event: () => this.props.focus(this.props.Document) }); ContextMenu.Instance.addItem({ description: "Open Right", event: () => CollectionDockingView.Instance.AddRightSplit(this.props.Document) }); - ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(ServerUtils.prepend("/doc/" + this.props.Document.Id)) }); + ContextMenu.Instance.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document.Id)) }); ContextMenu.Instance.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]) }); //ContextMenu.Instance.addItem({ description: "Docking", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) }) ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked }); @@ -362,11 +259,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu isSelected = () => SelectionManager.IsSelected(this); select = (ctrlPressed: boolean) => SelectionManager.SelectDoc(this, ctrlPressed); - @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth) || 0; } - @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight) || 0; } + @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); } @computed get contents() { return (<DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} layoutKey={"layout"} />); } - render() { var scaling = this.props.ContentScaling(); var nativeHeight = this.nativeHeight > 0 ? this.nativeHeight.toString() + "px" : "100%"; @@ -376,11 +272,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`} ref={this._mainCont} style={{ + borderRadius: "inherit", background: FieldValue(this.Document.backgroundColor) || "", width: nativeWidth, height: nativeHeight, transform: `scale(${scaling}, ${scaling})` }} - onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} + onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} > {this.contents} </div> diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index b11a87d75..c00c47fc4 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -11,11 +11,11 @@ import { returnFalse, emptyFunction } from "../../../Utils"; import { CollectionView } from "../collections/CollectionView"; import { CollectionPDFView } from "../collections/CollectionPDFView"; import { CollectionVideoView } from "../collections/CollectionVideoView"; -import { IconField } from "../../../fields/IconFIeld"; 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"; // @@ -38,6 +38,8 @@ export interface FieldViewProps { active: () => boolean; whenActiveChanged: (isActive: boolean) => void; focus: (doc: Doc) => void; + PanelWidth: () => number; + PanelHeight: () => number; } @observer @@ -91,6 +93,7 @@ export class FieldView extends React.Component<FieldViewProps> { layoutKey={"layout"} ContainingCollectionView={this.props.ContainingCollectionView} parentActive={this.props.active} + toggleMinimized={emptyFunction} whenActiveChanged={this.props.whenActiveChanged} /> ); } @@ -106,7 +109,7 @@ export class FieldView extends React.Component<FieldViewProps> { else if (typeof field === "number") { return <p>{field}</p>; } - else if (field !== FieldWaiting) { + else if (!(field instanceof Promise)) { return <p>{JSON.stringify(field)}</p>; } else { diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss index 5eb2bf7ce..f4f37250f 100644 --- a/src/client/views/nodes/FormattedTextBox.scss +++ b/src/client/views/nodes/FormattedTextBox.scss @@ -12,9 +12,9 @@ .formattedTextBox-cont-scroll, .formattedTextBox-cont-hidden { background: $light-color-secondary; - padding: 0.9em; + padding: 0; border-width: 0px; - border-radius: $border-radius; + border-radius: inherit; border-color: $intermediate-color; box-sizing: border-box; border-style: solid; diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index cb082dc69..c65b1ac89 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, reaction } from "mobx"; +import { action, IReactionDisposer, reaction, trace, computed } from "mobx"; import { baseKeymap } from "prosemirror-commands"; import { history } from "prosemirror-history"; import { keymap } from "prosemirror-keymap"; @@ -18,6 +18,9 @@ import { SelectionManager } from "../../util/SelectionManager"; import { DocComponent } from "../DocComponent"; import { createSchema, makeInterface } from "../../../new_fields/Schema"; import { Opt, Doc } from "../../../new_fields/Doc"; +import { observer } from "mobx-react"; +import { InkingControl } from "../InkingControl"; +import { StrCast } from "../../../new_fields/Types"; const { buildMenuItems } = require("prosemirror-example-setup"); const { menuBar } = require("prosemirror-menu"); @@ -49,6 +52,7 @@ const richTextSchema = createSchema({ type RichTextDocument = makeInterface<[typeof richTextSchema]>; const RichTextDocument = makeInterface(richTextSchema); +@observer export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxOverlay), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(FormattedTextBox, fieldStr); @@ -112,34 +116,26 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (this._editorView) { this._editorView.destroy(); } - this.setupEditor(config, 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, this.props.Document);// MainOverlayTextBox.Instance.TextDoc); // bcz: not sure why, but the order of events is such that this.props.Document hasn't updated yet, so without forcing the editor to the MainOverlayTextBox, it will display the previously focused textbox } ); } else { this._proxyReactionDisposer = reaction(() => this.props.isSelected(), - () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform())); + () => this.props.isSelected() && MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform)); } + this._reactionDisposer = reaction( () => { const field = this.props.Document ? this.props.Document.GetT(this.props.fieldKey, RichTextField) : undefined; return field && field !== FieldWaiting ? field.Data : undefined; }, - field => { - if (field && this._editorView && !this._applyingChange) { - this._editorView.updateState( - EditorState.fromJSON(config, JSON.parse(field)) - ); - } - } + field => field && this._editorView && !this._applyingChange && + this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field))) ); this.setupEditor(config, this.props.Document); } - shouldComponentUpdate() { - return false; - } - private setupEditor(config: any, doc?: Document) { let field = doc ? doc.GetT(this.props.fieldKey, RichTextField) : undefined; if (this._ref.current) { @@ -190,7 +186,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe onFocused = (e: React.FocusEvent): void => { if (!this.props.isOverlay) { - MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform()); + if (MainOverlayTextBox.Instance.TextDoc !== this.props.Document) { + MainOverlayTextBox.Instance.SetTextDoc(this.props.Document, this.props.fieldKey, this._ref.current!, this.props.ScreenToLocalTransform); + } } else { if (this._ref.current) { this._ref.current.scrollTop = MainOverlayTextBox.Instance.TextScroll; @@ -231,6 +229,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe } } + onClick = (e: React.MouseEvent): void => { + this._ref.current!.focus(); + } + tooltipTextMenuPlugin() { let myprops = this.props; return new Plugin({ @@ -249,7 +251,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe }); } - onKeyPress(e: React.KeyboardEvent) { + onKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Escape") { SelectionManager.DeselectAll(); } @@ -257,22 +259,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe if (e.keyCode === 9) e.preventDefault(); // stop propagation doesn't seem to stop propagation of native keyboard events. // so we set a flag on the native event that marks that the event's been handled. - // (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; + (e.nativeEvent as any).DASHFormattedTextBoxHandled = true; + if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) { + let str = this._editorView.state.doc.textContent; + let titlestr = str.substr(0, Math.min(40, str.length)); + this.props.Document.title = "-" + titlestr + (str.length > 40 ? "..." : ""); + } } render() { - let style = this.props.isSelected() || this.props.isOverlay ? "scroll" : "hidden"; + let style = this.props.isOverlay ? "scroll" : "hidden"; + let color = StrCast(this.props.Document.backgroundColor); + let interactive = InkingControl.Instance.selectedTool ? "" : "interactive"; return ( <div className={`formattedTextBox-cont-${style}`} + style={{ + pointerEvents: interactive ? "all" : "none", + background: color, + }} onKeyDown={this.onKeyPress} onKeyPress={this.onKeyPress} onFocus={this.onFocused} + onClick={this.onClick} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu} // tfs: do we need this event handler onWheel={this.onPointerWheel} - ref={this._ref} - /> + ref={this._ref} /> ); } } diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index f4b3837ff..9fe211df0 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -6,6 +6,10 @@ height: auto; max-width: 100%; max-height: 100%; + pointer-events: none; +} +.imageBox-cont-interactive { + pointer-events: all; } .imageBox-dot { diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index cd003ba71..0e9e904a8 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -10,12 +10,14 @@ import { ContextMenu } from "../../views/ContextMenu"; import { FieldView, FieldViewProps } from './FieldView'; import "./ImageBox.scss"; import React = require("react"); -import { createSchema, makeInterface } from '../../../new_fields/Schema'; +import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema'; import { DocComponent } from '../DocComponent'; import { positionSchema } from './DocumentView'; -import { FieldValue, Cast } from '../../../new_fields/Types'; +import { FieldValue, Cast, StrCast } from '../../../new_fields/Types'; import { ImageField } from '../../../new_fields/URLField'; import { List } from '../../../new_fields/List'; +import { InkingControl } from '../InkingControl'; +import { Doc } from '../../../new_fields/Doc'; export const pageSchema = createSchema({ curPage: "number" @@ -28,7 +30,7 @@ const ImageDocument = makeInterface(pageSchema, positionSchema); export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) { public static LayoutString() { return FieldView.LayoutString(ImageBox); } - private _imgRef: React.RefObject<HTMLImageElement>; + private _imgRef: React.RefObject<HTMLImageElement> = React.createRef(); private _downX: number = 0; private _downY: number = 0; private _lastTap: number = 0; @@ -36,12 +38,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD @observable private _isOpen: boolean = false; private dropDisposer?: DragManager.DragDropDisposer; - constructor(props: FieldViewProps) { - super(props); - - this._imgRef = React.createRef(); - } - @action onLoad = (target: any) => { var h = this._imgRef.current!.naturalHeight; @@ -71,19 +67,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.DocumentDragData) { - de.data.droppedDocuments.forEach(action((drop: Document) => { - let layout = drop.GetText(KeyStore.BackgroundLayout, ""); + de.data.droppedDocuments.forEach(action((drop: Doc) => { + let layout = StrCast(drop.backgroundLayout); if (layout.indexOf(ImageBox.name) !== -1) { - let imgData = this.props.Document.Get(KeyStore.Data); - if (imgData instanceof ImageField && imgData) { - this.props.Document.SetOnPrototype(KeyStore.Data, new ListField([imgData])); + let imgData = this.props.Document[this.props.fieldKey]; + if (imgData instanceof ImageField) { + Doc.SetOnPrototype(this.props.Document, "data", new List([imgData])); } - let imgList = this.props.Document.GetList(KeyStore.Data, [] as any[]); + let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]); if (imgList) { - let field = drop.Get(KeyStore.Data); - if (field === FieldWaiting) { } - else if (field instanceof ImageField) imgList.push(field); - else if (field instanceof ListField) imgList.push(field.Data); + let field = drop.data; + if (field instanceof ImageField) imgList.push(field); + else if (field instanceof List) imgList.concat(field); } e.stopPropagation(); } @@ -95,7 +90,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD onPointerDown = (e: React.PointerEvent): void => { if (Date.now() - this._lastTap < 300) { if (e.buttons === 1) { - e.stopPropagation(); this._downX = e.clientX; this._downY = e.clientY; document.removeEventListener("pointerup", this.onPointerUp); @@ -135,7 +129,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD specificContextMenu = (e: React.MouseEvent): void => { let field = Cast(this.Document[this.props.fieldKey], ImageField); - if (field && field !== FieldWaiting) { + if (field) { let url = field.url.href; ContextMenu.Instance.addItem({ description: "Copy path", event: () => { @@ -165,12 +159,12 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD render() { let field = this.Document[this.props.fieldKey]; let paths: string[] = ["http://www.cs.brown.edu/~bcz/face.gif"]; - if (field === FieldWaiting) paths = ["https://image.flaticon.com/icons/svg/66/66163.svg"]; - else if (field instanceof ImageField) paths = [field.url.href]; + if (field instanceof ImageField) paths = [field.url.href]; else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => (p as ImageField).url.href); let nativeWidth = FieldValue(this.Document.nativeWidth, 1); + let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive"; return ( - <div className="imageBox-cont" onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> + <div className={`imageBox-cont${interactive}`} onPointerDown={this.onPointerDown} onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}> <img src={paths[Math.min(paths.length, this._photoIndex)]} style={{ objectFit: (this._photoIndex === 0 ? undefined : "contain") }} width={nativeWidth} alt="Image not found" ref={this._imgRef} onLoad={this.onLoad} /> {paths.length > 1 ? this.dots(paths) : (null)} {this.lightbox(paths)} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 7b922b620..86276660a 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -2,7 +2,6 @@ import React = require("react"); import { observer } from "mobx-react"; import { FieldView, FieldViewProps } from './FieldView'; import "./VideoBox.scss"; -import Measure from "react-measure"; import { action, computed } from "mobx"; import { DocComponent } from "../DocComponent"; import { positionSchema } from "./DocumentView"; @@ -10,6 +9,8 @@ import { makeInterface } from "../../../new_fields/Schema"; import { pageSchema } from "./ImageBox"; import { Cast, FieldValue } from "../../../new_fields/Types"; import { VideoField } from "../../../new_fields/URLField"; +import Measure from "react-measure"; +import "./VideoBox.scss"; type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; const VideoDocument = makeInterface(positionSchema, pageSchema); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 63778fa50..c12fd5655 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,20 +1,17 @@ import "./WebBox.scss"; import React = require("react"); import { FieldViewProps, FieldView } from './FieldView'; -import { observer } from "mobx-react"; -import { computed } from 'mobx'; 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"; @observer export class WebBox extends React.Component<FieldViewProps> { public static LayoutString() { return FieldView.LayoutString(WebBox); } - constructor(props: FieldViewProps) { - super(props); - } - _ignore = 0; onPreWheel = (e: React.WheelEvent) => { this._ignore = e.timeStamp; @@ -36,23 +33,25 @@ export class WebBox extends React.Component<FieldViewProps> { let field = this.props.Document[this.props.fieldKey]; let view; if (field instanceof HtmlField) { - view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} /> + view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />; } else if (field instanceof WebField) { - view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} /> + view = <iframe src={field.url.href} style={{ position: "absolute", width: "100%", height: "100%" }} />; } else { - view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} /> + view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%" }} />; } let content = <div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}> {view} </div>; + let frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + return ( <> <div className="webBox-cont" > {content} </div> - {this.props.isSelected() ? (null) : <div onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} style={{ width: "100%", height: "100%", position: "absolute" }} />} + {!frozen ? (null) : <div onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} style={{ width: "100%", height: "100%", position: "absolute" }} />} </>); } }
\ No newline at end of file diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts index bd10e5474..79e5a156d 100644 --- a/src/new_fields/Doc.ts +++ b/src/new_fields/Doc.ts @@ -5,6 +5,9 @@ import { Utils } from "../Utils"; import { DocServer } from "../client/DocServer"; import { setter, getter, getField } from "./util"; import { Cast, ToConstructor, PromiseValue, FieldValue } from "./Types"; +import { UndoManager, undoBatch } from "../client/util/UndoManager"; +import { listSpec } from "./Schema"; +import { List } from "./List"; export type FieldId = string; export const HandleUpdate = Symbol("HandleUpdate"); @@ -54,7 +57,7 @@ export class Doc extends RefField { return doc; } - proto: FieldResult<Doc>; + proto: Opt<Doc>; [key: string]: FieldResult; @serializable(alias("fields", map(autoObject()))) @@ -126,6 +129,31 @@ export namespace Doc { return alias; } + export function MakeLink(source: Doc, target: Doc): Doc { + let linkDoc = new Doc; + UndoManager.RunInBatch(() => { + linkDoc.title = "New Link"; + linkDoc.linkDescription = ""; + linkDoc.linkTags = "Default"; + + linkDoc.linkedTo = target; + linkDoc.linkedFrom = source; + + let linkedFrom = Cast(target.linkedFromDocs, listSpec(Doc)); + if (!linkedFrom) { + target.linkedFromDocs = linkedFrom = new List<Doc>(); + } + linkedFrom.push(linkDoc); + + let linkedTo = Cast(source.linkedToDocs, listSpec(Doc)); + if (!linkedTo) { + source.linkedToDocs = linkedTo = new List<Doc>(); + } + linkedTo.push(linkDoc); + }, "make link"); + return linkDoc; + } + export function MakeDelegate(doc: Doc): Doc; export function MakeDelegate(doc: Opt<Doc>): Opt<Doc>; export function MakeDelegate(doc: Opt<Doc>): Opt<Doc> { diff --git a/src/new_fields/Types.ts b/src/new_fields/Types.ts index 4ad8dcade..079c7b76d 100644 --- a/src/new_fields/Types.ts +++ b/src/new_fields/Types.ts @@ -59,6 +59,14 @@ export function Cast<T extends ToConstructor<Field> | ListSpec<Field>>(field: Fi return defaultVal; } +export function NumCast(field: FieldResult, defaultVal: Opt<number> = 0) { + return Cast(field, "number", defaultVal); +} + +export function StrCast(field: FieldResult, defaultVal: Opt<string> = "") { + return Cast(field, "string", defaultVal); +} + type WithoutList<T extends Field> = T extends List<infer R> ? R[] : T; export function FieldValue<T extends Field, U extends WithoutList<T>>(field: Opt<T> | Promise<Opt<T>>, defaultValue: U): WithoutList<T>; |