aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
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];
},
},