aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2024-02-07 16:18:16 -0500
committerbobzel <zzzman@gmail.com>2024-02-07 16:18:16 -0500
commite3fde25014d523c5f43a138093718899fe17d108 (patch)
tree8ae6e905408302857dcdadf39f0a7ba68479c2a9 /src/client/views/nodes
parent97bc8fb32741051554509eeaf9d223b327ebd611 (diff)
made various render methods in DocumentView computed getters for efficiency and to avoid artifacts (LInkanchorBox dragging) when something else invalidates causing components to regenerate. fixed linklines to animate when doing a zoom transition and to be able to target texts hyperlinks. fixed link lines to share properties with ink and updated the properties panel / menus to allow editing of either. addding toggling link lines on and off from linkitemmenu
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/DocumentView.tsx35
-rw-r--r--src/client/views/nodes/LinkBox.tsx197
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts5
3 files changed, 143 insertions, 94 deletions
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5efa028d1..042ae6e55 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -48,6 +48,7 @@ import { KeyValueBox } from './KeyValueBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PresEffect, PresEffectDirection } from './trails';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
interface Window {
MediaRecorder: MediaRecorder;
}
@@ -726,7 +727,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
};
removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false);
- allLinkEndpoints = () => {
+ @computed get allLinkEndpoints() {
// the small blue dots that mark the endpoints of links
if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
return this.filteredLinks.map(link => (
@@ -750,9 +751,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
/>
</div>
));
- };
+ }
- viewBoxContents = () => {
+ @computed get viewBoxContents() {
TraceMobx();
const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
@@ -778,10 +779,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
setTitleFocus={this.setTitleFocus}
hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
/>
- {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()}
+ {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
</div>
);
- };
+ }
captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption');
fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => {
@@ -814,7 +815,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
* setting layout_showTitle using the format: field1[;field2[...][:hover]]
* from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover]
**/
- titleView = () => {
+ @computed get titleView() {
const showTitle = this.layout_showTitle?.split(':')[0];
const showTitleHover = this.layout_showTitle?.includes(':hover');
@@ -888,9 +889,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
</div>
</div>
);
- };
+ }
- captionView = () => {
+ @computed get captionView() {
return !this.layout_showCaption ? null : (
<div
className="documentView-captionWrapper"
@@ -913,7 +914,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
/>
</div>
);
- };
+ }
renderDoc = (style: object) => {
TraceMobx();
@@ -933,15 +934,15 @@ 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 ? this._props.DataTransition?.() || StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+ transition: !this._animateScalingTo ? this._props.DataTransition?.() : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
}}>
{this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
- this.viewBoxContents()
+ this.viewBoxContents
) : (
<div className="documentView-styleWrapper">
- {this.titleView()}
- {this.viewBoxContents()}
- {this.captionView()}
+ {this.titleView}
+ {this.viewBoxContents}
+ {this.captionView}
</div>
)}
{this.widgetDecorations ?? null}
@@ -1191,8 +1192,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined };
}
// 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) };
+ const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.());
+ return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() };
}
@computed get nativeWidth() {
@@ -1337,6 +1338,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}
}
};
+ DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition);
ShouldNotScale = () => this.shouldNotScale;
NativeWidth = () => this.effectiveNativeWidth;
NativeHeight = () => this.effectiveNativeHeight;
@@ -1402,6 +1404,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
<DocumentViewInternal
{...this._props}
fieldKey={this.LayoutFieldKey}
+ DataTransition={this.DataTransition}
DocumentView={this.selfView}
docViewPath={this.docViewPath}
PanelWidth={this.PanelWidth}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 85d22abfd..0788e5adc 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,18 +1,21 @@
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Xarrow from 'react-xarrows';
+import { DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
import { DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, lightOrDark, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils';
import { DocumentManager } from '../../util/DocumentManager';
import { LinkManager } from '../../util/LinkManager';
import { SnappingManager } from '../../util/SnappingManager';
import { ViewBoxBaseComponent } from '../DocComponent';
+import { EditableView } from '../EditableView';
+import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkBox.scss';
-import { LinkDescriptionPopup } from './LinkDescriptionPopup';
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -43,19 +46,20 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.disposer = reaction(
() => ({ 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;
- }
- })
- );
+ !LightboxView.Contains(this.DocumentView?.()) &&
+ 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 }
);
@@ -63,89 +67,130 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
select = (ctrlKey: boolean, shiftKey: boolean) => (LinkManager.Instance.currentLink = this.Document);
- descriptionDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- returnFalse,
- action(() => {
- LinkManager.Instance.currentLink = this.Document;
- LinkDescriptionPopup.Instance.popupX = e.clientX;
- LinkDescriptionPopup.Instance.popupY = e.clientY;
- LinkDescriptionPopup.Instance.display = true;
-
- const rect = document.body.getBoundingClientRect();
- if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) {
- LinkDescriptionPopup.Instance.popupX -= 190;
- }
- if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) {
- LinkDescriptionPopup.Instance.popupY -= 40;
- }
- }),
- false
- );
- };
render() {
- trace();
+ if (this._hide) return null;
const a = this.anchor1;
const b = this.anchor2;
this._forceAnimate;
- if (a && b && !this._hide) {
+ if (a && b && !LightboxView.Contains(this.DocumentView?.())) {
+ // text selection bounds are not directly observable, so we have to
+ // force an update when anything that could affect them changes (text edits causing reflow, scrolling)
+ a.Document[a.LayoutFieldKey];
+ b.Document[b.LayoutFieldKey];
+ a.Document.layout_scrollTop;
+ b.Document.layout_scrollTop;
+
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;
+ const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation)
+
+ // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>),
+ // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
+ // otherwise, we just use the computed nearest point on the document boundary to the target Document
+ const targetAhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement();
+ const targetBhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement();
+
+ const aid = targetAhyperlink?.id || a.Document[Id];
+ const bid = targetBhyperlink?.id || b.Document[Id];
+ if (!document.getElementById(aid) || !document.getElementById(bid)) {
+ setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01)));
+ return null;
+ }
+
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 color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
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 { strokeWidth, stroke_startMarker, stroke_endMarker } = this.Document;
- const dash = StrCast(this.Document.stroke_dash);
- const stroke = highlightColor ?? 'lightblue';
+ const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor));
+ const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document;
+ const strokeWidth = NumCast(stroke_width, 4);
const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
return (
- <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={{
- 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 }}
- />
+ <>
+ {!highlightColor ? null : (
+ <Xarrow
+ divContainerStyle={{ transform: `scale(${scale})` }}
+ start={aid}
+ end={bid} //
+ strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)}
+ showHead={stroke_startMarker ? true : false}
+ showTail={stroke_endMarker ? true : false}
+ headSize={NumCast(stroke_markerScale, 3)}
+ tailSize={NumCast(stroke_markerScale, 3)}
+ tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
+ headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
+ color={highlightColor}
+ />
+ )}
+ <Xarrow
+ divContainerStyle={{ transform: `scale(${scale})` }}
+ start={aid}
+ end={bid} //
+ strokeWidth={strokeWidth}
+ dashness={Number(stroke_dash) ? true : false}
+ showHead={stroke_startMarker ? true : false}
+ showTail={stroke_endMarker ? true : false}
+ headSize={NumCast(stroke_markerScale, 3)}
+ tailSize={NumCast(stroke_markerScale, 3)}
+ tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'}
+ headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'}
+ color={color}
+ labels={
+ <div
+ style={{
+ borderRadius: '8px',
+ pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
+ fontSize,
+ fontFamily /*, fontStyle: 'italic'*/,
+ color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()),
+ paddingLeft: 4,
+ paddingRight: 4,
+ paddingTop: 3,
+ paddingBottom: 3,
+ background: DashColor(highlightColor || color)
+ .fade(0.5)
+ .toString(),
+ }}>
+ <EditableView
+ key="editableView"
+ oneLine
+ contents={labelText}
+ height={fontSize + 4}
+ fontSize={fontSize}
+ GetValue={() => linkDesc}
+ SetValue={action(val => {
+ this.Document[DocData].link_description = val;
+ return true;
+ })}
+ />
+
+ {/* <EditableText
+ placeholder={labelText}
+ background={color}
+ color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())}
+ type={Type.PRIM}
+ val={StrCast(this.Document[DocData].link_description)}
+ setVal={action(val => (this.Document[DocData].link_description = val))}
+ fillWidth
+ /> */}
+ </div>
+ }
+ passProps={{}}
+ />
+ </>
);
}
- return null;
return (
<div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}>
<ComparisonBox
- {...this._props} //
+ {...this.props} //
fieldKey="link_anchor"
setHeight={emptyFunction}
dontRegisterView={true}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index a342285b0..a141ef041 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -1,6 +1,7 @@
import * as React from 'react';
import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model';
import { Doc } from '../../../../fields/Doc';
+import { Utils } from '../../../../Utils';
const emDOM: DOMOutputSpec = ['em', 0];
const strongDOM: DOMOutputSpec = ['strong', 0];
@@ -44,7 +45,7 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM(node: any) {
const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), '');
const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
- return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0];
+ return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0];
},
},
noAutoLinkAnchor: {
@@ -104,7 +105,7 @@ export const marks: { [index: string]: MarkSpec } = {
node.attrs.title,
],
]
- : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0];
+ : ['a', { id: '' + Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0];
},
},