diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 1 | ||||
-rw-r--r-- | src/client/util/DragManager.ts | 2 | ||||
-rw-r--r-- | src/client/util/SelectionManager.ts | 8 | ||||
-rw-r--r-- | src/client/views/DocComponent.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackingView.tsx | 12 | ||||
-rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 15 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 24 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 213 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 7 |
9 files changed, 122 insertions, 162 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 95058da22..1978c144b 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -594,7 +594,6 @@ export namespace Docs { layout: { view: LinkBox, dataField: 'link' }, options: { childDontRegisterViews: true, - onClick: FollowLinkScript(), layout_hideLinkAnchors: true, _height: 1, _width: 1, diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 071b25a0e..a6ad0f1b3 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -243,7 +243,7 @@ export namespace DragManager { }; dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded StartDrag(eles, dragData, downX, downY, options, finishDrag); - dragData.draggedViews.forEach(view => view.props.dragStarting?.(dragData)); + dragData.draggedViews.forEach(view => view.props.dragStarting?.()); return true; } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index eddc7fc16..36b926053 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -52,9 +52,11 @@ export class SelectionManager { public static DeselectAll = (except?: Doc): void => { const found = this.Instance.SelectedViews.find(dv => dv.Document === except); - LinkManager.Instance.currentLink = undefined; - LinkManager.Instance.currentLinkAnchor = undefined; - runInAction(() => (this.Instance.SelectedSchemaDocument = undefined)); + runInAction(() => { + LinkManager.Instance.currentLink = undefined; + LinkManager.Instance.currentLinkAnchor = undefined; + this.Instance.SelectedSchemaDocument = undefined; + }); this.Instance.SelectedViews.forEach(dv => { dv.IsSelected = false; dv._props.whenChildContentsActiveChanged(false); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 3c772bd42..dacd359c5 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -50,7 +50,7 @@ export interface ViewBoxInterface { dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag) incrementalRendering?: () => void; infoUI?: () => JSX.Element | null; - screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }>; + screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>; ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 89e72152a..54314f62c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -135,14 +135,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return docs.map((d, i) => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); + const trans = () => this.getDocTransition(d); // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> - {this.getDisplayDoc(d, width, i)} + {this.getDisplayDoc(d, width, trans, i)} </div> ); }); @@ -309,7 +310,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list - getDisplayDoc(doc: Doc, width: () => number, count: number) { + getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; const height = () => this.getDocHeight(doc); const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight())); @@ -330,6 +331,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection layout_fitWidth={this.childFitWidth} isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive} onKey={this.onKeyDown} + DataTransition={trans} onBrowseClickScript={this._props.onBrowseClickScript} isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} @@ -379,6 +381,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } return maxWidth; } + getDocTransition(d?: Doc) { + if (!d) return ''; + return StrCast(d.dataTransition); + } getDocHeight(d?: Doc) { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 0ae4ed62c..a0a64ab59 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -33,7 +33,6 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView opacity?: number; highlight?: boolean; transition?: string; - dataTransition?: string; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } @@ -56,7 +55,6 @@ export class CollectionFreeFormDocumentViewWrapper extends ObservableReactCompon @observable Height = this.props.height; @observable AutoDim = this.props.autoDim; @observable Transition = this.props.transition; - @observable DataTransition = this.props.dataTransition; CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking @@ -83,7 +81,6 @@ export class CollectionFreeFormDocumentViewWrapper extends ObservableReactCompon w_Height = () => this.Height; // prettier-ignore w_AutoDim = () => this.AutoDim; // prettier-ignore w_Transition = () => this.Transition; // prettier-ignore - w_DataTransition = () => this.DataTransition; // prettier-ignore PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore @@ -117,7 +114,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { w_Transition: () => string | undefined; w_Width: () => number; w_Height: () => number; - w_DataTransition: () => string | undefined; PanelWidth: () => number; PanelHeight: () => number; RenderCutoffProvider: (doc: Doc) => boolean; @@ -288,14 +284,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this._props.PanelWidth(), height: this._props.PanelHeight(), transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, - transition: this._props.w_Transition?.() ?? (this._props.w_DataTransition?.() || this._props.w_Transition?.()), + transition: this._props.w_Transition?.() || StrCast(this.Document.dataTransition), zIndex: this._props.w_ZIndex?.(), display: this._props.w_Width?.() ? undefined : 'none', }}> {this._props.RenderCutoffProvider(this.Document) ? ( <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> ) : ( - <DocumentView {...passOnProps} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} /> + <DocumentView + {...passOnProps} + DataTransition={this._props.w_Transition} + CollectionFreeFormDocumentView={this.returnThis} + styleProvider={this.styleProvider} + ScreenToLocalTransform={this.screenToLocalTransform} + isGroupActive={this.isGroupActive} + /> )} </div> ); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 02f756f16..5efa028d1 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -104,6 +104,7 @@ export interface DocumentViewProps extends FieldViewSharedProps { dontHideOnDrag?: boolean; suppressSetHeight?: boolean; onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + DataTransition?: () => string | undefined; NativeWidth?: () => number; NativeHeight?: () => number; contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; @@ -116,7 +117,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. - /** * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without * needing to know about/reference MainView @@ -933,7 +933,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + transition: !this._animateScalingTo ? this._props.DataTransition?.() || StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( this.viewBoxContents() @@ -1075,6 +1075,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; private _animEffectTimer: NodeJS.Timeout | undefined; + public Guid = Utils.GenerateGuid(); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. @computed public static get exploreMode() { return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); @@ -1151,6 +1152,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } componentWillUnmount() { + this._viewTimer && clearTimeout(this._viewTimer); runInAction(() => this.Document[DocViews].delete(this)); Object.values(this._disposers).forEach(disposer => disposer?.()); !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); @@ -1174,21 +1176,23 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; } - @computed get getBounds() { - if (!this._docViewInternal?._contentDiv || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) { + @computed get getBounds(): Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }> { + if (!this.ContentDiv || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) { return undefined; } - if (this._docViewInternal._componentView?.screenBounds?.()) { - return this._docViewInternal._componentView.screenBounds(); + if (this.ComponentView?.screenBounds?.()) { + return this.ComponentView.screenBounds(); } const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this._docViewInternal._contentDiv.getElementsByClassName('linkAnchorBox-cont'); - if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; + const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); + if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined }; } - return { left, top, right, bottom }; + // transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox + const transition = this.docViewPath().find((parent: DocumentView) => parent._props.DataTransition?.() || StrCast(parent.Document.dataTransition)); + return { left, top, right, bottom, transition: transition?._props.DataTransition?.() || StrCast(transition?.Document.dataTransition) }; } @computed get nativeWidth() { @@ -1385,7 +1389,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 07511d0ec..85d22abfd 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,119 +1,67 @@ -import { Bezier } from 'bezier-js'; -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Id } from '../../../fields/FieldSymbols'; +import Xarrow from 'react-xarrows'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { aggregateBounds, emptyFunction, lightOrDark, returnAlways, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils'; +import { emptyFunction, lightOrDark, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; -import { Transform } from '../../util/Transform'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; +import { LinkManager } from '../../util/LinkManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; -import { LinkManager } from '../../util/LinkManager'; @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } + disposer: IReactionDisposer | undefined; + @observable _forceAnimate = 0; // forces xArrow to animate when a transition is detected on something that affects an anchor + @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor constructor(props: FieldViewProps) { super(props); makeObservable(this); } + @computed get anchor1() { return this.anchor(1); } // prettier-ignore + @computed get anchor2() { return this.anchor(2); } // prettier-ignore - onClickScriptDisable = returnAlways; - @computed get anchor1() { - const anchor1 = DocCast(this.dataDoc.link_anchor_1); - const anchor_1 = anchor1?.layout_unrendered ? DocCast(anchor1.annotationOn) : anchor1; - return DocumentManager.Instance.getDocumentView(anchor_1, this.DocumentView?.().containerViewPath?.().lastElement()); - } - @computed get anchor2() { - const anchor2 = DocCast(this.dataDoc.link_anchor_2); - const anchor_2 = anchor2?.layout_unrendered ? DocCast(anchor2.annotationOn) : anchor2; - return DocumentManager.Instance.getDocumentView(anchor_2, this.DocumentView?.().containerViewPath?.().lastElement()); - } - screenBounds = () => { - if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.CollectionFreeFormView) { - const a_invXf = this.anchor1.screenToViewTransform().inverse(); - const b_invXf = this.anchor2.screenToViewTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(this.anchor1.Document._width), NumCast(this.anchor1.Document._height)) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(this.anchor2.Document._width), NumCast(this.anchor2.Document._height)) }; - - const pts = [] as number[][]; - pts.push([(a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2]); - pts.push(Utils.getNearestPointInPerimeter(a_scrBds.tl[0], a_scrBds.tl[1], a_scrBds.br[0] - a_scrBds.tl[0], a_scrBds.br[1] - a_scrBds.tl[1], (b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2)); - pts.push(Utils.getNearestPointInPerimeter(b_scrBds.tl[0], b_scrBds.tl[1], b_scrBds.br[0] - b_scrBds.tl[0], b_scrBds.br[1] - b_scrBds.tl[1], (a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2)); - pts.push([(b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2]); - const agg = aggregateBounds( - pts.map(pt => ({ x: pt[0], y: pt[1] })), - 0, - 0 - ); - return { left: agg.x, top: agg.y, right: agg.r, bottom: agg.b, center: undefined }; - } - return undefined; + anchor = (which: number) => { + const anch = DocCast(this.dataDoc['link_anchor_' + which]); + const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; + return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; - disposer: IReactionDisposer | undefined; + componentWillUnmount(): void { + this.disposer?.(); + } componentDidMount() { this._props.setContentViewBox?.(this); this.disposer = reaction( - () => { - if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.CollectionFreeFormView) { - const a = (this.anchor1 ?? this.anchor2)!; - const b = (this.anchor2 ?? this.anchor1)!; - - const parxf = this.DocumentView?.().containerViewPath?.().lastElement().ComponentView as CollectionFreeFormView; - const this_xf = parxf?.screenToFreeformContentsXf ?? Transform.Identity; //this.ScreenToLocalTransform(); - const a_invXf = a.screenToViewTransform().inverse(); - const b_invXf = b.screenToViewTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(a.Document._width), NumCast(a.Document._height)) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(b.Document._width), NumCast(b.Document._height)) }; - const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; - const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; - - const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; - const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); - const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); - const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; - - const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); - const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; - return { pts, lx, rx, ty, by }; - } - return undefined; - }, - params => { - this.renderProps = params; - if (params) { - if ( - Math.abs(params.lx - NumCast(this.layoutDoc.x)) > 1e-5 || - Math.abs(params.ty - NumCast(this.layoutDoc.y)) > 1e-5 || - Math.abs(params.rx - params.lx - NumCast(this.layoutDoc._width)) > 1e-5 || - Math.abs(params.by - params.ty - NumCast(this.layoutDoc._height)) > 1e-5 - ) { - this.layoutDoc.x = params?.lx; - this.layoutDoc.y = params?.ty; - this.layoutDoc._width = Math.max(1, params.rx - params?.lx); - this.layoutDoc._height = Math.max(1, params?.by - params?.ty); - } - } else { - this.layoutDoc._width = Math.max(50, NumCast(this.layoutDoc._width)); - this.layoutDoc._height = Math.max(50, NumCast(this.layoutDoc._height)); - } + () => ({ drag: SnappingManager.IsDragging, a: this.anchor1, b: this.anchor2 }), + ({ drag, a, b }) => { + setTimeout( + // need to wait for drag manager to set 'hidden' flag on dragged elements + action(() => { + let a1 = a && document.getElementById(a.Guid); + let a2 = b && document.getElementById(b.Guid); + if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; + else { + for (; a1 && !a1.hidden; a1 = a1.parentElement); + for (; a2 && !a2.hidden; a2 = a2.parentElement); + this._hide = a1 || a2 ? true : false; + } + }) + ); }, { fireImmediately: true } ); } - select = (ctrlKey: boolean, shiftKey: boolean) => { - LinkManager.Instance.currentLink = this.Document; - }; + select = (ctrlKey: boolean, shiftKey: boolean) => (LinkManager.Instance.currentLink = this.Document); descriptionDown = (e: React.PointerEvent) => { setupMoveUpEvents( @@ -121,7 +69,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { e, returnFalse, returnFalse, - () => { + action(() => { LinkManager.Instance.currentLink = this.Document; LinkDescriptionPopup.Instance.popupX = e.clientX; LinkDescriptionPopup.Instance.popupY = e.clientY; @@ -134,69 +82,66 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) { LinkDescriptionPopup.Instance.popupY -= 40; } - }, + }), false ); }; - componentWillUnmount(): void { - this.disposer?.(); - } - @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined = undefined; render() { - if (this.renderProps) { + trace(); + const a = this.anchor1; + const b = this.anchor2; + this._forceAnimate; + + if (a && b && !this._hide) { + const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) + const bxf = b.screenToViewTransform(); + const scale = a.CollectionFreeFormView === this.DocumentView?.().CollectionFreeFormView ? axf.Scale : bxf.Scale; + const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition + const bt = b.getBounds?.transition; + if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); const color = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); - - const { pts, lx, ty, rx, by } = this.renderProps; - const bez = new Bezier(pts.map(p => ({ x: p[0] - lx, y: p[1] - ty }))); - const { x, y } = bez.get(0.5); - const linkDesc = StrCast(this.dataDoc.link_description) || ' '; - const strokeWidth = NumCast(this.Document.stroke_width, 4); + const { strokeWidth, stroke_startMarker, stroke_endMarker } = this.Document; const dash = StrCast(this.Document.stroke_dash); - const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined; - const pointerEvents = this._props.pointerEvents?.() === 'none' ? 'none' : 'all'; const stroke = highlightColor ?? 'lightblue'; + + const linkDesc = StrCast(this.dataDoc.link_description) || ' '; + const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( - <div className="linkBox"> - <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)}> - <defs> - <filter x="0" y="0" width="1" height="1" id={`${this.Document[Id] + 'background'}`}> - <feFlood floodColor={`${highlightColor ?? StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" /> - <feMerge> - <feMergeNode in="bg" /> - <feMergeNode in="SourceGraphic" /> - </feMerge> - </filter> - </defs> - <path + <Xarrow + divContainerStyle={{ transform: `scale(${scale})` }} + start={a.Guid} + end={b.Guid} // + strokeWidth={NumCast(strokeWidth, 4)} + dashness={dash ? true : false} + showHead={stroke_startMarker ? true : false} + showTail={stroke_endMarker ? true : false} + tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + color={stroke} + labels={ + <div + onPointerDown={this.descriptionDown} // style={{ - pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // - stroke, - strokeDasharray, - strokeWidth, - }} - d={`M ${bez.points[0].x} ${bez.points[0].y} C ${bez.points[1].x} ${bez.points[1].y}, - ${bez.points[2].x} ${bez.points[2].y}, ${bez.points[3].x} ${bez.points[3].y}`} - /> - {!linkDesc.trim().length ? ( - <circle r={5} onPointerDown={this.descriptionDown} style={{ fill: stroke, pointerEvents }} cx={x} cy={y} /> - ) : ( - <text - onPointerDown={this.descriptionDown} // - filter={`url(#${this.Document[Id] + 'background'})`} - style={{ pointerEvents, background: stroke }} - x={x} - y={y}> - <tspan style={{ fontSize, fontFamily, strokeWidth: 0, background: stroke, fill: color || lightOrDark(stroke) }}> {linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')} </tspan> - </text> - )} - </svg> - </div> + borderRadius: '20%', // + padding: '3', + pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, + background: stroke, + color: color || lightOrDark(stroke), + fontSize, + fontFamily /*, fontStyle: 'italic'*/, + }}> + {labelText} + </div> + } + passProps={{ onPointerDown: this.descriptionDown }} + /> ); } + return null; return ( <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 1b2c45e72..b8f6575dd 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, Observ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Field, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; -import { Animation, DocData } from '../../../../fields/DocSymbols'; +import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -414,7 +414,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { bestTarget.rotation = NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)); bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]); + bestTarget[TransitionTimer] = setTimeout(() => (bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined), transTime + 10); changed = true; } } @@ -441,7 +442,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const bestTargetData = bestTarget[DocData]; const current = bestTargetData[fkey]; const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as Field)) : undefined; - if (hash) bestTargetData[fkey + '_' +hash] = current instanceof ObjectField ? current[Copy]() : current; + if (hash) bestTargetData[fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; |