aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-02-07 00:02:36 -0500
committerbobzel <zzzman@gmail.com>2024-02-07 00:02:36 -0500
commit97bc8fb32741051554509eeaf9d223b327ebd611 (patch)
tree003a0c1b62e67f295fa5b98efc160547fefed56f
parentc6955fe2c0720a8e5f13bad91fdc0e2a6f76f8bd (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.json18
-rw-r--r--package.json1
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/util/SelectionManager.ts8
-rw-r--r--src/client/views/DocComponent.tsx2
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx12
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx15
-rw-r--r--src/client/views/nodes/DocumentView.tsx24
-rw-r--r--src/client/views/nodes/LinkBox.tsx213
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx7
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) }}>&nbsp;{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}&nbsp;</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;