diff options
| author | bobzel <zzzman@gmail.com> | 2024-02-07 00:02:36 -0500 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-02-07 00:02:36 -0500 |
| commit | 97bc8fb32741051554509eeaf9d223b327ebd611 (patch) | |
| tree | 003a0c1b62e67f295fa5b98efc160547fefed56f | |
| parent | c6955fe2c0720a8e5f13bad91fdc0e2a6f76f8bd (diff) | |
switch to xAnchor to clean up link lines. added transition to getBounds() so that LinkBox can follow animated transitions. added dataTransitions for stacking view. fixed presBox to be able to clear transition timers when a new slide transition is chosen.
| -rw-r--r-- | package-lock.json | 18 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -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 |
11 files changed, 141 insertions, 162 deletions
diff --git a/package-lock.json b/package-lock.json index aafc14a86..d85f54f51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -182,6 +182,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", "rehype-raw": "^7.0.0", @@ -29602,6 +29603,23 @@ "react-dom": ">= 15.0.0" } }, + "node_modules/react-xarrows": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/react-xarrows/-/react-xarrows-2.0.2.tgz", + "integrity": "sha512-tDlAqaxHNmy0vegW/6NdhoWyXJq1LANX/WUAlHyzoHe9BwFVnJPPDghmDjYeVr7XWFmBrVTUrHsrW7GKYI6HtQ==", + "dependencies": { + "@types/prop-types": "^15.7.3", + "lodash": "^4.17.21", + "prop-types": "^15.7.2" + }, + "funding": { + "type": "individual", + "url": "https://www.paypal.com/donate?hosted_button_id=CRQ343F9VTRS8" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", diff --git a/package.json b/package.json index a94f8c9be..258bd4550 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,7 @@ "react-measure": "^2.5.2", "react-resizable": "^3.0.5", "react-select": "^5.8.0", + "react-xarrows": "^2.0.2", "readline": "^1.3.0", "recharts": "^2.10.3", "rehype-raw": "^7.0.0", 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; |
