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/CollectionFreeFormDocumentView.tsx15
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx6
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx6
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx99
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.scss10
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx8
-rw-r--r--src/client/views/nodes/LabelBox.tsx2
-rw-r--r--src/client/views/nodes/LinkBox.scss25
-rw-r--r--src/client/views/nodes/LinkBox.tsx281
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx18
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx4
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx39
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss10
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx27
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx65
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx16
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts151
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts5
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts12
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx7
22 files changed, 526 insertions, 288 deletions
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/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
index c0e738cfa..6672603f3 100644
--- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -436,9 +436,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> {
this.updateBarColors();
this._histogramData;
var curSelectedBarName = '';
- var titleAccessor: any = '';
- if (this._props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0] + '-' + this._props.axes[1];
- else if (this._props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0];
+ var titleAccessor: any = 'dataViz_histogram_title';
+ if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
+ else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2';
if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>();
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 8268d22b6..e093ec648 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -394,9 +394,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> {
}
render() {
- var titleAccessor: any = '';
- if (this._props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0] + '-' + this._props.axes[1];
- else if (this._props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0];
+ var titleAccessor: any = 'dataViz_lineChart_title';
+ if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
+ else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
var selectedTitle = "";
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
index 54a83879c..fc23f47de 100644
--- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -332,9 +332,9 @@ export class PieChart extends ObservableReactComponent<PieChartProps> {
};
render() {
- var titleAccessor: any = '';
- if (this._props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this._props.axes[0] + '-' + this._props.axes[1];
- else if (this._props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this._props.axes[0];
+ var titleAccessor: any = 'dataViz_pie_title';
+ if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
+ else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0];
if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>();
var selected: string;
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d1805308d..2a68d2bf6 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -157,7 +157,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB
startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink;
const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined });
- LinkManager.currentLink = linkDoc;
+ LinkManager.Instance.currentLink = linkDoc;
if (linkDoc) {
if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2e1ba2a7e..73c13b5dd 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -20,13 +20,14 @@ import { DocServer } from '../../DocServer';
import { Networking } from '../../Network';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocOptions, DocUtils, Docs, FInfo } from '../../documents/Documents';
+import { DocOptions, DocUtils, Docs } from '../../documents/Documents';
import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SearchUtil } from '../../util/SearchUtil';
import { SelectionManager } from '../../util/SelectionManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
@@ -103,9 +104,11 @@ 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 }[];
+ dragConfig?: (data: DragManager.DocumentDragData) => void;
dragStarting?: () => void;
dragEnding?: () => void;
}
@@ -114,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
@@ -288,7 +290,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
dragData.moveDocument = this._props.moveDocument;
dragData.draggedViews = [docView];
dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false;
- this._componentView?.dragConfig?.(dragData);
+ (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData);
DragManager.StartDocumentDrag(
selected.map(dv => dv.ContentDiv!),
dragData,
@@ -359,7 +361,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
this._singleClickFunc =
// prettier-ignore
clickFunc ?? (() => (sendToBack ? documentView._props.bringToFront?.(this.Document, true) :
- this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ??
this._props.select(e.ctrlKey||e.shiftKey, e.metaKey)));
const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
@@ -456,11 +457,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
setToggleDetail = undoable(
- () =>
- (this.Document.onClick = ScriptField.MakeScript(
+ (defaultLayout: string, scriptFieldKey: 'onClick') =>
+ (this.Document[scriptFieldKey] = ScriptField.MakeScript(
`toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
.replace('layout_', '')
- .replace(/^layout$/, 'detail')}")`,
+ .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
{ documentView: 'any' }
)),
'set toggle detail'
@@ -488,7 +489,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
if (linkDoc) {
de.complete.linkDocument = linkDoc;
linkDoc.layout_isSvg = true;
- this._docView?.CollectionFreeFormView?.addDocument(linkDoc);
+ DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
}
}
e.stopPropagation();
@@ -725,7 +726,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 => (
@@ -749,9 +750,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');
@@ -777,10 +778,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) => {
@@ -813,7 +814,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');
@@ -887,9 +888,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
</div>
</div>
);
- };
+ }
- captionView = () => {
+ @computed get captionView() {
return !this.layout_showCaption ? null : (
<div
className="documentView-captionWrapper"
@@ -912,7 +913,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
/>
</div>
);
- };
+ }
renderDoc = (style: object) => {
TraceMobx();
@@ -932,15 +933,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 ? 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}
@@ -1074,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);
@@ -1150,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);
@@ -1173,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.DataTransition?.() || parent.ComponentView?.viewTransition?.());
+ return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() };
}
@computed get nativeWidth() {
@@ -1212,7 +1217,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
};
public noOnClick = () => this._docViewInternal?.noOnClick();
public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle);
- public setToggleDetail = () => this._docViewInternal?.setToggleDetail();
+ public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey);
public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY);
public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents();
public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource);
@@ -1332,6 +1337,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
}
}
};
+ DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition);
ShouldNotScale = () => this.shouldNotScale;
NativeWidth = () => this.effectiveNativeWidth;
NativeHeight = () => this.effectiveNativeHeight;
@@ -1384,7 +1390,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"
@@ -1397,6 +1403,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}
@@ -1460,13 +1467,13 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
});
-ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
- if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') {
+ if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
});
ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
- const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
+ const collectedLinks = DocListCast(linkCollection[DocData].data);
let wid = NumCast(linkSource._width);
let embedding: Doc | undefined;
const links = LinkManager.Links(linkSource);
@@ -1485,3 +1492,27 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour
embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise
return links;
});
+ScriptingGlobals.add(function updateTagsCollection(collection: Doc) {
+ const tag = StrCast(collection.title).split('-->')[1];
+ const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys());
+ const collectionDocs = DocListCast(collection[DocData].data).concat(collection);
+ let wid = 100;
+ let created = false;
+ const matchedDocs = matchedTags
+ .filter(tagDoc => !Doc.AreProtosEqual(collection, tagDoc))
+ .map(tagDoc => {
+ let embedding = collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc));
+ if (!embedding) {
+ embedding = Doc.MakeEmbedding(tagDoc);
+ embedding.x = wid;
+ embedding.y = 0;
+ embedding._lockedPosition = false;
+ wid += NumCast(tagDoc._width);
+ created = true;
+ }
+ return embedding;
+ });
+
+ created && (collection[DocData].data = new List<Doc>(matchedDocs));
+ return true;
+});
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index db2ffa756..2db285910 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -1,5 +1,15 @@
@import '../../global/globalCssVariables.module.scss';
+// bcz: something's messed up with the IconButton css. this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text')
+.iconButton-container {
+ width: unset !important;
+ min-width: 30px !important;
+ height: unset !important;
+ min-height: 30px;
+ .color {
+ height: 3px !important;
+ }
+}
.menuButton {
height: 100%;
display: flex;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 8290e102c..3577cc8d9 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -381,7 +381,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
case ButtonType.ColorButton: return this.colorButton;
case ButtonType.MultiToggleButton: return this.multiToggleButton;
case ButtonType.ToggleButton: return this.toggleButton;
- case ButtonType.ClickButton:
+ case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />;
case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />;
case ButtonType.TextButton: return <Button {...btnProps} color={color}
background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>;
@@ -392,6 +392,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
};
render() {
- return <div onContextMenu={this.specificContextMenu}>{this.renderButton()}</div>;
+ return (
+ <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}>
+ {this.renderButton()}
+ </div>
+ );
}
}
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 10eeff08d..fd3074a88 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -88,7 +88,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
@observable _mouseOver = false;
@computed get hoverColor() {
- return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
+ return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor);
}
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index 767f0291b..484fb301e 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -5,3 +5,28 @@
.linkBox-container {
width: 100%;
}
+
+.linkBox {
+ transition: inherit;
+ pointer-events: none;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ path {
+ transition: inherit;
+ fill: transparent;
+ }
+ svg {
+ transition: inherit;
+ overflow: visible;
+ }
+ text {
+ cursor: default;
+ text-anchor: middle;
+ font-size: 12;
+ stroke: black;
+ }
+ circle {
+ cursor: default;
+ }
+}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 8b6293806..decdbb240 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,14 +1,17 @@
-import { Bezier } from 'bezier-js';
-import { computed, IReactionDisposer, makeObservable, observable, reaction } 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 { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, lightOrDark, returnFalse } 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 { EditableView } from '../EditableView';
+import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
@@ -19,152 +22,178 @@ 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 animation 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() {
+ 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 = params.rx - params?.lx;
- this.layoutDoc._height = 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 }),
+ ({ drag }) => {
+ !LightboxView.Contains(this.DocumentView?.()) &&
+ setTimeout(
+ // need to wait for drag manager to set 'hidden' flag on dragged DOM elements
+ action(() => {
+ const a = this.anchor1,
+ b = this.anchor2;
+ let a1 = a && document.getElementById(a.Guid);
+ let a2 = b && document.getElementById(b.Guid);
+ // test whether the anchors themselves are hidden,...
+ if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true;
+ else {
+ // .. or whether and of their DOM parents are hidden
+ for (; a1 && !a1.hidden; a1 = a1.parentElement);
+ for (; a2 && !a2.hidden; a2 = a2.parentElement);
+ this._hide = a1 || a2 ? true : false;
+ }
+ })
+ );
},
{ fireImmediately: true }
);
}
- componentWillUnmount(): void {
- this.disposer?.();
- }
- @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined = undefined;
+
render() {
- if (this.renderProps) {
+ if (this._hide) return null;
+ const a = this.anchor1;
+ const b = this.anchor2;
+ this._forceAnimate;
+ const docView = this._props.docViewPath().lastElement();
+
+ if (a && b && !LightboxView.Contains(docView)) {
+ // 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 = docView?.screenToViewTransform().Scale ?? 1;
+ 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; // 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(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement();
+ const targetBhyperlink = Array.from(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 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 bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] })));
- const text = bez.get(0.5);
- const linkDesc = StrCast(this.dataDoc.link_description) || 'description';
- const strokeWidth = NumCast(this.dataDoc.stroke_width, 4);
- const dash = StrCast(this.Document.stroke_dash);
- const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined;
- const { pts, lx, ty, rx, by } = this.renderProps;
+ const strokeWidth = NumCast(stroke_width, 4);
+ const linkDesc = StrCast(this.dataDoc.link_description) || ' ';
+ const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '');
return (
- <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}>
- <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}>
- <defs>
- <filter x="0" y="0" width="1" height="1" id={`${this.Document[Id] + 'background'}`}>
- <feFlood floodColor={`${StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" />
- <feMerge>
- <feMergeNode in="bg" />
- <feMergeNode in="SourceGraphic" />
- </feMerge>
- </filter>
- </defs>
- <path
- className="collectionfreeformlinkview-linkLine"
- style={{
- pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', //
- stroke: highlightColor ?? 'lightblue',
- strokeDasharray,
- strokeWidth,
- transition: 'inherit',
- }}
- d={`M ${pts[1][0] - lx} ${pts[1][1] - ty} C ${pts[1][0] + pts[1][0] - pts[0][0] - lx} ${pts[1][1] + pts[1][1] - pts[0][1] - ty},
- ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`}
+ <>
+ {!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}
/>
- <text
- filter={`url(#${this.Document[Id] + 'background'})`}
- style={{ pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }}
- x={text.x - lx}
- y={text.y - ty}>
- <tspan>&nbsp;</tspan>
- <tspan dy="2">{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}</tspan>
- <tspan dy="2">&nbsp;</tspan>
- </text>
- </svg>
- </div>
+ )}
+ <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((!docView?.isSelected() && 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 (
<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/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 13f0ac4fc..1645d0813 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -1,10 +1,11 @@
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { DocData } from '../../../fields/DocSymbols';
import { LinkManager } from '../../util/LinkManager';
import './LinkDescriptionPopup.scss';
import { TaskCompletionBox } from './TaskCompletedBox';
+import { StrCast } from '../../../fields/Types';
@observer
export class LinkDescriptionPopup extends React.Component<{}> {
@@ -31,21 +32,26 @@ export class LinkDescriptionPopup extends React.Component<{}> {
onDismiss = (add: boolean) => {
this.display = false;
if (add) {
- LinkManager.currentLink && (LinkManager.currentLink[DocData].link_description = this.description);
+ LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink[DocData].link_description = this.description);
}
+ this.description = '';
};
@action
onClick = (e: PointerEvent) => {
if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) {
this.display = false;
+ this.description = '';
TaskCompletionBox.taskCompleted = false;
}
};
- @action
componentDidMount() {
document.addEventListener('pointerdown', this.onClick, true);
+ reaction(
+ () => this.display,
+ display => display && (this.description = StrCast(LinkManager.Instance.currentLink?.link_description))
+ );
}
componentWillUnmount() {
@@ -65,8 +71,10 @@ export class LinkDescriptionPopup extends React.Component<{}> {
className="linkDescriptionPopup-input"
onKeyDown={e => e.stopPropagation()}
onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
- placeholder={'(Optional) Enter link description...'}
- onChange={e => this.descriptionChanged(e)}></input>
+ value={this.description}
+ placeholder={this.description || '(Optional) Enter link description...'}
+ onChange={e => this.descriptionChanged(e)}
+ />
<div className="linkDescriptionPopup-btn">
<div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}>
{' '}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 3f793b85e..ae25ff179 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -152,8 +152,8 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
returnFalse,
emptyFunction,
action(() => {
- LinkManager.currentLink = this._linkDoc;
- LinkManager.currentLinkAnchor = this._linkSrc;
+ LinkManager.Instance.currentLink = this._linkDoc;
+ LinkManager.Instance.currentLinkAnchor = this._linkSrc;
this._props.DocumentView?.().select(false);
if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) {
SettingsManager.Instance.propertiesWidth = 250;
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index b7d2a24c2..a72ed1813 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -3,6 +3,8 @@ import * as ReactDOM from 'react-dom/client';
import { Doc } from '../../../../fields/Doc';
import { DocServer } from '../../../DocServer';
import * as React from 'react';
+import { IReactionDisposer, computed, reaction } from 'mobx';
+import { NumCast } from '../../../../fields/Types';
// creates an inline comment in a note when '>>' is typed.
// the comment sits on the right side of the note and vertically aligns with its anchor in the text.
@@ -10,8 +12,10 @@ import * as React from 'react';
export class DashDocCommentView {
dom: HTMLDivElement; // container for label and value
root: any;
+ node: any;
constructor(node: any, view: any, getPos: any) {
+ this.node = node;
this.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
@@ -32,10 +36,14 @@ export class DashDocCommentView {
};
this.root = ReactDOM.createRoot(this.dom);
- this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
+ this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />);
(this as any).dom = this.dom;
}
+ setHeight = (hgt: number) => {
+ !this.node.attrs.reflow && DocServer.GetRefField(this.node.attrs.docId).then(doc => doc instanceof Doc && (this.dom.style.height = hgt + ''));
+ };
+
destroy() {
this.root.unmount();
}
@@ -51,9 +59,15 @@ interface IDashDocCommentViewInternal {
docId: string;
view: any;
getPos: any;
+ setHeight: (height: number) => void;
}
export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> {
+ _reactionDisposer: IReactionDisposer | undefined;
+
+ @computed get _dashDoc() {
+ return DocServer.GetRefField(this.props.docId);
+ }
constructor(props: any) {
super(props);
this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this);
@@ -61,15 +75,32 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this);
this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this);
}
+ componentDidMount(): void {
+ this._reactionDisposer?.();
+ this._dashDoc.then(
+ doc =>
+ doc instanceof Doc &&
+ (this._reactionDisposer = reaction(
+ () => NumCast((doc as Doc)._height),
+ hgt => this.props.setHeight(hgt),
+ {
+ fireImmediately: true,
+ }
+ ))
+ );
+ }
+ componentWillUnmount(): void {
+ this._reactionDisposer?.();
+ }
onPointerLeaveCollapsed(e: any) {
- DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
e.preventDefault();
e.stopPropagation();
}
onPointerEnterCollapsed(e: any) {
- DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
e.preventDefault();
e.stopPropagation();
}
@@ -82,7 +113,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
setTimeout(() => {
- expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try {
this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
} catch (e) {}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index 3426ba1a7..7a0ff8776 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -5,6 +5,16 @@
display: inline-flex;
align-items: center;
+ select {
+ display: none;
+ }
+
+ &:hover {
+ select {
+ display: unset;
+ }
+ }
+
.dashFieldView-enumerables {
width: 10px;
height: 10px;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 18286267a..ec0b76aa8 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -4,21 +4,24 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
import { observer } from 'mobx-react';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
-import { Doc, Field } from '../../../../fields/Doc';
+import Select from 'react-select';
+import { Doc, DocListCast, Field } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast } from '../../../../fields/Types';
+import { Cast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { CollectionViewType } from '../../../documents/DocumentTypes';
import { Transform } from '../../../util/Transform';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { OpenWhere } from '../DocumentView';
import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
+import { FilterPanel } from '../../FilterPanel';
export class DashFieldView {
dom: HTMLDivElement; // container for label and value
@@ -97,7 +100,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
_reactionDisposer: IReactionDisposer | undefined;
_textBoxDoc: Doc;
_fieldKey: string;
- _fieldStringRef = React.createRef<HTMLSpanElement>();
+ _fieldRef = React.createRef<HTMLDivElement>();
@observable _dashDoc: Doc | undefined = undefined;
@observable _expanded = false;
@@ -180,10 +183,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
});
};
+ @undoBatch
+ selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => {
+ event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value);
+ };
+
+ @computed get values() {
+ const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
+
+ return vals.strings.map(facet => ({ value: facet, label: facet }));
+ }
+
render() {
return (
<div
className="dashFieldView"
+ ref={this._fieldRef}
style={{
width: this._props.width,
height: this._props.height,
@@ -194,8 +209,12 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
{(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
</span>
)}
-
{this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
+ <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}>
+ {this.values.map(val => (
+ <option value={val.value}>{val.label}</option>
+ ))}
+ </select>
</div>
);
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0b857794b..f2c4c6c8f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { EditorView } from 'prosemirror-view';
import * as React from 'react';
import { BsMarkdownFill } from 'react-icons/bs';
import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
@@ -67,7 +67,7 @@ import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
import { RichTextRules } from './RichTextRules';
import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
-import { CollectionView } from '../../collections/CollectionView';
+import Select from 'react-select';
// import * as applyDevTools from 'prosemirror-dev-tools';
@observer
export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
@@ -361,7 +361,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
accumTags.push(node.attrs.fieldKey);
}
});
- dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
+ if (accumTags.some(atag => !StrListCast(dataDoc.tags).includes(atag))) {
+ dataDoc.tags = new List<string>(Array.from(new Set<string>(accumTags)));
+ }
let unchanged = true;
if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
@@ -487,14 +489,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
// creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@'
+ /**
+ * Searches the text for occurences of any strings that match the names of 'published' documents. These document
+ * names will begin with an '@' prefix. However, valid matches within the text can have any of the following formats:
+ * name, @<name>, or ^@<name>
+ * The last of these is interpreted as an include directive when converting the text into evaluated code in the paint
+ * function of a freeform view that is driven by the text box's text. The include directive will copy the code of the published
+ * document into the code being evaluated.
+ */
hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
const editorView = this._editorView;
if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) {
const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
var alink: Doc | undefined;
this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
- const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
- if (!sel.$anchor.pos || [autoLinkTerm, StrCast(target.title)].includes(editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim())) {
+ if (
+ !sel.$anchor.pos ||
+ autoLinkTerm ===
+ editorView.state.doc
+ .textBetween(sel.$anchor.pos - 1, sel.$to.pos)
+ .trim()
+ .replace(/[\^@]+/, '')
+ ) {
+ const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
tr = tr.addMark(sel.from, sel.to, splitter);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
@@ -667,12 +684,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let index = 0,
foundAt;
const ep = this.getNodeEndpoints(pm.state.doc, node);
- const regexp = new RegExp(find.replace('*', ''), 'i');
+ const regexp = new RegExp(find, 'i');
if (regexp) {
- while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) {
- const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
- ret.push(sel);
- index = index + foundAt + find.length;
+ var blockOffset = 0;
+ for (var i = 0; i < node.childCount; i++) {
+ var textContent = '';
+ while (i < node.childCount && node.child(i).type === pm.state.schema.nodes.text) {
+ textContent += node.child(i).textContent;
+ i++;
+ }
+ while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) {
+ const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1));
+ ret.push(sel);
+ index = index + foundAt + find.length;
+ }
+ blockOffset += textContent.length;
+ if (i < node.childCount) blockOffset += node.child(i).nodeSize;
}
}
} else {
@@ -933,17 +960,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
});
- optionItems.push({
- description: 'Make Paint Function',
- event: () => {
- this.dataDoc.layout_painted = CollectionView.LayoutString('painted');
- this.layoutDoc.layout_fieldKey = 'layout_painted';
- this.layoutDoc.type_collection = CollectionViewType.Freeform;
- this.DocumentView?.().setToggleDetail();
- this.dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${this.fieldKey}']?.Text)`);
- },
- icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye',
- });
!Doc.noviceMode &&
optionItems.push({
description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
@@ -1176,8 +1192,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._cachedLinks = LinkManager.Links(this.Document);
this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
this._disposers.layout_autoHeight = reaction(
- () => this.layout_autoHeight,
- layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight()
+ () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize }),
+ (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
);
this._disposers.highlights = reaction(
() => Array.from(FormattedTextBox._globalHighlights).slice(),
@@ -1538,15 +1554,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._downX = e.clientX;
this._downY = e.clientY;
this._downTime = Date.now();
- this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
FormattedTextBoxComment.textBox = this;
if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
// stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
e.stopPropagation(); // if the text box's content is active, then it consumes all down events
document.addEventListener('pointerup', this.onSelectEnd);
+ (this.ProseRef?.children?.[0] as any).focus();
}
}
+ this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false;
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 5858c3b11..cd0cdaa74 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -141,12 +141,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const activeSizes = active.activeSizes;
const activeColors = active.activeColors;
const activeHighlights = active.activeHighlights;
+ const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc();
+ const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey);
this.activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
- this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
- this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
- this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
+ this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
+ this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0];
+ this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(refDoc[refField + 'fontColor'], 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
// update link in current selection
@@ -358,8 +360,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
}
- } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) {
- SelectionManager.Views.forEach(dv => (dv.Document._text_fontSize = fontSize));
+ } else if (SelectionManager.Views.length) {
+ SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontSize'] = fontSize));
} else Doc.UserDoc().fontSize = fontSize;
this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
@@ -369,6 +371,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family });
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
+ } else if (SelectionManager.Views.length) {
+ SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontFamily'] = family));
} else Doc.UserDoc().fontFamily = family;
this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
@@ -387,6 +391,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color });
this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true);
this.view.focus();
+ } else if (SelectionManager.Views.length) {
+ SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontColor'] = color));
} else Doc.UserDoc().fontColor = color;
this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index be32a2c4a..9bd41f42c 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -4,8 +4,7 @@ import { Doc, StrListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { NumCast } from '../../../../fields/Types';
+import { NumCast, StrCast } from '../../../../fields/Types';
import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -15,6 +14,7 @@ import { RichTextMenu } from './RichTextMenu';
import { schema } from './schema_rts';
import { CollectionView } from '../../collections/CollectionView';
import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { ContextMenu } from '../../ContextMenu';
export class RichTextRules {
public Document: Doc;
@@ -68,23 +68,20 @@ export class RichTextRules {
),
// ``` create code block
- textblockTypeInputRule(/^```$/, schema.nodes.code_block),
-
- new InputRule(new RegExp(/^\^@paint/), (state, match, start, end) => {
- const { dataDoc, layoutDoc, fieldKey } = this.TextBox;
- layoutDoc.type_collection = CollectionViewType.Freeform;
- dataDoc.layout_painted = CollectionView.LayoutString('painted');
- const layoutFieldKey = layoutDoc.layout_fieldKey;
- layoutDoc.layout_fieldKey = 'layout_painted';
- this.TextBox.DocumentView?.().setToggleDetail();
- layoutDoc.layout_fieldKey = layoutFieldKey;
- dataDoc.paintFunc = ComputedField.MakeFunction(`toJavascriptString(this['${fieldKey}']?.Text)`);
- const comment = '/* this is now a paint func */';
- const tr = state.tr
- .deleteRange(start, end)
- .insertText(comment)
- .insert(start + comment.length, schema.nodes.code_block.create());
- return tr.setSelection(new TextSelection(tr.doc.resolve(start + comment.length + 2)));
+ new InputRule(/^```$/, (state, match, start, end) => {
+ let $start = state.doc.resolve(start);
+ if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), schema.nodes.code_block)) return null;
+
+ // this enables text with code blocks to be used as a 'paint' function via a styleprovider button that is added to Docs that have an onPaint script
+ this.TextBox.layoutDoc.type_collection = CollectionViewType.Freeform; // make it a freeform when rendered as a collection since those are the only views that know about the paint function
+ const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted'; // make a layout field key for storing the CollectionView jsx string pointing to the textbox's text
+ this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey);
+ const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey
+ this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key
+ this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view
+ this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text
+
+ return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block);
}),
// %<font-size> set the font size
@@ -94,6 +91,32 @@ export class RichTextRules {
}),
//Create annotation to a field on the text document
+ new InputRule(new RegExp(/>::$/), (state, match, start, end) => {
+ const creator = (doc: Doc) => {
+ const textDoc = this.Document[DocData];
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' });
+ const sm = state.storedMarks || undefined;
+ this.TextBox.EditorView?.dispatch(
+ node
+ ? this.TextBox.EditorView.state.tr
+ .insert(start, newNode)
+ .replaceRangeWith(start + 1, end + 2, dashDoc)
+ .insertText(' ', start + 2)
+ .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+ : this.TextBox.EditorView.state.tr
+ );
+ };
+ DocUtils.addDocumentCreatorMenuItems(creator, creator, 200, 200);
+ const cm = ContextMenu.Instance;
+ cm.displayMenu(200, 200, undefined, true);
+
+ return null;
+ }),
+ //Create annotation to a field on the text document
new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
const textDoc = this.Document[DocData];
const numInlines = NumCast(textDoc.inlineTextCount);
@@ -117,7 +140,7 @@ export class RichTextRules {
textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
textDoc[inlineFieldKey] = ''; // set a default value for the annotation
const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+ const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true });
const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
const sm = state.storedMarks || undefined;
const replaced = node
@@ -241,7 +264,7 @@ export class RichTextRules {
return null;
}),
- // stop using active style
+ // toggle alternate text UI %/
new InputRule(new RegExp(/%\//), (state, match, start, end) => {
setTimeout(this.TextBox.cycleAlternateText);
return state.tr.deleteRange(start, end);
@@ -265,49 +288,57 @@ export class RichTextRules {
// [[fieldKey]] => show field
// [[fieldKey=value]] => show field and also set its value
// [[fieldKey:docTitle]] => show field of doc
- new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
- const fieldKey = match[1];
- const docTitle = match[3]?.replace(':', '');
- const value = match[2]?.substring(1);
- const linkToDoc = (target: Doc) => {
- const rstate = this.TextBox.EditorView?.state;
- const selection = rstate?.selection.$from.pos;
- if (rstate) {
- this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
- }
+ new InputRule(
+ new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-z,A-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1];
+ const docTitle = match[3]?.replace(':', '');
+ const value = match[2]?.substring(1);
+ const linkToDoc = (target: Doc) => {
+ const rstate = this.TextBox.EditorView?.state;
+ const selection = rstate?.selection.$from.pos;
+ if (rstate) {
+ this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+ }
- DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
+ DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
- const fstate = this.TextBox.EditorView?.state;
- if (fstate && selection) {
- this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
- }
- };
- const getTitledDoc = (docTitle: string) => {
- if (!DocServer.FindDocByTitle(docTitle)) {
- Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
- }
- const titledDoc = DocServer.FindDocByTitle(docTitle);
- return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
- };
- if (!fieldKey) {
- if (docTitle) {
- const target = getTitledDoc(docTitle);
- if (target) {
- setTimeout(() => linkToDoc(target));
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate && selection) {
+ this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+ }
+ };
+ const getTitledDoc = (docTitle: string) => {
+ if (!DocServer.FindDocByTitle(docTitle)) {
+ Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
}
+ const titledDoc = DocServer.FindDocByTitle(docTitle);
+ return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
+ };
+ if (!fieldKey) {
+ if (docTitle) {
+ const target = getTitledDoc(docTitle);
+ if (target) {
+ setTimeout(() => linkToDoc(target));
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ }
+ }
+ return state.tr;
}
- return state.tr;
- }
- if (value !== '' && value !== undefined) {
- const num = value.match(/^[0-9.]$/);
- this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
- }
- const target = getTitledDoc(docTitle);
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
- return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
- }),
+ if (value?.includes(',')) {
+ const values = value.split(',');
+ const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
+ this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
+ } else if (value !== '' && value !== undefined) {
+ const num = value.match(/^[0-9.]$/);
+ this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+ }
+ const target = getTitledDoc(docTitle);
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+ return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
+ },
+ { inCode: true }
+ ),
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
// wiki:title
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];
},
},
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 31f001b11..4706a97fa 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -179,6 +179,7 @@ export const nodes: { [index: string]: NodeSpec } = {
dashComment: {
attrs: {
docId: { default: '' },
+ reflow: { default: true },
},
inline: true,
group: 'inline',
@@ -275,6 +276,17 @@ export const nodes: { [index: string]: NodeSpec } = {
},
},
+ paintButton: {
+ inline: true,
+ attrs: {},
+ group: 'inline',
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ['div', { ...node.attrs, ...attrs }];
+ },
+ },
+
video: {
inline: true,
attrs: {
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;