diff options
Diffstat (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 304 |
1 files changed, 254 insertions, 50 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 50dc5a619..a0efc3154 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,84 +1,288 @@ -import { computed, trace } from "mobx"; +import { action, computed, IReactionDisposer, reaction } from "mobx"; import { observer } from "mobx-react"; -import { KeyStore } from "../../../fields/KeyStore"; -import { NumberField } from "../../../fields/NumberField"; +import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema"; +import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { OmitKeys, Utils } from "../../../Utils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; -import { DocumentView, DocumentViewProps } from "./DocumentView"; +import { UndoManager } from "../../util/UndoManager"; +import { CollectionDockingView } from "../collections/CollectionDockingView"; +import { DocComponent } from "../DocComponent"; +import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"; import "./DocumentView.scss"; import React = require("react"); +export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { +} + +const schema = createSchema({ + zoomBasis: "number", + zIndex: "number", +}); + +//TODO Types: The import order is wrong, so positionSchema is undefined +type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>; +const FreeformDocument = makeInterface(schema, positionSchema); @observer -export class CollectionFreeFormDocumentView extends React.Component<DocumentViewProps> { +export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) { private _mainCont = React.createRef<HTMLDivElement>(); + private _downX: number = 0; + private _downY: number = 0; + _bringToFrontDisposer?: IReactionDisposer; - constructor(props: DocumentViewProps) { - super(props); - } - get screenRect(): ClientRect | DOMRect { - if (this._mainCont.current) { - return this._mainCont.current.getBoundingClientRect(); - } - return new DOMRect(); - } - - @computed - get transform(): string { - return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.props.Document.GetNumber(KeyStore.X, 0)}px, ${this.props.Document.GetNumber(KeyStore.Y, 0)}px)`; + @computed get transform() { + return `scale(${this.props.ContentScaling()}, ${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) scale(${this.zoom}, ${this.zoom}) `; } - @computed get zIndex(): number { return this.props.Document.GetNumber(KeyStore.ZIndex, 0); } - @computed get width(): number { return this.props.Document.Width(); } - @computed get height(): number { return this.props.Document.Height(); } - @computed get nativeWidth(): number { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); } - @computed get nativeHeight(): number { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); } + @computed get X() { return FieldValue(this.Document.x, 0); } + @computed get Y() { return FieldValue(this.Document.y, 0); } + @computed get zoom(): number { return 1 / FieldValue(this.Document.zoomBasis, 1); } + @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); } + @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); } + @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.width, 0); } + @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : FieldValue(this.Document.height, 0); } set width(w: number) { - this.props.Document.SetData(KeyStore.Width, w, NumberField) + this.Document.width = w; if (this.nativeWidth && this.nativeHeight) { - this.props.Document.SetNumber(KeyStore.Height, this.nativeHeight / this.nativeWidth * w) + this.Document.height = this.nativeHeight / this.nativeWidth * w; } } - set height(h: number) { - this.props.Document.SetData(KeyStore.Height, h, NumberField); + this.Document.height = h; if (this.nativeWidth && this.nativeHeight) { - this.props.Document.SetNumber(KeyStore.Width, this.nativeWidth / this.nativeHeight * h) + this.Document.width = this.nativeWidth / this.nativeHeight * h; } } + contentScaling = () => this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + panelWidth = () => this.props.PanelWidth(); + panelHeight = () => this.props.PanelHeight(); + toggleMinimized = async () => this.toggleIcon(await DocListCastAsync(this.props.Document.maximizedDocs)); + getTransform = (): Transform => this.props.ScreenToLocalTransform() + .translate(-this.X, -this.Y) + .scale(1 / this.contentScaling()).scale(1 / this.zoom) - set zIndex(h: number) { - this.props.Document.SetData(KeyStore.ZIndex, h, NumberField) + @computed + get docView() { + return <DocumentView {...OmitKeys(this.props, ['zoomFade']).omit} + toggleMinimized={this.toggleMinimized} + ContentScaling={this.contentScaling} + ScreenToLocalTransform={this.getTransform} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + />; } - contentScaling = () => { - return this.nativeWidth > 0 ? this.width / this.nativeWidth : 1; + componentDidMount() { + this._bringToFrontDisposer = reaction(() => this.props.Document.isIconAnimating, (values) => { + this.props.bringToFront(this.props.Document); + if (values instanceof List) { + let scrpt = this.props.ScreenToLocalTransform().transformPoint(values[0], values[1]); + this.animateBetweenIcon(true, scrpt, [this.Document.x || 0, this.Document.y || 0], + this.Document.width || 0, this.Document.height || 0, values[2], values[3] ? true : false); + } + }, { fireImmediately: true }); } - getTransform = (): Transform => { - return this.props.ScreenToLocalTransform(). - translate(-this.props.Document.GetNumber(KeyStore.X, 0), -this.props.Document.GetNumber(KeyStore.Y, 0)).scale(1 / this.contentScaling()); + componentWillUnmount() { + if (this._bringToFrontDisposer) this._bringToFrontDisposer(); } - @computed - get docView() { - return <DocumentView {...this.props} - ContentScaling={this.contentScaling} - ScreenToLocalTransform={this.getTransform} - /> + animateBetweenIcon(first: boolean, icon: number[], targ: number[], width: number, height: number, stime: number, maximizing: boolean) { + + setTimeout(() => { + let now = Date.now(); + let progress = Math.min(1, (now - stime) / 200); + let pval = maximizing ? + [icon[0] + (targ[0] - icon[0]) * progress, icon[1] + (targ[1] - icon[1]) * progress] : + [targ[0] + (icon[0] - targ[0]) * progress, targ[1] + (icon[1] - targ[1]) * progress]; + this.props.Document.width = maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress; + this.props.Document.height = maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress; + this.props.Document.x = pval[0]; + this.props.Document.y = pval[1]; + if (first) { + this.props.Document.proto!.willMaximize = false; + } + if (now < stime + 200) { + this.animateBetweenIcon(false, icon, targ, width, height, stime, maximizing); + } + else { + if (!maximizing) { + this.props.Document.proto!.isMinimized = true; + this.props.Document.x = targ[0]; + this.props.Document.y = targ[1]; + this.props.Document.width = width; + this.props.Document.height = height; + } + this.props.Document.proto!.isIconAnimating = undefined; + } + }, + 2); + } + @action + public toggleIcon = async (maximizedDocs: Doc[] | undefined): Promise<void> => { + SelectionManager.DeselectAll(); + let isMinimized: boolean | undefined; + let minimizedDoc: Doc | undefined = this.props.Document; + if (!maximizedDocs) { + minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc); + if (minimizedDoc) maximizedDocs = await DocListCastAsync(minimizedDoc.maximizedDocs); + } + if (minimizedDoc && maximizedDocs) { + let minimizedTarget = minimizedDoc; + if (!CollectionFreeFormDocumentView._undoBatch) { + CollectionFreeFormDocumentView._undoBatch = UndoManager.StartBatch("iconAnimating"); + } + maximizedDocs.map(maximizedDoc => { + let iconAnimating = Cast(maximizedDoc.isIconAnimating, List); + if (!iconAnimating || (Date.now() - iconAnimating[6] > 1000)) { + if (isMinimized === undefined) { + isMinimized = BoolCast(maximizedDoc.isMinimized, false); + } + let minx = NumCast(minimizedTarget.x, undefined) + NumCast(minimizedTarget.width, undefined) / 2; + let miny = NumCast(minimizedTarget.y, undefined) + NumCast(minimizedTarget.height, undefined) / 2; + if (minx !== undefined && miny !== undefined) { + let scrpt = this.props.ScreenToLocalTransform().inverse().transformPoint(minx, miny); + maximizedDoc.willMaximize = isMinimized; + maximizedDoc.isMinimized = false; + maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]); + } + } + }); + setTimeout(() => { + CollectionFreeFormDocumentView._undoBatch && CollectionFreeFormDocumentView._undoBatch.end(); + CollectionFreeFormDocumentView._undoBatch = undefined; + }, 500); + } + } + private _lastTap: number = 0; + static _undoBatch?: UndoManager.Batch = undefined; + onPointerDown = (e: React.PointerEvent): void => { + this._downX = e.clientX; + this._downY = e.clientY; + if (e.button === 0 && e.altKey) { + e.stopPropagation(); // prevents panning from happening on collection if shift is pressed after a document drag has started + } // allow pointer down to go through otherwise so that marquees can be drawn starting over a document + if (Date.now() - this._lastTap < 300) { + if (e.buttons === 1) { + this._downX = e.clientX; + this._downY = e.clientY; + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); + } + } else { + this._lastTap = Date.now(); + } + } + onPointerUp = (e: PointerEvent): void => { + + document.removeEventListener("pointerup", this.onPointerUp); + if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) { + this.props.addDocTab(this.props.Document); + } + e.stopPropagation(); + } + onClick = async (e: React.MouseEvent) => { + e.stopPropagation(); + let altKey = e.altKey; + if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && + Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + let isExpander = (e.target as any).id === "isExpander"; + if (BoolCast(this.props.Document.isButton, false) || isExpander) { + let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs); + let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs); + let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs); + let linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []); + let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []); + let expandedDocs: Doc[] = []; + expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs; + expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs; + expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs; + // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), + // ...(maximizedDocs ? maximizedDocs : []), + // ...(summarizedDocs ? summarizedDocs : []),]; + if (expandedDocs.length) { // bcz: need a better way to associate behaviors with click events on widget-documents + let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView); + if (!hasView && ((altKey && !this.props.Document.maximizeOnRight) || (!altKey && this.props.Document.maximizeOnRight))) { + let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data); + if (dataDocs) { + SelectionManager.DeselectAll(); + expandedDocs.forEach(maxDoc => { + maxDoc.isMinimized = false; + if (!CollectionDockingView.Instance.CloseRightSplit(maxDoc)) { + CollectionDockingView.Instance.AddRightSplit(Doc.MakeDelegate(maxDoc)); + } + }); + } + } else { + //if (altKey) this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(maxDoc, false)); + this.toggleIcon(expandedDocs); + } + } + else if (linkedToDocs.length || linkedFromDocs.length) { + SelectionManager.DeselectAll(); + let linkedFwdDocs = [ + linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0], + linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]]; + if (linkedFwdDocs) { + DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], altKey); + } + } + } + } + } + + onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; }; + onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; }; + + borderRounding = () => { + let br = NumCast(this.props.Document.borderRounding); + return br >= 0 ? br : + NumCast(this.props.Document.nativeWidth) === 0 ? + Math.min(this.props.PanelWidth(), this.props.PanelHeight()) + : Math.min(this.Document.nativeWidth || 0, this.Document.nativeHeight || 0); } render() { + let maximizedDoc = FieldValue(Cast(this.props.Document.maximizedDocs, listSpec(Doc))); + let zoomFade = 1; + //var zoom = doc.GetNumber(KeyStore.ZoomBasis, 1); + let transform = this.getTransform().scale(this.contentScaling()).inverse(); + var [sptX, sptY] = transform.transformPoint(0, 0); + let [bptX, bptY] = transform.transformPoint(this.props.PanelWidth(), this.props.PanelHeight()); + let w = bptX - sptX; + //zoomFade = area < 100 || area > 800 ? Math.max(0, Math.min(1, 2 - 5 * (zoom < this.scale ? this.scale / zoom : zoom / this.scale))) : 1; + const screenWidth = Math.min(50 * NumCast(this.props.Document.nativeWidth, 0), 1800); + let fadeUp = .75 * screenWidth; + let fadeDown = (maximizedDoc ? .0075 : .075) * screenWidth; + zoomFade = w < fadeDown /* || w > fadeUp */ ? Math.max(0.1, Math.min(1, 2 - (w < fadeDown ? Math.sqrt(Math.sqrt(fadeDown / w)) : w / fadeUp))) : 1; + return ( - <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{ - transformOrigin: "left top", - transform: this.transform, - width: this.width, - height: this.height, - position: "absolute", - zIndex: this.zIndex, - backgroundColor: "transparent" - }} > + <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} + onPointerDown={this.onPointerDown} + onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} onPointerOver={this.onPointerEnter} + onClick={this.onClick} + style={{ + outlineColor: "maroon", + outlineStyle: "dashed", + outlineWidth: BoolCast(this.props.Document.libraryBrush, false) || + BoolCast(this.props.Document.protoBrush, false) ? + `${1 * this.getTransform().Scale}px` : "0px", + opacity: zoomFade, + borderRadius: `${this.borderRounding()}px`, + transformOrigin: "left top", + transform: this.transform, + pointerEvents: (zoomFade < 0.09 ? "none" : "all"), + width: this.width, + height: this.height, + position: "absolute", + zIndex: this.Document.zIndex || 0, + backgroundColor: "transparent" + }} > {this.docView} </div> ); |