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/AudioBox.tsx14
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx191
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx72
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx79
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx41
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx9
-rw-r--r--src/client/views/nodes/DocumentView.scss8
-rw-r--r--src/client/views/nodes/DocumentView.tsx282
-rw-r--r--src/client/views/nodes/EquationBox.tsx16
-rw-r--r--src/client/views/nodes/FieldView.tsx8
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx53
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx35
-rw-r--r--src/client/views/nodes/ImageBox.tsx102
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx68
-rw-r--r--src/client/views/nodes/LabelBox.tsx10
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx6
-rw-r--r--src/client/views/nodes/LinkBox.tsx103
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx6
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx6
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx796
-rw-r--r--src/client/views/nodes/MapBox/MapPushpinBox.tsx4
-rw-r--r--src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx4
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx12
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss322
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx36
-rw-r--r--src/client/views/nodes/VideoBox.tsx21
-rw-r--r--src/client/views/nodes/WebBox.tsx77
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss33
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx160
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx7
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx4
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss15
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx358
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts96
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx289
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts138
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts21
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts36
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx6
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx18
40 files changed, 1713 insertions, 1849 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8a38ef663..c685ec66f 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -22,6 +22,7 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent';
import './AudioBox.scss';
import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
import { PinProps, PresBox } from './trails';
+import { OpenWhere } from './DocumentView';
/**
* AudioBox
@@ -383,7 +384,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
newDoc.overlayY = NumCast(this.Document.y) + NumCast(this.layoutDoc._height);
Doc.AddToMyOverlay(newDoc);
} else {
- this._props.addDocument?.(newDoc);
+ this._props.addDocTab(newDoc, OpenWhere.addRight);
}
}),
false
@@ -606,7 +607,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
<div
className="audiobox-file"
style={{
- pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none',
flexDirection: this.miniPlayer ? 'row' : 'column',
justifyContent: this.miniPlayer ? 'flex-start' : 'space-between',
}}>
@@ -662,9 +662,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
max="1"
value={this._muted ? 0 : this._volume}
className="toolbar-slider volume"
- onPointerDown={(e: React.PointerEvent) => {
- e.stopPropagation();
- }}
+ onPointerDown={e => e.stopPropagation()}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
/>
</div>
@@ -691,8 +689,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
value={this.timeline?._zoomFactor ?? 1}
className="toolbar-slider"
id="zoom-slider"
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))}
+ onPointerDown={e => e.stopPropagation()}
+ onChange={e => this.zoom(Number(e.target.value))}
/>
</div>
)}
@@ -752,7 +750,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
render() {
return (
- <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined }}>
+ <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this._isAnyChildContentActive || this._props.isContentActive() ? 'all' : 'none' }}>
{!this.path ? this.recordingControls : this.playbackControls}
</div>
);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a0a64ab59..0d0a7c623 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { OmitKeys, numberRange } from '../../../Utils';
@@ -12,14 +12,17 @@ import { DocumentManager } from '../../util/DocumentManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
import { DocComponent } from '../DocComponent';
-import { ObservableReactComponent } from '../ObservableReactComponent';
import { StyleProp } from '../StyleProvider';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import { FieldViewProps } from './FieldView';
+import { TransitionTimer } from '../../../fields/DocSymbols';
-export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps {
+/// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need
+/// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
+const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition'];
+interface freeFormProps {
x: number;
y: number;
z: number;
@@ -33,15 +36,48 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView
opacity?: number;
highlight?: boolean;
transition?: string;
+}
+export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
RenderCutoffProvider: (doc: Doc) => boolean;
CollectionFreeFormView: CollectionFreeFormView;
}
@observer
-export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> {
- constructor(props: CollectionFreeFormDocumentViewWrapperProps) {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() {
+ get displayName() { // this makes mobx trace() statements more descriptive
+ return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
+ } // prettier-ignore
+ public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
+ public static animFields: { key: string; val?: number }[] = [
+ { key: 'x' },
+ { key: 'y' },
+ { key: 'opacity', val: 1 },
+ { key: '_height' },
+ { key: '_width' },
+ { key: '_rotation', val: 0 },
+ { key: '_layout_scrollTop' },
+ { key: '_currentFrame' },
+ { key: 'freeform_scale', val: 1 },
+ { key: 'freeform_panX' },
+ { key: 'freeform_panY' },
+ ]; // fields that are configured to be animatable using animation frames
+ public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
+ public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
+
+ constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) {
super(props);
makeObservable(this);
}
+
+ get WrapperKeys() {
+ // each of these keys is set by the CollectionView and passed via props. however, if any one of these props changes
+ // (or any other prop), then it's as if they all change.
+ // Anything that accesses these props will then invalidate unncessarily.
+ // To avoid this, we copy these prop values into local observables. Now when 'props' changes, nothing invalidates.
+ // Instead, we copy each values into its observable which ohnly triggers invalidations if the new value is different
+ // than the old value, and then only things that access that observable will invalidate.
+ return freeFormPropsKeys
+ .map(key => ({upper:key[0].toUpperCase() + key.substring(1), lower:key})); // prettier-ignore
+ }
@observable X = this.props.x;
@observable Y = this.props.y;
@observable Z = this.props.z;
@@ -55,106 +91,35 @@ export class CollectionFreeFormDocumentViewWrapper extends ObservableReactCompon
@observable Height = this.props.height;
@observable AutoDim = this.props.autoDim;
@observable Transition = this.props.transition;
- CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
- RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
- get Document() {
- return this._props.Document;
- }
- @computed get WrapperKeys() {
- return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', ''))
- .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore
+ componentDidMount(): void {
+ if (this.props.transition && !this.Document[TransitionTimer]) {
+ const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]);
+ this.Document[TransitionTimer] = setTimeout(
+ action(() => (this.Document[TransitionTimer] = this.Transition = undefined)),
+ num
+ );
+ }
}
- // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing.
- // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
- w_X = () => this.X; // prettier-ignore
- w_Y = () => this.Y; // prettier-ignore
- w_Z = () => this.Z; // prettier-ignore
- w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore
- w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore
- w_Opacity = () => this.Opacity; // prettier-ignore
- w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore
- w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore
- w_Highlight = () => this.Highlight; // prettier-ignore
- w_Width = () => this.Width; // prettier-ignore
- w_Height = () => this.Height; // prettier-ignore
- w_AutoDim = () => this.AutoDim; // prettier-ignore
- w_Transition = () => this.Transition; // prettier-ignore
-
- PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
- PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore
-
- componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) {
+ componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) {
super.componentDidUpdate(prevProps);
this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])));
}
- render() {
- const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function });
- return (
- <CollectionFreeFormDocumentView
- {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore
- {...layoutProps}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- />
- );
- }
-}
-export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- w_X: () => number;
- w_Y: () => number;
- w_Z: () => number;
- w_ZIndex?: () => number;
- w_Rotation?: () => number;
- w_Color: () => string;
- w_BackgroundColor: () => string;
- w_Opacity: () => number | undefined;
- w_Highlight: () => boolean | undefined;
- w_Transition: () => string | undefined;
- w_Width: () => number;
- w_Height: () => number;
- PanelWidth: () => number;
- PanelHeight: () => number;
- RenderCutoffProvider: (doc: Doc) => boolean;
- CollectionFreeFormView: CollectionFreeFormView;
-}
-
-@observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
- constructor(props: CollectionFreeFormDocumentViewProps) {
- super(props);
- makeObservable(this);
- }
- get displayName() { // this makes mobx trace() statements more descriptive
- return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
- } // prettier-ignore
- public static animFields: { key: string; val?: number }[] = [
- { key: 'x' },
- { key: 'y' },
- { key: 'opacity', val: 1 },
- { key: '_height' },
- { key: '_width' },
- { key: '_rotation', val: 0 },
- { key: '_layout_scrollTop' },
- { key: '_currentFrame' },
- { key: 'freeform_scale', val: 1 },
- { key: 'freeform_panX' },
- { key: 'freeform_panY' },
- ]; // fields that are configured to be animatable using animation frames
- public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
- public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
- get CollectionFreeFormView() {
- return this._props.CollectionFreeFormView;
- }
+ CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
+ // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
+ DataTransition = () => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore
+ RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
+ PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
+ PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore
styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
if (doc === this.layoutDoc) {
- switch (property) {
- case StyleProp.Opacity: return this._props.w_Opacity(); // only change the opacity for this specific document, not its children
- case StyleProp.BackgroundColor: return this._props.w_BackgroundColor();
- case StyleProp.Color: return this._props.w_Color();
+ switch (property.split(':')[0]) {
+ case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children
+ case StyleProp.BackgroundColor: return this.BackgroundColor;
+ case StyleProp.Color: return this.Color;
} // prettier-ignore
}
return this._props.styleProvider?.(doc, props, property);
@@ -253,14 +218,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
nudge = (x: number, y: number) => {
const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y);
- this.Document.x = this._props.w_X() + locX;
- this.Document.y = this._props.w_Y() + locY;
+ this.Document.x = this.X + locX;
+ this.Document.y = this.Y + locY;
};
screenToLocalTransform = () =>
this._props
.ScreenToLocalTransform()
- .translate(-this._props.w_X(), -this._props.w_Y())
- .rotateDeg(-(this._props.w_Rotation?.() || 0));
+ .translate(-this.X, -this.Y)
+ .rotateDeg(-(this.Rotation || 0));
returnThis = () => this;
/// this indicates whether the doc view is activated because of its relationshop to a group
@@ -270,34 +235,36 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
// undefined - this is not activated by a group
isGroupActive = () => {
if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined;
- const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
+ const backColor = this.BackgroundColor;
+ const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent');
return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined;
};
- public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
render() {
TraceMobx();
- const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore
+
return (
<div
className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName}
style={{
- 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?.() || StrCast(this.Document.dataTransition),
- zIndex: this._props.w_ZIndex?.(),
- display: this._props.w_Width?.() ? undefined : 'none',
+ width: this.PanelWidth(),
+ height: this.PanelHeight(),
+ transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`,
+ transition: this.DataTransition(),
+ zIndex: this.ZIndex,
+ display: this.Width ? undefined : 'none',
}}>
- {this._props.RenderCutoffProvider(this.Document) ? (
- <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} />
+ {this.RenderCutoffProvider(this.Document) ? (
+ <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
) : (
<DocumentView
- {...passOnProps}
- DataTransition={this._props.w_Transition}
+ {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
+ DataTransition={this.DataTransition}
CollectionFreeFormDocumentView={this.returnThis}
styleProvider={this.styleProvider}
ScreenToLocalTransform={this.screenToLocalTransform}
isGroupActive={this.isGroupActive}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
/>
)}
</div>
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 116dc48a6..9ffdc350d 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -4,15 +4,18 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
-import { DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { RichTextField } from '../../../fields/RichTextField';
+import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
import { DocUtils, Docs } from '../../documents/Documents';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
import { StyleProp } from '../StyleProvider';
import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { KeyValueBox } from './KeyValueBox';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { PinProps, PresBox } from './trails';
@observer
@@ -66,8 +69,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
action((e, doubleTap) => {
if (doubleTap) {
this._isAnyChildContentActive = true;
- if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
- if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
}
}),
false,
@@ -128,14 +131,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return false;
};
- whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
-
closeDown = (e: React.PointerEvent, which: string) => {
setupMoveUpEvents(
this,
e,
e => {
- const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move');
+ const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move);
de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
this.clearDoc(which);
return addDocument(doc);
@@ -156,6 +157,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+
+ /**
+ * Tests for whether a comparison box slot (ie, before or after) has renderable text content
+ * @param whichSlot field key for start or end slot
+ * @returns a JSX layout string if a text field is found, othwerise undefined
+ */
+ testForTextFields = (whichSlot: string) => {
+ const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string';
+ const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
+ const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
+ const layoutTemplateString =
+ slotHasText ? FormattedTextBox.LayoutString(whichSlot):
+ whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
+ altText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
+
+ // A bit hacky to try out the concept of using GPT to fill in flashcards
+ // If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
+ // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>).
+ // eg., this.text_alternate is
+ // "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
+ // where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
+ // The GPT call will put the "answer" in the second slot of the comparison (eg., text_2)
+ if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
+ var queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+ if (queryText && queryText.match(/\(\(.*\)\)/)) {
+ KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
+ }
+ }
+ return layoutTemplateString;
+ };
+
_closeRef = React.createRef<HTMLDivElement>();
render() {
const clearButton = (which: string) => {
@@ -169,18 +201,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
</div>
);
};
- const displayDoc = (which: string) => {
- const whichDoc = DocCast(this.dataDoc[which]);
+
+ /**
+ * Display the Docs in the before/after fields of the comparison. This also supports a GPT flash card use case
+ * where if there are no Docs in the slots, but the main fieldKey contains text, then
+ * @param whichSlot
+ * @returns
+ */
+ const displayDoc = (whichSlot: string) => {
+ const whichDoc = DocCast(this.dataDoc[whichSlot]);
const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
- return targetDoc ? (
+ const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot);
+ return targetDoc || layoutTemplateString ? (
<>
<DocumentView
{...this._props}
- Document={targetDoc}
- TemplateDataDocument={undefined}
+ ignoreUsePath={layoutTemplateString ? true : undefined}
+ renderDepth={this.props.renderDepth + 1}
+ LayoutTemplateString={layoutTemplateString}
+ Document={layoutTemplateString ? this.Document : targetDoc}
containerViewPath={this.DocumentView?.().docViewPath}
- moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
- removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
+ moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
+ removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
NativeWidth={returnZero}
NativeHeight={returnZero}
isContentActive={emptyFunction}
@@ -190,7 +232,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
hideLinkButton={true}
pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
/>
- {clearButton(which)}
+ {layoutTemplateString ? null : clearButton(whichSlot)}
</> // placeholder image if doc is missing
) : (
<div className="placeholder">
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 66a08f13e..22f1f7b79 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -118,8 +118,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
@action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
restoreView = (data: Doc) => {
- const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
- const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes)));
+ const changedView = data.config_dataViz && this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
+ const changedAxes = data.config_dataVizAxes && this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes)));
this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows);
this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors);
this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor;
@@ -266,7 +266,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData();
this._disposers.datavis = reaction(
() => {
- if (this.layoutDoc.dataViz_schemaLive==undefined) this.layoutDoc.dataViz_schemaLive = true;
+ if (this.layoutDoc.dataViz_schemaLive == undefined) this.layoutDoc.dataViz_schemaLive = true;
const getFrom = DocCast(this.layoutDoc.dataViz_asSchema);
const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text');
if (!keys) return;
@@ -283,42 +283,43 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
});
current.push(row);
});
- if (!this.layoutDoc._dataViz_schemaOG){ // makes a copy of the original table for the "live" toggle
- let csvRows = [];
- csvRows.push(keys.join(','));
- for (let i = 0; i < children.length-1; i++) {
- let eachRow = [];
- for (let j = 0; j < keys.length; j++) {
- var cell = children[i][keys[j]];
- if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
- eachRow.push(cell);
- }
- csvRows.push(eachRow);
+ if (!this.layoutDoc._dataViz_schemaOG) {
+ // makes a copy of the original table for the "live" toggle
+ let csvRows = [];
+ csvRows.push(keys.join(','));
+ for (let i = 0; i < children.length - 1; i++) {
+ let eachRow = [];
+ for (let j = 0; j < keys.length; j++) {
+ var cell = children[i][keys[j]];
+ if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, '');
+ eachRow.push(cell);
}
- const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
- const options = { x: 0, y: 0, title: 'schemaTable for static dataviz', _width: 300, _height: 100, type: 'text/csv' };
- const file = new File([blob], 'schemaTable for static dataviz', options);
- const loading = Docs.Create.LoadingDocument(file, options);
- DocUtils.uploadFileToDoc(file, {}, loading);
- this.layoutDoc._dataViz_schemaOG = loading;
- }
- const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc
- const ogHref = CsvCast(ogDoc[this.fieldKey])? CsvCast(ogDoc[this.fieldKey]).url.href : undefined;
- const href = CsvCast(this.Document[this.fieldKey]).url.href
- if (ogHref && !DataVizBox.datasetSchemaOG.has(href)){ // sets original dataset to the var
- const lastRow = current.pop();
- DataVizBox.datasetSchemaOG.set(href, current);
- current.push(lastRow!);
- fetch('/csvData?uri=' + ogHref)
- .then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res))));
+ csvRows.push(eachRow);
}
+ const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
+ const options = { x: 0, y: 0, title: 'schemaTable for static dataviz', _width: 300, _height: 100, type: 'text/csv' };
+ const file = new File([blob], 'schemaTable for static dataviz', options);
+ const loading = Docs.Create.LoadingDocument(file, options);
+ DocUtils.uploadFileToDoc(file, {}, loading);
+ this.layoutDoc._dataViz_schemaOG = loading;
+ }
+ const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc;
+ const ogHref = CsvCast(ogDoc[this.fieldKey]) ? CsvCast(ogDoc[this.fieldKey]).url.href : undefined;
+ const href = CsvCast(this.Document[this.fieldKey]).url.href;
+ if (ogHref && !DataVizBox.datasetSchemaOG.has(href)) {
+ // sets original dataset to the var
+ const lastRow = current.pop();
+ DataVizBox.datasetSchemaOG.set(href, current);
+ current.push(lastRow!);
+ fetch('/csvData?uri=' + ogHref).then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res))));
+ }
return current;
},
current => {
if (current) {
- const href = CsvCast(this.Document[this.fieldKey]).url.href;
- if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current);
- else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!);
+ const href = CsvCast(this.Document[this.fieldKey]).url.href;
+ if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current);
+ else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!);
}
},
{ fireImmediately: true }
@@ -399,8 +400,8 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
@action
changeLiveSchemaCheckbox = () => {
- this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive
- }
+ this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive;
+ };
render() {
const scale = this._props.NativeDimScaling?.() || 1;
@@ -427,11 +428,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im
<Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} />
</div>
- {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?(
+ {this.layoutDoc && this.layoutDoc.dataViz_asSchema ? (
<div className={'liveSchema-checkBox'} style={{ width: this._props.width }}>
- <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} />
- Display Live Updates to Canvas
- </div>
+ <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} />
+ Display Live Updates to Canvas
+ </div>
) : null}
{this.renderVizView}
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 1b239b5e5..53d1869d9 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -12,7 +12,7 @@ import { ObservableReactComponent } from '../../../ObservableReactComponent';
import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
import './Chart.scss';
-const { default: { DATA_VIZ_TABLE_ROW_HEIGHT } } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
+const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
interface TableBoxProps {
Document: Doc;
layoutDoc: Doc;
@@ -155,15 +155,14 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
},
emptyFunction,
action(e => {
- if (e.shiftKey){
- if (this._props.titleCol == col) this._props.titleCol = "";
+ if (e.shiftKey) {
+ if (this._props.titleCol == col) this._props.titleCol = '';
else this._props.titleCol = col;
this._props.selectTitleCol(this._props.titleCol);
- }
- else{
+ } else {
const newAxes = this._props.axes;
if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1);
- else if (newAxes.length > 2) newAxes[newAxes.length-1] = col;
+ else if (newAxes.length > 2) newAxes[newAxes.length - 1] = col;
else newAxes.push(col);
this._props.selectAxes(newAxes);
}
@@ -220,12 +219,22 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
<th
key={this.columns.indexOf(col)}
style={{
- color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen'
- : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? 'darkred'
- : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? 'darkblue' : undefined,
- background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb'
- : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? '#Fbdbdb'
- : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? '#c6ebf7' : undefined,
+ color:
+ this._props.axes.slice().reverse().lastElement() === col
+ ? 'darkgreen'
+ : this._props.axes.length > 2 && this._props.axes.lastElement() === col
+ ? 'darkred'
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+ ? 'darkblue'
+ : undefined,
+ background:
+ this._props.axes.slice().reverse().lastElement() === col
+ ? '#E3fbdb'
+ : this._props.axes.length > 2 && this._props.axes.lastElement() === col
+ ? '#Fbdbdb'
+ : this._props.axes.lastElement() === col || (this._props.axes.length > 2 && this._props.axes[1] == col)
+ ? '#c6ebf7'
+ : undefined,
// blue: #ADD8E6
// green: #E3fbdb
// red: #Fbdbdb
@@ -251,10 +260,10 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> {
}}>
{this.columns.map(col => {
var colSelected = false;
- if (this._props.axes.length>2) colSelected = this._props.axes[0]==col || this._props.axes[1]==col || this._props.axes[2]==col;
- else if (this._props.axes.length>1) colSelected = this._props.axes[0]==col || this._props.axes[1]==col;
- else if (this._props.axes.length>0) colSelected = this._props.axes[0]==col;
- if (this._props.titleCol==col) colSelected = true;
+ if (this._props.axes.length > 2) colSelected = this._props.axes[0] == col || this._props.axes[1] == col || this._props.axes[2] == col;
+ else if (this._props.axes.length > 1) colSelected = this._props.axes[0] == col || this._props.axes[1] == col;
+ else if (this._props.axes.length > 0) colSelected = this._props.axes[0] == col;
+ if (this._props.titleCol == col) colSelected = true;
return (
<td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
<div className="tableBox-cell">{this._props.records[rowId][col]}</div>
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 07e179246..e729e2fa2 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -7,7 +7,7 @@ import { OmitKeys, Without, emptyPath } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { AclPrivate, DocData } from '../../../fields/DocSymbols';
import { ScriptField } from '../../../fields/ScriptField';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { InkingStroke } from '../InkingStroke';
import { ObservableReactComponent } from '../ObservableReactComponent';
@@ -128,15 +128,15 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString;
if (!this.layoutDoc) return '<p>awaiting layout</p>';
if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString());
- const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string');
+ const tempLayout = DocCast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')]);
+ const layoutDoc = tempLayout ?? this.layoutDoc;
+ const layout = Cast(layoutDoc[layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(layoutDoc.layout_fieldKey, 'layout')], 'string');
if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
if (typeof layout === 'string') return layout;
return '<p>Loading layout</p>';
}
get layoutDoc() {
- // bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script
- // const template: Doc = this._props.LayoutTemplate?.() || Doc.Layout(this._props.Document, this._props.fieldKey ? Cast(this._props.Document[this._props.fieldKey], Doc, null) : undefined);
const template: Doc =
this._props.LayoutTemplate?.() ||
(this._props.LayoutTemplateString && this._props.Document) ||
@@ -156,6 +156,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte
'LayoutTemplate',
'layoutFieldKey',
'dontCenter',
+ 'DataTransition',
'contextMenuItems',
//'onClick', // don't need to omit this since it will be set
'onDoubleClickScript',
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c4dab16fb..23dada260 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -180,7 +180,6 @@
.documentView-titleWrapper,
.documentView-titleWrapper-hover {
- overflow: hidden;
color: $black;
transform-origin: top left;
top: 0;
@@ -235,6 +234,12 @@
}
}
+.documntViewInternal-dropdown {
+ > div {
+ transform-origin: top left !important;
+ }
+}
+
.contentFittingDocumentView {
position: relative;
display: flex;
@@ -257,6 +262,5 @@
.documentView-node:first-child {
position: relative;
- background: '#B59B66'; //$white;
}
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 17ebc5b5e..01c1de137 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,16 +1,16 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { Dropdown, DropdownType, Type } from 'browndash-components';
import { Howl } from 'howler';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
-import { Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils';
+import { DivWidth, Utils, emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../Utils';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
+import { PrefetchProxy } from '../../../fields/Proxy';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -20,10 +20,11 @@ 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 } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
+import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -37,9 +38,10 @@ import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent, ViewBoxInterface } from '../DocComponent';
import { EditableView } from '../EditableView';
+import { FieldsDropdown } from '../FieldsDropdown';
import { GestureOverlay } from '../GestureOverlay';
import { LightboxView } from '../LightboxView';
-import { StyleProp } from '../StyleProvider';
+import { AudioAnnoState, StyleProp } from '../StyleProvider';
import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
@@ -104,12 +106,11 @@ export interface DocumentViewProps extends FieldViewSharedProps {
childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
dragWhenActive?: boolean;
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 }[];
+ contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[];
dragConfig?: (data: DragManager.DocumentDragData) => void;
dragStarting?: () => void;
dragEnding?: () => void;
@@ -201,6 +202,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
((!this.disableClickScriptFunc && //
this.onClickHandler &&
!this._props.onBrowseClickScript?.() &&
+ !this.layoutDoc.layout_isSvg &&
this.isContentActive() !== true) ||
this.isContentActive() === false)
? 'none'
@@ -223,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.Document))
);
}
- @computed get _allLinks() {
+ @computed get _allLinks(): Doc[] {
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
}
@@ -415,7 +417,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
this.cleanupPointerEvents();
this._longPressSelector && clearTimeout(this._longPressSelector);
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType));
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && dropActionType.embed) || ((this.Document.dragAction || this._props.dragAction || undefined) as dropActionType));
}
};
@@ -459,11 +461,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc');
setToggleDetail = undoable(
- (defaultLayout: string, scriptFieldKey: 'onClick') =>
+ (scriptFieldKey: 'onClick') =>
(this.Document[scriptFieldKey] = ScriptField.MakeScript(
`toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
.replace('layout_', '')
- .replace(/^layout$/, 'detail')}", "${defaultLayout}")`,
+ .replace(/^layout$/, 'detail')}")`,
{ documentView: 'any' }
)),
'set toggle detail'
@@ -520,7 +522,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
};
onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
- if (e && this.layoutDoc._layout_hideContextMenu && Doc.noviceMode) {
+ if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
e.preventDefault();
e.stopPropagation();
//!this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false);
@@ -560,7 +562,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' })
);
- this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp }));
+ this._props
+ .contextMenuItems?.()
+ .forEach(
+ item =>
+ item.label &&
+ cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp })
+ );
if (!this.Document.isFolder) {
const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null);
@@ -597,7 +605,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
if (!this.Document.annotationOn) {
onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
!Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
- cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+ !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
} else if (LinkManager.Links(this.Document).length) {
onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' });
onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
@@ -704,7 +712,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
panelHeight = () => this._props.PanelHeight() - this.headerMargin;
screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHandler;
- setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = height);
+ setHeight = (height: number) => !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height));
setContentView = action((view: ViewBoxInterface) => (this._componentView = view));
isContentActive = (): boolean | undefined => this._isContentActive;
childFilters = () => [...this._props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
@@ -786,35 +794,31 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
}
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) => {
- const filteredFields = Object.entries(DocOptions).reduce((set, [field, opts]) => (opts.filterable ? set.add(field) : set), new Set(reqdFields));
+ fieldsDropdown = (placeholder: string) => {
return (
- <div style={{ width: dropdownWidth }}>
- <div
- ref={action((r: any) => r && (this._titleDropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))}
- onPointerDown={action(e => (this._changingTitleField = true))}
- style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}>
- <Dropdown
- activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))}
- selectedVal={placeholder}
- setSelectedVal={onChange}
- color={SettingsManager.userColor}
- background={SettingsManager.userVariantColor}
- type={Type.TERT}
- closeOnSelect={true}
- dropdownType={DropdownType.SELECT}
- items={Array.from(filteredFields).map(facet => ({ val: facet, text: facet }))}
- width={100}
- fillWidth
- />
- </div>
+ <div
+ ref={action((r: any) => r && (this._titleDropDownInnerWidth = DivWidth(r)))}
+ onPointerDown={action(e => (this._changingTitleField = true))}
+ style={{ width: 'max-content', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, transformOrigin: 'left', transform: `scale(${this.titleHeight / 30 /* height of Dropdown */})` }}>
+ <FieldsDropdown
+ Document={this.Document}
+ placeholder={placeholder}
+ selectFunc={action((field: string | number) => {
+ if (this.layoutDoc.layout_showTitle) {
+ this.layoutDoc._layout_showTitle = field;
+ } else if (!this._props.layout_showTitle) {
+ Doc.UserDoc().layout_showTitle = field;
+ }
+ this._changingTitleField = false;
+ })}
+ menuClose={action(() => (this._changingTitleField = false))}
+ />
</div>
);
};
/**
* displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by
- * 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]
+ * setting layout_showTitle using the format: field1[:hover]
**/
@computed get titleView() {
const showTitle = this.layout_showTitle?.split(':')[0];
@@ -841,22 +845,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
background,
pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this._props.isDocumentActive?.() ? 'all' : undefined,
}}>
- {!dropdownWidth
- ? null
- : this.fieldsDropdown(
- [StrCast(this.layoutDoc.layout_showTitle)],
- dropdownWidth,
- StrCast(this.layoutDoc.layout_showTitle).split(':')[0],
- action((field: string | number) => {
- if (this.layoutDoc.layout_showTitle) {
- this.layoutDoc._layout_showTitle = field;
- } else if (!this._props.layout_showTitle) {
- Doc.UserDoc().layout_showTitle = field;
- }
- this._changingTitleField = false;
- }),
- action(() => (this._changingTitleField = false))
- )}
+ {!dropdownWidth ? null : (
+ <div className="documntViewInternal-dropdown" style={{ width: dropdownWidth }}>
+ {this.fieldsDropdown(showTitle)}
+ </div>
+ )}
<div
style={{
width: `calc(100% - ${dropdownWidth}px)`,
@@ -866,22 +859,29 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
}}>
<EditableView
ref={this._titleRef}
- contents={showTitle
- .split(';')
- .map(field => targetDoc[field.trim()]?.toString())
- .join(' \\ ')}
+ contents={
+ showTitle
+ .split(';')
+ .map(field => Field.toJavascriptString(this.Document[field] as Field))
+ .join(' \\ ') || '-unset-'
+ }
display="block"
oneLine={true}
fontSize={(this.titleHeight / 15) * 10}
- GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))}
+ GetValue={() =>
+ showTitle
+ .split(';')
+ .map(field => Field.toKeyValueString(this.Document, field))
+ .join('\\')
+ }
SetValue={undoBatch((input: string) => {
- if (input?.startsWith('#')) {
+ if (input?.startsWith('$')) {
if (this.layoutDoc.layout_showTitle) {
- this.layoutDoc._layout_showTitle = input?.substring(1);
+ this.layoutDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else if (!this._props.layout_showTitle) {
- Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date';
+ Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title';
}
- } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') {
+ } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') {
KeyValueBox.SetField(targetDoc, showTitle, input);
}
return true;
@@ -908,6 +908,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
fieldKey={this.layout_showCaption}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
+ rootSelected={this.rootSelected}
noSidebar={true}
dontScale={true}
renderDepth={this._props.renderDepth}
@@ -925,7 +926,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
: this.docContents ?? (
<div
className="documentView-node"
- id={this.Document[Id]}
+ id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined}
style={{
...style,
background: this.backgroundBoxColor,
@@ -1052,49 +1053,41 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
let gumStream: any;
let recorder: any;
- navigator.mediaDevices
- .getUserMedia({
- audio: true,
- })
- .then(function (stream) {
- let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
- if (audioTextAnnos) audioTextAnnos.push('');
- else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
- DictationManager.Controls.listen({
- interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
- continuous: { indefinite: false },
- }).then(results => {
- if (results && [DictationManager.Controls.Infringed].includes(results)) {
- DictationManager.Controls.stop();
- }
- onEnd?.();
- });
-
- gumStream = stream;
- recorder = new MediaRecorder(stream);
- recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
- if (!(result instanceof Error)) {
- const audioField = new AudioField(result.accessPaths.agnostic.client);
- const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
- if (audioAnnos === undefined) {
- dataDoc[field + '_audioAnnotations'] = new List([audioField]);
- } else {
- audioAnnos.push(audioField);
- }
- }
- };
- //runInAction(() => (dataDoc.audioAnnoState = 'recording'));
- recorder.start();
- const stopFunc = () => {
- recorder.stop();
- DictationManager.Controls.stop(false);
- runInAction(() => (dataDoc.audioAnnoState = 'stopped'));
- gumStream.getAudioTracks()[0].stop();
- };
- if (onRecording) onRecording(stopFunc);
- else setTimeout(stopFunc, 5000);
+ navigator.mediaDevices.getUserMedia({ audio: true }).then(function (stream) {
+ let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
+ if (audioTextAnnos) audioTextAnnos.push('');
+ else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
+ DictationManager.Controls.listen({
+ interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ onEnd?.();
});
+
+ gumStream = stream;
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
+ if (!(result instanceof Error)) {
+ const audioField = new AudioField(result.accessPaths.agnostic.client);
+ const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
+ if (audioAnnos) audioAnnos.push(audioField);
+ else dataDoc[field + '_audioAnnotations'] = new List([audioField]);
+ }
+ };
+ recorder.start();
+ const stopFunc = () => {
+ recorder.stop();
+ DictationManager.Controls.stop(false);
+ dataDoc.audioAnnoState = AudioAnnoState.stopped;
+ gumStream.getAudioTracks()[0].stop();
+ };
+ if (onRecording) onRecording(stopFunc);
+ else setTimeout(stopFunc, 5000);
+ });
}
}
@@ -1107,8 +1100,18 @@ 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.
-
+ /**
+ * This is used to create an id for tracking a Doc. Since the Doc can be in a regular view and in the lightbox at
+ * the same time, this creates a different version of the id depending on whether the search scope will be in the lightbox or not.
+ * @param inLightbox is the id scoped to the lightbox
+ * @param id the id
+ * @returns
+ */
+ public static UniquifyId(inLightbox: boolean | undefined, id: string) {
+ return (inLightbox ? 'lightbox-' : '') + id;
+ }
+ public ViewGuid = DocumentView.UniquifyId(LightboxView.Contains(this), Utils.GenerateGuid()); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations.
+ public DocUniqueId = DocumentView.UniquifyId(LightboxView.Contains(this), this.Document[Id]);
@computed public static get exploreMode() {
return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
}
@@ -1249,7 +1252,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 = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey);
+ public setToggleDetail = (scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(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);
@@ -1271,7 +1274,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', '');
} else {
const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
- this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
+ this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished, true);
this.Document.deiconifyLayout = undefined;
this._props.bringToFront?.(this.Document);
}
@@ -1279,25 +1282,25 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
public playAnnotation = () => {
const self = this;
- const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
+ const audioAnnoState = this.dataDoc.audioAnnoState ?? AudioAnnoState.stopped;
const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null);
const anno = audioAnnos?.lastElement();
if (anno instanceof AudioField) {
switch (audioAnnoState) {
- case 'stopped':
+ case AudioAnnoState.stopped:
this.dataDoc[AudioPlay] = new Howl({
src: [anno.url.href],
format: ['mp3'],
autoplay: true,
loop: false,
volume: 0.5,
- onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
+ onend: action(() => (self.dataDoc.audioAnnoState = AudioAnnoState.stopped)),
});
- this.dataDoc.audioAnnoState = 'playing';
+ this.dataDoc.audioAnnoState = AudioAnnoState.playing;
break;
- case 'playing':
+ case AudioAnnoState.playing:
this.dataDoc[AudioPlay]?.stop();
- this.dataDoc.audioAnnoState = 'stopped';
+ this.dataDoc.audioAnnoState = AudioAnnoState.stopped;
break;
}
}
@@ -1327,7 +1330,48 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
}, 'set custom view');
+ public static setDefaultTemplate(checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.UserDoc().defaultTextLayout;
+ }
+ const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
+ undoable(() => {
+ var tempDoc: Opt<Doc>;
+ if (view) {
+ if (!view.layoutDoc.isTemplateDoc) {
+ tempDoc = view.Document;
+ MakeTemplate(tempDoc);
+ Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
+ Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+ tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
+ } else {
+ tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
+ if (!tempDoc) {
+ tempDoc = view.Document;
+ while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto);
+ }
+ }
+ }
+ Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
+ }, 'set default template')();
+ }
+
+ /**
+ * This switches between the current view of a Doc and a specified alternate layout view.
+ * The current view of the Doc is stored in the layout_default field so that it can be restored.
+ * If the current view of the Doc is already the specified alternate layout view, this will switch
+ * back to the original layout (stored in layout_default)
+ * @param detailLayoutKeySuffix the name of the alternate layout field key (NOTE: 'layout_' will be prepended to this string to get the actual field nam)
+ */
+ public toggleDetail = (detailLayoutKeySuffix: string) => {
+ const curLayout = StrCast(this.Document.layout_fieldKey).replace('layout_', '').replace('layout', '');
+ if (!this.Document.layout_default && curLayout !== detailLayoutKeySuffix) this.Document.layout_default = curLayout;
+ const defaultLayout = StrCast(this.Document.layout_default);
+ if (this.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) this.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true);
+ else this.switchViews(true, detailLayoutKeySuffix, undefined, true);
+ };
public switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+ const batch = UndoManager.StartBatch('switchView:' + view);
runInAction(() => this._docViewInternal && (this._docViewInternal._animateScalingTo = 0.1)); // shrink doc
setTimeout(
action(() => {
@@ -1340,6 +1384,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
setTimeout(
action(() => {
this._docViewInternal && (this._docViewInternal._animateScalingTo = 0);
+ batch.end();
finished?.();
}),
Math.max(0, (this._docViewInternal?.animateScaleTime() ?? 0) - 10)
@@ -1422,7 +1467,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined;
return (
- <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
+ <div id={this.ViewGuid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
{!this.Document || !this._props.PanelWidth() ? null : (
<div
className="contentFittingDocumentView-previewDoc"
@@ -1499,9 +1544,8 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView
LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0);
});
-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 toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
+ dv.toggleDetail(detailLayoutKeySuffix);
});
ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 2e03a766a..a557cff4f 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -1,10 +1,11 @@
import { action, makeObservable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { DivHeight, DivWidth } from '../../../Utils';
import { Id } from '../../../fields/FieldSymbols';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { LightboxView } from '../LightboxView';
@@ -57,8 +58,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
keyPressed = (e: KeyboardEvent) => {
- const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', ''));
- const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', ''));
+ const _height = DivHeight(this._ref.current!.element.current);
+ const _width = DivWidth(this._ref.current!.element.current);
if (e.key === 'Enter') {
const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', {
title: '# math',
@@ -79,7 +80,9 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
_height: 300,
backgroundColor: 'white',
});
+ const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
this._props.addDocument?.(graph);
+ link && this._props.addDocument?.(link);
e.stopPropagation();
}
if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document);
@@ -93,11 +96,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (this.layoutDoc._nativeWidth) {
// if equation has been scaled then editing the expression must also edit the native dimensions to keep the aspect ratio
const prevNwidth = NumCast(this.layoutDoc._nativeWidth);
- const prevNheight = NumCast(this.layoutDoc._nativeHeight);
- this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', '')));
- this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', '')));
+ const newNwidth = (this.layoutDoc._nativeWidth = Math.max(35, Number(style.width.replace('px', ''))));
+ const newNheight = (this.layoutDoc._nativeHeight = Math.max(25, Number(style.height.replace('px', ''))));
this.layoutDoc._width = (NumCast(this.layoutDoc._width) * NumCast(this.layoutDoc._nativeWidth)) / prevNwidth;
- this.layoutDoc._height = (NumCast(this.layoutDoc._height) * NumCast(this.layoutDoc._nativeHeight)) / prevNheight;
+ this.layoutDoc._height = (NumCast(this.layoutDoc._width) * newNheight) / newNwidth;
} else {
this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', '')));
this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', '')));
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 8a49b4757..771856788 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -11,6 +11,7 @@ import { ViewBoxInterface } from '../DocComponent';
import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
import { DocumentView, OpenWhere } from './DocumentView';
import { PinProps } from './trails';
+import { computed } from 'mobx';
export interface FocusViewOptions {
willPan?: boolean; // determines whether to pan to target document
@@ -54,6 +55,7 @@ export interface FieldViewSharedProps {
ignoreAutoHeight?: boolean;
disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors
+ ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs)
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
containerViewPath?: () => DocumentView[];
fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
@@ -90,6 +92,7 @@ export interface FieldViewSharedProps {
waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<string>;
+ suppressSetHeight?: boolean;
}
/**
@@ -121,9 +124,12 @@ export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string) {
return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />"
}
+ @computed get fieldval() {
+ return this.props.Document[this.props.fieldKey];
+ }
render() {
- const field = this.props.Document[this.props.fieldKey];
+ const field = this.fieldval;
// prettier-ignore
if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>;
if (field === undefined) return <p>{'<null>'}</p>;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 3577cc8d9..57ae92359 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,13 +1,12 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
-import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { SelectionManager } from '../../../util/SelectionManager';
import { SettingsManager } from '../../../util/SettingsManager';
@@ -61,29 +60,22 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
}
@observable noTooltip = false;
- showTemplate = (): void => {
- const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
- dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight);
- };
- dragAsTemplate = (): void => {
- this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
- };
- useAsPrototype = (): void => {
- this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
- };
+ showTemplate = (dragFactory: Doc) => this._props.addDocTab(dragFactory, OpenWhere.addRight);
specificContextMenu = (): void => {
- if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) {
- const cm = ContextMenu.Instance;
- cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
- cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
- cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
+ const dragFactory = DocCast(this.layoutDoc.dragFactory);
+ if (!Doc.noviceMode && dragFactory) {
+ ContextMenu.Instance.addItem({ description: 'Show Template', event: () => this.showTemplate(dragFactory), icon: 'tag' });
}
};
- // Determining UI Specs
+ /**
+ * this chooses the appropriate title for the label
+ * if the Document is a template, then we use the title of the data doc that it renders
+ * otherwise, we use the Document's title itself.
+ */
@computed get label() {
- return StrCast(this.dataDoc.icon_label, StrCast(this.Document.title));
+ return StrCast(this.Document.isTemplateDoc ? this.dataDoc.title : this.Document.title);
}
Icon = (color: string, iconFalse?: boolean) => {
let icon;
@@ -308,13 +300,16 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
);
}
+ @observable _hackToRecompute = 0; // bcz: ugh ... <Toggle>'s toggleStatus initializes but doesn't track its value after a click. so a click that does nothing to the toggle state will toggle the button anyway. this forces the Toggle to re-read the ToggleStatus value.
+
@computed get toggleButton() {
// Determine the type of toggle button
const buttonText = StrCast(this.dataDoc.buttonText);
const tooltip = StrCast(this.Document.toolTip);
const script = ScriptCast(this.Document.onClick);
- const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
+ const double = ScriptCast(this.Document.onDoubleClick);
+ const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false;
// Colors
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
@@ -330,7 +325,19 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
//background={SettingsManager.userBackgroundColor}
icon={this.Icon(color)!}
label={this.label}
- onPointerDown={() => script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false })}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnTrue,
+ emptyFunction,
+ action((e, doubleTap) => {
+ (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+ doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+ this._hackToRecompute = this._hackToRecompute + 1;
+ })
+ )
+ }
/>
);
}
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 2e7a2120e..a86bdbd79 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -7,12 +7,13 @@ import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { Cast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
import { PinProps, PresBox } from './trails';
+import { LinkManager } from '../../util/LinkManager';
@observer
export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -33,7 +34,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
componentDidMount() {
this._props.setContentViewBox?.(this);
reaction(
- () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
+ () => [this.graphFuncs, this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
() => this.createGraph()
);
}
@@ -45,11 +46,26 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
if (addAsAnnotation) this.addDocument(anchor);
return anchor;
};
+ @computed get graphFuncs() {
+ const links = LinkManager.Instance.getAllRelatedLinks(this.Document)
+ .map(d => LinkManager.getOppositeAnchor(d, this.Document))
+ .filter(d => d)
+ .map(d => d!);
+ const funcs = links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc =>
+ StrCast(doc.text, 'x^2')
+ .replace(/\\sqrt/g, 'sqrt')
+ .replace(/\\frac\{(.*)\}\{(.*)\}/g, '($1/$2)')
+ .replace(/\\left/g, '')
+ .replace(/\\right/g, '')
+ .replace(/\{/g, '')
+ .replace(/\}/g, '')
+ );
+ return funcs;
+ }
createGraph = (ele?: HTMLDivElement) => {
this._plotEle = ele || this._plotEle;
const width = this._props.PanelWidth();
const height = this._props.PanelHeight();
- const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
try {
this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
this._plot = functionPlot({
@@ -59,7 +75,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
grid: true,
- data: fns.map(fn => ({
+ data: this.graphFuncs.map(fn => ({
fn,
// derivative: { fn: "2 * x", updateOnMouseMove: true }
})),
@@ -72,7 +88,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData?.droppedDocuments.length) {
- const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true);
+ const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
+ ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
+ if (res) {
+ const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+ link && this._props.addDocument?.(link);
+ }
+ return res;
+ }, true);
!added && e.preventDefault();
e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
return added;
@@ -104,7 +127,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
{this.theGraph}
<div
style={{
- display: this._props.isSelected() ? 'none' : undefined,
+ display: this._props.isContentActive() ? 'none' : undefined,
position: 'absolute',
width: '100%',
height: '100%',
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 923aead64..bb1f70f97 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,20 +1,23 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
+import { Colors } from 'browndash-components';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
+import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
@@ -23,29 +26,39 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../DocComponent';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { OverlayView } from '../OverlayView';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
import './ImageBox.scss';
import { PinProps, PresBox } from './trails';
+export class ImageEditorData {
+ private static _instance: ImageEditorData;
+ private static get imageData() { return (ImageEditorData._instance ?? new ImageEditorData()).imageData; } // prettier-ignore
+ @observable imageData: { rootDoc: Doc | undefined; open: boolean; source: string; addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean> } = observable({ rootDoc: undefined, open: false, source: '', addDoc: undefined });
+ @action private static set = (open: boolean, rootDoc: Doc | undefined, source: string, addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) => (this._instance.imageData = { open, rootDoc, source, addDoc });
+
+ constructor() {
+ makeObservable(this);
+ ImageEditorData._instance = this;
+ }
+
+ public static get Open() { return ImageEditorData.imageData.open; } // prettier-ignore
+ public static get Source() { return ImageEditorData.imageData.source; } // prettier-ignore
+ public static get RootDoc() { return ImageEditorData.imageData.rootDoc; } // prettier-ignore
+ public static get AddDoc() { return ImageEditorData.imageData.addDoc; } // prettier-ignore
+ public static set Open(open: boolean) { ImageEditorData.set(open, this.imageData.rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+ public static set Source(source: string) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, source, this.imageData.addDoc); } // prettier-ignore
+ public static set RootDoc(rootDoc: Opt<Doc>) { ImageEditorData.set(this.imageData.open, rootDoc, this.imageData.source, this.imageData.addDoc); } // prettier-ignore
+ public static set AddDoc(addDoc: Opt<(doc: Doc | Doc[], annotationKey?: string) => boolean>) { ImageEditorData.set(this.imageData.open, this.imageData.rootDoc, this.imageData.source, addDoc); } // prettier-ignore
+}
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
-
- @observable public static imageRootDoc: Doc | undefined = undefined;
- @observable public static imageEditorOpen: boolean = false;
- @observable public static imageEditorSource: string = '';
- @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined = undefined;
- @action public static setImageEditorOpen(open: boolean) {
- ImageBox.imageEditorOpen = open;
- }
- @action public static setImageEditorSource(source: string) {
- ImageBox.imageEditorSource = source;
- }
private _ignoreScroll = false;
private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -134,13 +147,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) {
added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => {
this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
- return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
+ return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop);
}, true);
} else if (de.altKey || !this.dataDoc[this.fieldKey]) {
const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
const targetField = Doc.LayoutFieldKey(layoutDoc);
const targetDoc = layoutDoc[DocData];
if (targetDoc[targetField] instanceof ImageField) {
+ added = true;
this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
@@ -158,8 +172,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
@undoBatch
setNativeSize = action(() => {
- const scaling = (this.DocumentView?.().screenToViewTransform().Scale || 1) / NumCast(this.layoutDoc._freeform_scale, 1);
- const nscale = NumCast(this._props.PanelWidth()) / scaling;
+ const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
@@ -242,10 +255,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
funcs.push({
description: 'Open Image Editor',
event: action(() => {
- ImageBox.setImageEditorOpen(true);
- ImageBox.setImageEditorSource(this.choosePath(field.url));
- ImageBox.addDoc = this._props.addDocument;
- ImageBox.imageRootDoc = this.Document;
+ ImageEditorData.Open = true;
+ ImageEditorData.Source = this.choosePath(field.url);
+ ImageEditorData.AddDoc = this._props.addDocument;
+ ImageEditorData.RootDoc = this.Document;
}),
icon: 'pencil-alt',
});
@@ -258,7 +271,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
@@ -296,7 +309,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
ref={this._overlayIconRef}
onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))}
style={{
- display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none',
+ display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none',
width: 'min(10%, 25px)',
height: 'min(10%, 25px)',
background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
@@ -310,13 +323,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
@computed get paths() {
const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
- const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
- const altpaths = alts
- .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
- .filter(url => url)
- .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+ const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images
+ const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png'));
+ const altpaths =
+ alts
+ ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl))
+ .filter(url => url)
+ .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents
const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
- return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')];
+ return paths.length ? paths : [defaultUrl.href];
}
@observable _error = '';
@@ -325,7 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
@computed get content() {
TraceMobx();
- const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor));
+ const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) ?? Colors.WHITE);
const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1;
const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
@@ -369,6 +384,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
}
screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale);
marqueeDown = (e: React.PointerEvent) => {
+ if (!this.dataDoc[this.fieldKey]) return this.chooseImage();
if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
@@ -467,4 +483,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
</div>
);
}
+
+ public chooseImage = () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.multiple = true;
+ input.accept = 'image/*';
+ input.onchange = async _e => {
+ const file = input.files?.[0];
+ if (file) {
+ const disposer = OverlayView.ShowSpinner();
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
+ if (result instanceof Error) {
+ alert('Error uploading files - possibly due to unsupported file types');
+ } else {
+ this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
+ !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+ }
+ disposer();
+ } else {
+ console.log('No file selected');
+ }
+ };
+ input.click();
+ };
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 39a45693e..31a2367fc 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -10,7 +10,7 @@ import { DocCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
-import { CompileScript, CompiledScript, ScriptOptions } from '../../util/Scripting';
+import { CompiledScript } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -55,7 +55,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
@observable _splitPercentage = 50;
get fieldDocToLayout() {
- return this._props.fieldKey ? DocCast(this._props.Document[this._props.fieldKey], DocCast(this._props.Document)) : this._props.Document;
+ return DocCast(this._props.Document);
}
@action
@@ -71,34 +71,52 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
}
}
};
- public static CompileKVPScript(value: string): KVPScript | undefined {
- const eq = value.startsWith('=');
- value = eq ? value.substring(1) : value;
- const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
- value = dubEq ? value.substring(2) : value;
- const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true };
- if (dubEq) options.typecheck = false;
- const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
- return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
+ /**
+ * this compiles a string as a script after parsing off initial characters that determine script parameters
+ * if the script starts with '=', then it will be stored on the delegate of the Doc, otherise on the data doc
+ * if the script then starts with a ':=', then it will be treated as ComputedField,
+ * '$=', then it will just be a Script
+ * @param value
+ * @returns
+ */
+ public static CompileKVPScript(rawvalue: string): KVPScript | undefined {
+ const onDelegate = rawvalue.startsWith('=');
+ rawvalue = onDelegate ? rawvalue.substring(1) : rawvalue;
+ const type: 'computed' | 'script' | false = rawvalue.startsWith(':=') ? 'computed' : rawvalue.startsWith('$=') ? 'script' : false;
+ rawvalue = type ? rawvalue.substring(2) : rawvalue;
+ rawvalue = rawvalue.replace(/.*\(\((.*)\)\)/, 'dashCallChat(_setCacheResult_, this, `$1`)');
+ const value = ["'", '"', '`'].includes(rawvalue.length ? rawvalue[0] : '') || !isNaN(rawvalue as any) ? rawvalue : '`' + rawvalue + '`';
+
+ var script = ScriptField.CompileScript(rawvalue, {}, true, undefined, DocumentIconContainer.getTransformer());
+ if (!script.compiled) {
+ script = ScriptField.CompileScript(value, {}, true, undefined, DocumentIconContainer.getTransformer());
+ }
+ return !script.compiled ? undefined : { script, type, onDelegate };
}
- public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
+ public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
const { script, type, onDelegate } = kvpScript;
//const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
- let field: Field;
- if (type === 'computed') {
- field = new ComputedField(script);
- } else if (type === 'script') {
- field = new ScriptField(script);
- } else {
- const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
- if (!res.success) {
- target[key] = script.originalScript;
- return true;
+ let field: Field | undefined;
+ switch (type) {
+ case 'computed': field = new ComputedField(script); break; // prettier-ignore
+ case 'script': field = new ScriptField(script); break; // prettier-ignore
+ default: {
+ const _setCacheResult_ = (value: FieldResult) => {
+ field = value as Field;
+ if (setResult) setResult?.(value);
+ else target[key] = field;
+ };
+ const res = script.run({ this: Doc.Layout(doc), self: doc, _setCacheResult_ }, console.log);
+ if (!res.success) {
+ if (key) target[key] = script.originalScript;
+ return false;
+ }
+ field === undefined && (field = res.result instanceof Array ? new List<any>(res.result) : res.result);
}
- field = res.result;
}
+ if (!key) return false;
if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
target[key] = field;
return true;
@@ -107,10 +125,10 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
}
@undoBatch
- public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean) {
+ public static SetField(doc: Doc, key: string, value: string, forceOnDelegate?: boolean, setResult?: (value: FieldResult) => void) {
const script = this.CompileKVPScript(value);
if (!script) return false;
- return this.ApplyKVPScript(doc, key, script, forceOnDelegate);
+ return this.ApplyKVPScript(doc, key, script, forceOnDelegate, setResult);
}
onPointerDown = (e: React.PointerEvent): void => {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index fd3074a88..74e78c671 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -17,12 +17,8 @@ import './LabelBox.scss';
import { PinProps, PresBox } from './trails';
import { Docs } from '../../documents/Documents';
-export interface LabelBoxProps extends FieldViewProps {
- label?: string;
-}
-
@observer
-export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
+export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(LabelBox, fieldKey);
}
@@ -32,7 +28,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: any;
- constructor(props: LabelBoxProps) {
+ constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
}
@@ -45,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() {
}
@computed get Title() {
- return this.dataDoc.title_custom ? StrCast(this.Document.title) : this._props.label ? this._props.label : Field.toString(this.dataDoc[this.fieldKey] as Field);
+ return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title);
}
protected createDropTarget = (ele: HTMLDivElement) => {
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 00e1f04c5..ff1e62885 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -5,7 +5,7 @@ import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
import { Doc } from '../../../fields/Doc';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
import { SelectionManager } from '../../util/SelectionManager';
import { ViewBoxBaseComponent } from '../DocComponent';
@@ -13,7 +13,7 @@ import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkAnchorBox.scss';
import { LinkInfo } from './LinkDocPreview';
-const { default: { MEDIUM_GRAY }, } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
+const { MEDIUM_GRAY } = require('../global/globalCssVariables.module.scss'); // prettier-ignore
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -54,7 +54,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
if (separation > 100) {
const dragData = new DragManager.DocumentDragData([this.Document]);
- dragData.dropAction = 'embed';
+ dragData.dropAction = dropActionType.embed;
dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
return true;
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index decdbb240..3a2509c3d 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -2,18 +2,20 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
import { observer } from 'mobx-react';
import * as React from 'react';
import Xarrow from 'react-xarrows';
-import { DocData } from '../../../fields/DocSymbols';
+import { FieldResult } from '../../../fields/Doc';
+import { DocCss, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
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 { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkBox.scss';
@@ -22,8 +24,8 @@ 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
+ _disposers: { [name: string]: IReactionDisposer } = {};
+ @observable _forceAnimate: number = 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) {
@@ -38,51 +40,56 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch;
return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
};
+ _hackToSeeIfDeleted: any;
componentWillUnmount() {
- this.disposer?.();
+ this._hackToSeeIfDeleted && clearTimeout(this._hackToSeeIfDeleted);
+ Object.keys(this._disposers).forEach(key => this._disposers[key]());
}
componentDidMount() {
this._props.setContentViewBox?.(this);
- this.disposer = reaction(
- () => ({ 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 }
+ this._disposers.deleting = reaction(
+ () => !this.anchor1 && !this.anchor2 && this.DocumentView?.() && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView!())),
+ empty => empty && ((this._hackToSeeIfDeleted = setTimeout(() =>
+ (!this.anchor1 && !this.anchor2) && this._props.removeDocument?.(this.Document)
+ )), 1000) // prettier-ignore
+ );
+ this._disposers.dragging = reaction(
+ () => SnappingManager.IsDragging,
+ () => setTimeout( action(() => {// need to wait for drag manager to set 'hidden' flag on dragged DOM elements
+ const a = this.anchor1,
+ b = this.anchor2;
+ let a1 = a && document.getElementById(a.ViewGuid);
+ let a2 = b && document.getElementById(b.ViewGuid);
+ // 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 any 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;
+ }
+ })) // prettier-ignore
);
}
render() {
+ TraceMobx();
+
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)) {
+ if (a && b) {
// 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;
+ a.Document[DocCss];
+ b.Document[DocCss];
const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove)
const bxf = b.screenToViewTransform();
@@ -90,18 +97,39 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
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)
+ var foundParent = false;
+ const getAnchor = (field: FieldResult): Element[] => {
+ const docField = DocCast(field);
+ const doc = docField?.layout_unrendered ? DocCast(docField.annotationOn, docField) : docField;
+ const ele = document.getElementById(DocumentView.UniquifyId(LightboxView.Contains(this.DocumentView?.()), doc[Id]));
+ if (ele?.className === 'linkBox-label') foundParent = true;
+ if (ele?.getBoundingClientRect().width) return [ele];
+ const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width);
+ const annoOn = DocCast(doc.annotationOn);
+ if (eles.length || !annoOn) return eles;
+ const pareles = getAnchor(annoOn);
+ foundParent = pareles.length ? true : false;
+ return pareles;
+ };
// 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 targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1);
+ const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2);
- const aid = targetAhyperlink?.id || a.Document[Id];
- const bid = targetBhyperlink?.id || b.Document[Id];
- if (!document.getElementById(aid) || !document.getElementById(bid)) {
+ const container = this.DocumentView?.().containerViewPath?.().lastElement()?.ContentDiv;
+ const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id;
+ const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id;
+ if (!aid || !bid) {
setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01)));
return null;
}
+ if (foundParent) {
+ setTimeout(
+ action(() => (this._forceAnimate = this._forceAnimate + 0.01)),
+ 1
+ );
+ }
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);
@@ -147,6 +175,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
color={color}
labels={
<div
+ id={this.DocumentView?.().DocUniqueId}
+ className={'linkBox-label'}
style={{
borderRadius: '8px',
pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined,
@@ -190,6 +220,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
</>
);
}
+
+ setTimeout(
+ action(() => (this._forceAnimate = this._forceAnimate + 1)),
+ 2
+ );
return (
<div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}>
<ComparisonBox
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 1645d0813..2a96ce458 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -69,8 +69,10 @@ export class LinkDescriptionPopup extends React.Component<{}> {
}}>
<input
className="linkDescriptionPopup-input"
- onKeyDown={e => e.stopPropagation()}
- onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
+ onKeyDown={e => {
+ e.key === 'Enter' && this.onDismiss(true);
+ e.stopPropagation();
+ }}
value={this.description}
placeholder={this.description || '(Optional) Enter link description...'}
onChange={e => this.descriptionChanged(e)}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index ae25ff179..c9c8f9260 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -172,6 +172,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
if (nextHrefInd !== this._hrefInd) {
this._linkDoc = undefined;
this._hrefInd = nextHrefInd;
+ this.updateHref();
}
}),
true
@@ -184,8 +185,9 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps
LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
} else if (this._props.hrefs?.length) {
const webDoc =
- Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()).lastElement() ??
- Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
+ Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys())
+ .filter(doc => doc.type === DocumentType.WEB)
+ .lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
DocumentManager.Instance.showDocument(webDoc, {
openLocation: OpenWhere.lightbox,
willPan: true,
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index c185c66fc..b73898f59 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -6,7 +6,7 @@ import { IconButton, Size, Type } from 'browndash-components';
import * as d3 from 'd3';
import { Feature, FeatureCollection, GeoJsonProperties, Geometry, LineString, Position } from 'geojson';
import mapboxgl, { LngLat, LngLatBoundsLike, MapLayerMouseEvent } from 'mapbox-gl';
-import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { CirclePicker, ColorResult } from 'react-color';
@@ -14,7 +14,6 @@ import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState
import { MarkerEvent } from 'react-map-gl/dist/esm/types';
import { Utils, emptyFunction, setupMoveUpEvents } from '../../../../Utils';
import { Doc, DocListCast, Field, LinkedTo, Opt } from '../../../../fields/Doc';
-import { DocCss, Highlight } from '../../../../fields/DocSymbols';
import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { DocumentType } from '../../../documents/DocumentTypes';
import { DocUtils, Docs } from '../../../documents/Documents';
@@ -28,7 +27,7 @@ import { SidebarAnnos } from '../../SidebarAnnos';
import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
import { Colors } from '../../global/globalEnums';
import { DocumentView } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
import { FormattedTextBox } from '../formattedText/FormattedTextBox';
import { PinProps, PresBox } from '../trails';
import { fastSpeedIcon, mediumSpeedIcon, slowSpeedIcon } from './AnimationSpeedIcons';
@@ -53,7 +52,6 @@ import { MarkerIcons } from './MarkerIcons';
* A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
*/
-const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiemF1bHRhdmFuZ2FyIiwiYSI6ImNscHgwNDd1MDA3MXIydm92ODdianp6cGYifQ.WFAqbhwxtMHOWSPtu0l2uQ';
const MAPBOX_FORWARD_GEOCODE_BASE_URL = 'https://api.mapbox.com/geocoding/v5/mapbox.places/';
@@ -66,72 +64,69 @@ type PopupInfo = {
description: string;
};
-// export type GeocoderControlProps = Omit<GeocoderOptions, 'accessToken' | 'mapboxgl' | 'marker'> & {
-// mapboxAccessToken: string;
-// marker?: Omit<MarkerProps, 'longitude' | 'latitude'>;
-// position: ControlPosition;
-
-// onResult: (...args: any[]) => void;
-// };
-
-type MapMarker = {
- longitude: number;
- latitude: number;
-};
-
-/**
- * Consider integrating later: allows for drawing, circling, making shapes on map
- */
-// const drawingManager = new window.google.maps.drawing.DrawingManager({
-// drawingControl: true,
-// drawingControlOptions: {
-// position: google.maps.ControlPosition.TOP_RIGHT,
-// drawingModes: [
-// google.maps.drawing.OverlayType.MARKER,
-// // currently we are not supporting the following drawing mode on map, a thought for future development
-// google.maps.drawing.OverlayType.CIRCLE,
-// google.maps.drawing.OverlayType.POLYLINE,
-// ],
-// },
-// });
-
@observer
export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(MapBox, fieldKey);
}
- private _dragRef = React.createRef<HTMLDivElement>();
+ private _unmounting = false;
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _mapRef: React.RefObject<MapRef> = React.createRef();
private _disposers: { [key: string]: IReactionDisposer } = {};
- private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
}
- @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
- @computed get allSidebarDocs() {
- return DocListCast(this.dataDoc[this.SidebarKey]);
- }
+ @observable _featuresFromGeocodeResults: any[] = [];
+ @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @observable _selectedPinOrRoute: Doc | undefined = undefined; // The pin that is selected
+ @observable _mapReady = false;
+ @observable _isAnimating: boolean = false;
+ @observable _routeToAnimate: Doc | undefined = undefined;
+ @observable _animationPhase: number = 0;
+ @observable _finishedFlyTo: boolean = false;
+ @observable _frameId: number | null = null;
+ @observable _animationUtility: AnimationUtility | null = null;
+ @observable _settingsOpen: boolean = false;
+ @observable _mapStyle: string = 'mapbox://styles/mapbox/standard';
+ @observable _showTerrain: boolean = true;
+ @observable _currentPopup: PopupInfo | undefined = undefined;
+ @observable _isStreetViewAnimation: boolean = false;
+ @observable _animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM;
+ @observable _animationLineColor: string = '#ffff00';
+ @observable _temporaryRouteSource: FeatureCollection = { type: 'FeatureCollection', features: [] };
+ @observable _dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = {
+ type: 'Feature',
+ properties: {},
+ geometry: { type: 'LineString', coordinates: [] },
+ };
+
+ @observable path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = {
+ type: 'Feature',
+ geometry: { type: 'LineString', coordinates: [] },
+ properties: {},
+ };
+
// this list contains pushpins and configs
- @computed get allAnnotations() {
- return DocListCast(this.dataDoc[this.annotationKey]);
- }
- @computed get allPushpins() {
- return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
- }
- @computed get allRoutes() {
- return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE);
+ @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } //prettier-ignore
+ @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } //prettier-ignore
+ @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } //prettier-ignore
+ @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } //prettier-ignore
+ @computed get SidebarShown() { return this.layoutDoc._layout_showSidebar ? true : false; } //prettier-ignore
+ @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } //prettier-ignore
+ @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } //prettier-ignore
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
@computed get updatedRouteCoordinates(): Feature<Geometry, GeoJsonProperties> {
- if (this.routeToAnimate?.routeCoordinates) {
- const originalCoordinates: Position[] = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
+ if (this._routeToAnimate?.routeCoordinates) {
+ const originalCoordinates: Position[] = JSON.parse(StrCast(this._routeToAnimate.routeCoordinates));
// const index = Math.floor(this.animationPhase * originalCoordinates.length);
- const index = this.animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index
- console.log('Animation phase', this.animationPhase);
+ const index = this._animationPhase * (originalCoordinates.length - 1); // Calculate the fractional index
+ console.log('Animation phase', this._animationPhase);
const startIndex = Math.floor(index);
const endIndex = Math.ceil(index);
let feature: Feature<Geometry, GeoJsonProperties>;
@@ -147,7 +142,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
feature = {
type: 'Feature',
properties: {
- routeTitle: StrCast(this.routeToAnimate.title),
+ routeTitle: StrCast(this._routeToAnimate.title),
},
geometry: geometry,
};
@@ -158,9 +153,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const fraction = index - startIndex;
const interpolator = d3.interpolateArray(startCoord, endCoord);
-
const interpolatedCoord = interpolator(fraction);
-
const coordinates = originalCoordinates.slice(0, startIndex + 1).concat([interpolatedCoord]);
geometry = {
@@ -170,14 +163,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
feature = {
type: 'Feature',
properties: {
- routeTitle: StrCast(this.routeToAnimate.title),
+ routeTitle: StrCast(this._routeToAnimate.title),
},
geometry: geometry,
};
}
autorun(() => {
- const animationUtil = this.animationUtility;
+ const animationUtil = this._animationUtility;
const concattedCoordinates = geometry.coordinates.concat(originalCoordinates.slice(endIndex));
const newFeature: Feature<LineString, turf.Properties> = {
type: 'Feature',
@@ -204,11 +197,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
};
}
@computed get selectedRouteCoordinates(): Position[] {
- let coordinates: Position[] = [];
- if (this.routeToAnimate?.routeCoordinates) {
- coordinates = JSON.parse(StrCast(this.routeToAnimate.routeCoordinates));
- }
- return coordinates;
+ return !this._routeToAnimate?.routeCoordinates ? [] : JSON.parse(StrCast(this._routeToAnimate.routeCoordinates));
}
@computed get allRoutesGeoJson(): FeatureCollection {
@@ -233,29 +222,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
};
}
- @computed get SidebarShown() {
- return this.layoutDoc._layout_showSidebar ? true : false;
- }
- @computed get sidebarWidthPercent() {
- return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
- }
- @computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4'));
- }
- @computed get SidebarKey() {
- return this.fieldKey + '_sidebar';
- }
-
componentDidMount() {
this._unmounting = false;
this._props.setContentViewBox?.(this);
}
- _unmounting = false;
- componentWillUnmount(): void {
+ componentWillUnmount() {
this._unmounting = true;
this.deselectPinOrRoute();
- this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
}
@@ -269,7 +243,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
docs.forEach(doc => {
- let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPinOrRoute;
+ let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this._selectedPinOrRoute;
if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
}
@@ -370,16 +344,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const sourceAnchorCreator = action(() => {
const note = this.getAnchor(true);
- if (note && this.selectedPinOrRoute) {
- note.latitude = this.selectedPinOrRoute.latitude;
- note.longitude = this.selectedPinOrRoute.longitude;
- note.map = this.selectedPinOrRoute.map;
+ if (note && this._selectedPinOrRoute) {
+ note.latitude = this._selectedPinOrRoute.latitude;
+ note.longitude = this._selectedPinOrRoute.longitude;
+ note.map = this._selectedPinOrRoute.map;
}
return note as Doc;
});
const targetCreator = (annotationOn: Doc | undefined) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow');
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
FormattedTextBox.SetSelectOnLoad(target);
return target;
};
@@ -399,10 +373,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const createFunc = undoable(
action(() => {
const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]);
- if (note && this.selectedPinOrRoute) {
- note.latitude = this.selectedPinOrRoute.latitude;
- note.longitude = this.selectedPinOrRoute.longitude;
- note.map = this.selectedPinOrRoute.map;
+ if (note && this._selectedPinOrRoute) {
+ note.latitude = this._selectedPinOrRoute.latitude;
+ note.longitude = this._selectedPinOrRoute.longitude;
+ note.map = this._selectedPinOrRoute.map;
}
}),
'create note annotation'
@@ -423,12 +397,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
return false;
};
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
-
- addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey);
-
pointerEvents = () => (this._props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');
-
panelWidth = () => this._props.PanelWidth() / (this._props.NativeDimScaling?.() || 1) - this.sidebarWidth();
panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1);
scrollXf = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
@@ -439,52 +408,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
savedAnnotations = () => this._savedAnnotations;
- _bingSearchManager: any;
- _bingMap: any;
- get MicrosoftMaps() {
- return (window as any).Microsoft.Maps;
- }
- // uses Bing Search to retrieve lat/lng for a location. eg.,
- // const results = this.geocodeQuery(map.map, 'Philadelphia, PA');
- // to move the map to that location:
- // const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA');
- // this._bingMap.current.setView({
- // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
- // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
- // });
- //
- bingGeocode = (map: any, query: string) => {
- return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
- //If search manager is not defined, load the search module.
- if (!this._bingSearchManager) {
- //Create an instance of the search manager and call the geocodeQuery function again.
- this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
- this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
- res(this.bingGeocode(map, query));
- });
- } else {
- this._bingSearchManager.geocode({
- where: query,
- callback: action((r: any) => res(r.results[0].location)),
- errorCallback: (e: any) => reject(),
- });
- }
- });
- };
-
- @observable
- bingSearchBarContents: any = this.Document.map; // For Bing Maps: The contents of the Bing search bar (string)
-
- geoDataRequestOptions = {
- entityType: 'PopulatedPlace',
- };
-
- // The pin that is selected
- @observable selectedPinOrRoute: Doc | undefined = undefined;
-
@action
deselectPinOrRoute = () => {
- if (this.selectedPinOrRoute) {
+ if (this._selectedPinOrRoute) {
// // Removes filter
// Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'remove');
// Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'remove');
@@ -511,79 +437,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
}
return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
};
- /*
- * Pushpin onclick
- */
- @action
- pushpinClicked = (pinDoc: Doc) => {
- this.deselectPinOrRoute();
- this.selectedPinOrRoute = pinDoc;
- this.bingSearchBarContents = pinDoc.map;
-
- // Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
- // Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
-
- this.recolorPin(this.selectedPinOrRoute, 'green');
-
- MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
- MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
- MapAnchorMenu.Instance.OnClick = this.createNoteAnnotation;
- MapAnchorMenu.Instance.StartDrag = this.startAnchorDrag;
-
- const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPinOrRoute.latitude, this.selectedPinOrRoute.longitude));
- const x = point.x + (this._props.PanelWidth() - this.sidebarWidth()) / 2;
- const y = point.y + this._props.PanelHeight() / 2 + 32;
- const cpt = this.ScreenToLocalBoxXf().inverse().transformPoint(x, y);
- MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true);
-
- document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
- };
-
- /**
- * Map OnClick
- */
- @action
- mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
- this._props.select(false);
- this.deselectPinOrRoute();
- };
- /*
- * Updates values of layout doc to match the current map
- */
- @action
- mapRecentered = () => {
- if (
- Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || //
- Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7
- ) {
- this.dataDoc.latitude = this._bingMap.current.getCenter().latitude;
- this.dataDoc.longitude = this._bingMap.current.getCenter().longitude;
- this.dataDoc.map = '';
- this.bingSearchBarContents = '';
- }
- this.dataDoc.map_zoom = this._bingMap.current.getZoom();
- };
- /*
- * Updates maptype
- */
- @action
- updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId());
-
- /*
- * For Bing Maps
- * Called by search button's onClick
- * Finds the geocode of the searched contents and sets location to that location
- **/
- @action
- bingSearch = () => {
- return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
- this.dataDoc.latitude = location.latitude;
- this.dataDoc.longitude = location.longitude;
- this.dataDoc.map_zoom = this._bingMap.current.getZoom();
- this.dataDoc.map = this.bingSearchBarContents;
- });
- };
/*
* Returns doc w/ relevant info
@@ -592,14 +445,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
/// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER
const anchor = Docs.Create.ConfigDocument({
title: 'MapAnchor:' + this.Document.title,
- text: StrCast(this.selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location',
- config_latitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude),
- config_longitude: NumCast((existingPin ?? this.selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude),
+ text: (StrCast(this._selectedPinOrRoute?.map) || StrCast(this.Document.map) || 'map location') as any,
+ config_latitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.latitude ?? this.dataDoc.latitude),
+ config_longitude: NumCast((existingPin ?? this._selectedPinOrRoute)?.longitude ?? this.dataDoc.longitude),
config_map_zoom: NumCast(this.dataDoc.map_zoom),
// config_map_type: StrCast(this.dataDoc.map_type),
- config_map: StrCast((existingPin ?? this.selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map),
+ config_map: StrCast((existingPin ?? this._selectedPinOrRoute)?.map) || StrCast(this.dataDoc.map),
layout_unrendered: true,
- mapPin: existingPin ?? this.selectedPinOrRoute,
+ mapPin: existingPin ?? this._selectedPinOrRoute,
annotationOn: this.Document,
});
if (anchor) {
@@ -613,25 +466,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
map_docToPinMap = new Map<Doc, any>();
map_pinHighlighted = new Map<Doc, boolean>();
- /*
- * Input: pin doc
- * Adds MicrosoftMaps Pushpin to the map (render)
- */
- @action
- addPushpin = (pin: Doc) => {
- const pushPin = pin.infoWindowOpen
- ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {})
- : new this.MicrosoftMaps.Pushpin(
- new this.MicrosoftMaps.Location(pin.latitude, pin.longitude)
- // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'}
- );
-
- this._bingMap.current.entities.push(pushPin);
-
- this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin));
- // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin));
- this.map_docToPinMap.set(pin, pushPin);
- };
/*
* Input: pin doc
@@ -640,27 +474,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
removePushpinOrRoute = (pinOrRouteDoc: Doc) => this.removeMapDocument(pinOrRouteDoc, this.annotationKey);
- /*
- * Removes pushpin from map render
- */
- deletePushpin = (pinDoc: Doc) => {
- if (!this._unmounting) {
- this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc));
- }
- this.map_docToPinMap.delete(pinDoc);
- this.selectedPinOrRoute = undefined;
- };
-
@action
deleteSelectedPinOrRoute = undoable(() => {
console.log('deleting');
- if (this.selectedPinOrRoute) {
+ if (this._selectedPinOrRoute) {
// Removes filter
- Doc.setDocFilter(this.Document, 'latitude', this.selectedPinOrRoute.latitude, 'remove');
- Doc.setDocFilter(this.Document, 'longitude', this.selectedPinOrRoute.longitude, 'remove');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPinOrRoute))}`, 'remove');
+ Doc.setDocFilter(this.Document, 'latitude', this._selectedPinOrRoute.latitude, 'remove');
+ Doc.setDocFilter(this.Document, 'longitude', this._selectedPinOrRoute.longitude, 'remove');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove');
- this.removePushpinOrRoute(this.selectedPinOrRoute);
+ this.removePushpinOrRoute(this._selectedPinOrRoute);
}
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
@@ -677,7 +500,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
e.preventDefault();
MapAnchorMenu.Instance.fadeOut(true);
runInAction(() => {
- this.temporaryRouteSource = {
+ this._temporaryRouteSource = {
type: 'FeatureCollection',
features: [],
};
@@ -688,9 +511,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
centerOnSelectedPin = () => {
- if (this.selectedPinOrRoute) {
+ if (this._selectedPinOrRoute) {
this._mapRef.current?.flyTo({
- center: [NumCast(this.selectedPinOrRoute.longitude), NumCast(this.selectedPinOrRoute.latitude)],
+ center: [NumCast(this._selectedPinOrRoute.longitude), NumCast(this._selectedPinOrRoute.latitude)],
});
}
// if (this.selectedPin) {
@@ -703,33 +526,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu);
};
- /**
- * View options for bing maps
- */
- bingViewOptions = {
- // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng },
- zoom: this.dataDoc.latitude ?? 10,
- mapTypeId: 'grayscale',
- };
-
- /**
- * Map options
- */
- bingMapOptions = {
- navigationBarMode: 'square',
- backgroundColor: '#f1f3f4',
- enableInertia: true,
- supportedMapTypes: ['grayscale', 'canvasLight'],
- disableMapTypeSelectorMouseOver: true,
- // showScalebar:true
- // disableRoadView:true,
- // disableBirdseye:true
- streetsideOptions: {
- showProblemReporting: false,
- showCurrentAddress: false,
- },
- };
-
recolorPin = (pin: Doc, color?: string) => {
// this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
// this.map_docToPinMap.delete(pin);
@@ -739,109 +535,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// this.map_docToPinMap.set(pin, newpin);
};
- /*
- * Called when BingMap is first rendered
- * Initializes starting values
- */
- @observable _mapReady = false;
- @action
- bingMapReady = (map: any) => {
- this._mapReady = true;
- this._bingMap = map.map;
- if (!this._bingMap.current) {
- alert('NO Map!?');
- }
- this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick);
- this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change'));
- this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change'));
-
- this._disposers.mapLocation = reaction(
- () => this.Document.map,
- mapLoc => (this.bingSearchBarContents = mapLoc),
- { fireImmediately: true }
- );
- this._disposers.highlight = reaction(
- () => this.allAnnotations.map(doc => doc[Highlight]),
- () => {
- const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
- allConfigPins.forEach(({ doc, pushpin }) => {
- if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
- this.recolorPin(pushpin);
- this.map_pinHighlighted.delete(pushpin);
- }
- });
- allConfigPins.forEach(({ doc, pushpin }) => {
- if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) {
- this.recolorPin(pushpin, 'orange');
- this.map_pinHighlighted.set(pushpin, true);
- }
- });
- },
- { fireImmediately: true }
- );
-
- this._disposers.location = reaction(
- () => ({ lat: this.Document.latitude, lng: this.Document.longitude, zoom: this.Document.map_zoom, mapType: this.Document.map_type }),
- locationObject => {
- // if (this._bingMap.current)
- try {
- locationObject?.zoom &&
- this._bingMap.current?.setView({
- mapTypeId: locationObject.mapType,
- zoom: locationObject.zoom,
- center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng),
- });
- } catch (e) {
- console.log(e);
- }
- },
- { fireImmediately: true }
- );
- };
-
- dragToggle = (e: React.PointerEvent) => {
- let dragClone: HTMLDivElement | undefined;
-
- setupMoveUpEvents(
- e,
- e,
- e => {
- // move event
- if (!dragClone) {
- dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; // copy draggable pin
- dragClone.style.position = 'absolute';
- dragClone.style.zIndex = '10000';
- DragManager.Root().appendChild(dragClone); // add clone to root
- }
- dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
- return false;
- },
- e => {
- // up event
- if (!dragClone) return;
- DragManager.Root().removeChild(dragClone);
- let target = document.elementFromPoint(e.x, e.y); // element for specified x and y coordinates
- while (target) {
- if (target === this._ref.current) {
- const cpt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
- const x = cpt[0] - (this._props.PanelWidth() - this.sidebarWidth()) / 2;
- const y = cpt[1] - 20 /* height of search bar */ - this._props.PanelHeight() / 2;
- const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y));
- this.createPushpin(location.latitude, location.longitude);
- break;
- }
- target = target.parentElement;
- }
- },
- e => {
- const createPin = () => this.createPushpin(this.Document.latitude, this.Document.longitude, this.Document.map);
- if (this.bingSearchBarContents) {
- this.bingSearch().then(createPin);
- } else createPin();
- }
- );
- };
-
// incrementer: number = 0;
/*
* Creates Pushpin doc and adds it to the list of annotations
@@ -880,7 +573,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
if (createPinForDestination) {
this.createPushpin(destination.center[1], destination.center[0], destination.place_name);
}
- this.temporaryRouteSource = {
+ this._temporaryRouteSource = {
type: 'FeatureCollection',
features: [],
};
@@ -890,10 +583,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// TODO: Display error that can't create route to same location
}, 'createmaproute');
- searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
-
- @observable
- featuresFromGeocodeResults: any[] = [];
+ @action
+ searchbarKeyDown = (e: any) => {
+ if (e.key === 'Enter' && this._featuresFromGeocodeResults) {
+ const center = this._featuresFromGeocodeResults[0]?.center;
+ this._featuresFromGeocodeResults = [];
+ setTimeout(() => center && this._mapRef.current?.flyTo({ center }));
+ }
+ };
@action
addMarkerForFeature = (feature: any) => {
@@ -910,7 +607,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
center: feature.center,
});
}
- this.featuresFromGeocodeResults = [];
+ this._featuresFromGeocodeResults = [];
} else {
// TODO: handle error
}
@@ -922,11 +619,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
*/
handleSearchChange = async (searchText: string) => {
const features = await MapboxApiUtility.forwardGeocodeForFeatures(searchText);
- if (features && !this.isAnimating) {
+ if (features && !this._isAnimating) {
runInAction(() => {
- this.settingsOpen = false;
- this.featuresFromGeocodeResults = features;
- this.routeToAnimate = undefined;
+ this._settingsOpen = false;
+ this._featuresFromGeocodeResults = features;
+ this._routeToAnimate = undefined;
});
}
// try {
@@ -946,8 +643,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
handleMapClick = (e: MapLayerMouseEvent) => {
- this.featuresFromGeocodeResults = [];
- this.settingsOpen = false;
+ this._featuresFromGeocodeResults = [];
+ this._settingsOpen = false;
if (this._mapRef.current) {
const features = this._mapRef.current.queryRenderedFeatures(e.point, {
layers: ['map-routes-layer'],
@@ -960,8 +657,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const routeDoc: Doc | undefined = this.allRoutes.find(routeDoc => routeDoc.title === routeTitle);
this.deselectPinOrRoute(); // TODO: Also deselect route if selected
if (routeDoc) {
- this.selectedPinOrRoute = routeDoc;
- Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
+ this._selectedPinOrRoute = routeDoc;
+ Doc.setDocFilter(this.Document, LinkedTo, `mapRoute=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check');
// TODO: Recolor route
@@ -1008,7 +705,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const features = await MapboxApiUtility.reverseGeocodeForFeatures(longitude, latitude);
if (features) {
runInAction(() => {
- this.featuresFromGeocodeResults = features;
+ this._featuresFromGeocodeResults = features;
});
}
@@ -1028,21 +725,18 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// }
};
- @observable
- currentPopup: PopupInfo | undefined = undefined;
-
@action
handleMarkerClick = (e: MarkerEvent<mapboxgl.Marker, MouseEvent>, pinDoc: Doc) => {
- this.featuresFromGeocodeResults = [];
+ this._featuresFromGeocodeResults = [];
this.deselectPinOrRoute(); // TODO: check this method
- this.selectedPinOrRoute = pinDoc;
+ this._selectedPinOrRoute = pinDoc;
// this.bingSearchBarContents = pinDoc.map;
// Doc.setDocFilter(this.Document, 'latitude', this.selectedPin.latitude, 'match');
// Doc.setDocFilter(this.Document, 'longitude', this.selectedPin.longitude, 'match');
- Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this.selectedPinOrRoute)}`, 'check');
+ Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(this._selectedPinOrRoute)}`, 'check');
- this.recolorPin(this.selectedPinOrRoute, 'green'); // TODO: check this method
+ this.recolorPin(this._selectedPinOrRoute, 'green'); // TODO: check this method
MapAnchorMenu.Instance.Delete = this.deleteSelectedPinOrRoute;
MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
@@ -1072,12 +766,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// })
};
- @observable
- temporaryRouteSource: FeatureCollection = {
- type: 'FeatureCollection',
- features: [],
- };
-
@action
displayRoute = (routeInfoMap: Record<TransportationType, any> | undefined, type: TransportationType) => {
if (routeInfoMap) {
@@ -1096,41 +784,23 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
};
// TODO: Create pin for destination
// TODO: Fly to point where full route will be shown
- this.temporaryRouteSource = newTempRouteSource;
+ this._temporaryRouteSource = newTempRouteSource;
}
};
- @observable
- isAnimating: boolean = false;
-
- @observable
- routeToAnimate: Doc | undefined = undefined;
-
- @observable
- animationPhase: number = 0;
-
- @observable
- finishedFlyTo: boolean = false;
-
@action
setAnimationPhase = (newValue: number) => {
- this.animationPhase = newValue;
+ this._animationPhase = newValue;
};
- @observable
- frameId: number | null = null;
-
@action
setFrameId = (frameId: number) => {
- this.frameId = frameId;
+ this._frameId = frameId;
};
- @observable
- animationUtility: AnimationUtility | null = null;
-
@action
setAnimationUtility = (util: AnimationUtility) => {
- this.animationUtility = util;
+ this._animationUtility = util;
};
@action
@@ -1138,8 +808,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
if (routeDoc) {
MapAnchorMenu.Instance.fadeOut(true);
document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
- this.featuresFromGeocodeResults = [];
- this.routeToAnimate = routeDoc;
+ this._featuresFromGeocodeResults = [];
+ this._routeToAnimate = routeDoc;
}
};
@@ -1161,29 +831,20 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@computed
get preAnimationViewState() {
- if (!this.isAnimating) {
+ if (!this._isAnimating) {
return this.mapboxMapViewState;
}
}
- @observable
- isStreetViewAnimation: boolean = false;
-
- @observable
- animationSpeed: AnimationSpeed = AnimationSpeed.MEDIUM;
-
- @observable
- animationLineColor: string = '#ffff00';
-
@action
setAnimationLineColor = (color: ColorResult) => {
- this.animationLineColor = color.hex;
+ this._animationLineColor = color.hex;
};
@action
updateAnimationSpeed = () => {
let newAnimationSpeed: AnimationSpeed;
- switch (this.animationSpeed) {
+ switch (this._animationSpeed) {
case AnimationSpeed.SLOW:
newAnimationSpeed = AnimationSpeed.MEDIUM;
break;
@@ -1197,63 +858,33 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
newAnimationSpeed = AnimationSpeed.MEDIUM;
break;
}
- this.animationSpeed = newAnimationSpeed;
- if (this.animationUtility) {
- this.animationUtility.updateAnimationSpeed(newAnimationSpeed);
+ this._animationSpeed = newAnimationSpeed;
+ if (this._animationUtility) {
+ this._animationUtility.updateAnimationSpeed(newAnimationSpeed);
}
};
@computed get animationSpeedTooltipText(): string {
- switch (this.animationSpeed) {
- case AnimationSpeed.SLOW:
- return '1x speed';
- case AnimationSpeed.MEDIUM:
- return '2x speed';
- case AnimationSpeed.FAST:
- return '3x speed';
- default:
- return '2x speed';
- }
+ switch (this._animationSpeed) {
+ case AnimationSpeed.SLOW: return '1x speed';
+ case AnimationSpeed.MEDIUM: return '2x speed';
+ case AnimationSpeed.FAST: return '3x speed';
+ default: return '2x speed';
+ } // prettier-ignore
}
@computed get animationSpeedIcon(): JSX.Element {
- switch (this.animationSpeed) {
- case AnimationSpeed.SLOW:
- return slowSpeedIcon;
- case AnimationSpeed.MEDIUM:
- return mediumSpeedIcon;
- case AnimationSpeed.FAST:
- return fastSpeedIcon;
- default:
- return mediumSpeedIcon;
- }
+ switch (this._animationSpeed) {
+ case AnimationSpeed.SLOW: return slowSpeedIcon;
+ case AnimationSpeed.MEDIUM: return mediumSpeedIcon;
+ case AnimationSpeed.FAST: return fastSpeedIcon;
+ default: return mediumSpeedIcon;
+ } // prettier-ignore
}
@action
toggleIsStreetViewAnimation = () => {
- const newVal = !this.isStreetViewAnimation;
- this.isStreetViewAnimation = newVal;
- if (this.animationUtility) {
- this.animationUtility.updateIsStreetViewAnimation(newVal);
- }
- };
-
- @observable
- dynamicRouteFeature: Feature<Geometry, GeoJsonProperties> = {
- type: 'Feature',
- properties: {},
- geometry: {
- type: 'LineString',
- coordinates: [],
- },
- };
-
- @observable
- path: turf.helpers.Feature<turf.helpers.LineString, turf.helpers.Properties> = {
- type: 'Feature',
- geometry: {
- type: 'LineString',
- coordinates: [],
- },
- properties: {},
+ const newVal = !this._isStreetViewAnimation;
+ this._isStreetViewAnimation = newVal;
+ this._animationUtility?.updateIsStreetViewAnimation(newVal);
};
getFeatureFromRouteDoc = (routeDoc: Doc): Feature<Geometry, GeoJsonProperties> => {
@@ -1272,19 +903,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
playAnimation = (status: AnimationStatus) => {
- if (!this._mapRef.current || !this.routeToAnimate) {
+ if (!this._mapRef.current || !this._routeToAnimate) {
return;
}
- this.animationPhase = status === AnimationStatus.RESUME ? this.animationPhase : 0;
- this.frameId = AnimationStatus.RESUME ? this.frameId : null;
- this.finishedFlyTo = AnimationStatus.RESUME ? this.finishedFlyTo : false;
+ this._animationPhase = status === AnimationStatus.RESUME ? this._animationPhase : 0;
+ this._frameId = AnimationStatus.RESUME ? this._frameId : null;
+ this._finishedFlyTo = AnimationStatus.RESUME ? this._finishedFlyTo : false;
const path = turf.lineString(this.selectedRouteCoordinates);
- this.settingsOpen = false;
+ this._settingsOpen = false;
this.path = path;
- this.isAnimating = true;
+ this._isAnimating = true;
runInAction(() => {
return new Promise<void>(async resolve => {
@@ -1293,18 +924,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
lat: this.selectedRouteCoordinates[0][1],
};
- const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this.isStreetViewAnimation, this.animationSpeed, this.showTerrain, this._mapRef.current);
- runInAction(() => {
- this.setAnimationUtility(animationUtil);
- });
-
- const updateFrameId = (newFrameId: number) => {
- this.setFrameId(newFrameId);
- };
+ const animationUtil = new AnimationUtility(targetLngLat, this.selectedRouteCoordinates, this._isStreetViewAnimation, this._animationSpeed, this._showTerrain, this._mapRef.current);
+ runInAction(() => this.setAnimationUtility(animationUtil));
- const updateAnimationPhase = (newAnimationPhase: number) => {
- this.setAnimationPhase(newAnimationPhase);
- };
+ const updateFrameId = (newFrameId: number) => this.setFrameId(newFrameId);
+ const updateAnimationPhase = (newAnimationPhase: number) => this.setAnimationPhase(newAnimationPhase);
if (status !== AnimationStatus.RESUME) {
const result = await animationUtil.flyInAndRotate({
@@ -1324,9 +948,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
console.log('Altitude: ', result.altitude);
}
- runInAction(() => {
- this.finishedFlyTo = true;
- });
+ runInAction(() => (this._finishedFlyTo = true));
// follow the path while slowly rotating the camera, passing in the camera bearing and altitude from the previous animation
await animationUtil.animatePath({
@@ -1335,7 +957,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// startBearing: -20,
// startAltitude: this.isStreetViewAnimation ? 80 : 12000,
// pitch: this.isStreetViewAnimation ? 80: 50,
- currentAnimationPhase: this.animationPhase,
+ currentAnimationPhase: this._animationPhase,
updateAnimationPhase,
updateFrameId,
});
@@ -1353,7 +975,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
});
setTimeout(() => {
- this.isStreetViewAnimation = false;
+ this._isStreetViewAnimation = false;
resolve();
}, 10000);
});
@@ -1362,27 +984,27 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
pauseAnimation = () => {
- if (this.frameId && this.animationPhase > 0) {
- window.cancelAnimationFrame(this.frameId);
- this.frameId = null;
- this.isAnimating = false;
+ if (this._frameId && this._animationPhase > 0) {
+ window.cancelAnimationFrame(this._frameId);
+ this._frameId = null;
+ this._isAnimating = false;
}
};
@action
stopAnimation = (close: boolean) => {
- if (this.frameId) {
- window.cancelAnimationFrame(this.frameId);
+ if (this._frameId) {
+ window.cancelAnimationFrame(this._frameId);
}
- this.animationPhase = 0;
- this.frameId = null;
- this.finishedFlyTo = false;
- this.isAnimating = false;
+ this._animationPhase = 0;
+ this._frameId = null;
+ this._finishedFlyTo = false;
+ this._isAnimating = false;
if (close) {
- this.animationSpeed = AnimationSpeed.MEDIUM;
- this.isStreetViewAnimation = false;
- this.routeToAnimate = undefined;
- this.animationUtility = null;
+ this._animationSpeed = AnimationSpeed.MEDIUM;
+ this._isStreetViewAnimation = false;
+ this._routeToAnimate = undefined;
+ this._animationUtility = null;
}
};
@@ -1390,21 +1012,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
return (
<>
<IconButton
- tooltip={this.isAnimating && this.finishedFlyTo ? 'Pause Animation' : 'Play Animation'}
+ tooltip={this._isAnimating && this._finishedFlyTo ? 'Pause Animation' : 'Play Animation'}
onPointerDown={() => {
- if (this.isAnimating && this.finishedFlyTo) {
+ if (this._isAnimating && this._finishedFlyTo) {
this.pauseAnimation();
- } else if (this.animationPhase > 0) {
+ } else if (this._animationPhase > 0) {
this.playAnimation(AnimationStatus.RESUME); // Resume from the current phase
} else {
this.playAnimation(AnimationStatus.START); // Play from the beginning
}
}}
- icon={this.isAnimating && this.finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />}
+ icon={this._isAnimating && this._finishedFlyTo ? <FontAwesomeIcon icon={faPause as IconLookup} /> : <FontAwesomeIcon icon={faPlay as IconLookup} />}
color="black"
size={Size.MEDIUM}
/>
- {this.isAnimating && this.finishedFlyTo && (
+ {this._isAnimating && this._finishedFlyTo && (
<IconButton
tooltip="Restart animation"
onPointerDown={() => {
@@ -1420,7 +1042,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
<>
<div className="animation-suboptions">
<div>|</div>
- <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this.isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} />
+ <FormControlLabel className="first-person-label" label="1st person animation:" labelPlacement="start" control={<Checkbox color="success" checked={this._isStreetViewAnimation} onChange={this.toggleIsStreetViewAnimation} />} />
<div id="divider">|</div>
<IconButton tooltip={this.animationSpeedTooltipText} onPointerDown={this.updateAnimationSpeed} icon={this.animationSpeedIcon} size={Size.MEDIUM} />
<div id="divider">|</div>
@@ -1436,26 +1058,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
hideRoute = () => {
- this.temporaryRouteSource = {
+ this._temporaryRouteSource = {
type: 'FeatureCollection',
features: [],
};
};
- @observable
- settingsOpen: boolean = false;
-
- @observable
- mapStyle: string = 'mapbox://styles/mapbox/standard';
-
- @observable
- showTerrain: boolean = true;
-
@action
toggleSettings = () => {
- if (!this.isAnimating && this.animationPhase == 0) {
- this.featuresFromGeocodeResults = [];
- this.settingsOpen = !this.settingsOpen;
+ if (!this._isAnimating && this._animationPhase == 0) {
+ this._featuresFromGeocodeResults = [];
+ this._settingsOpen = !this._settingsOpen;
}
};
@@ -1500,14 +1113,10 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
@action
onStepZoomChange = (increment: boolean) => {
if (this._mapRef.current) {
- let newZoom: number;
- if (increment) {
- console.log('inc');
- newZoom = Math.min(16, this.mapboxMapViewState.zoom + 1);
- } else {
- console.log('dec');
- newZoom = Math.max(0, this.mapboxMapViewState.zoom - 1);
- }
+ const newZoom = increment //
+ ? Math.min(16, this.mapboxMapViewState.zoom + 1)
+ : Math.max(0, this.mapboxMapViewState.zoom - 1);
+
this._mapRef.current.setZoom(newZoom);
this.dataDoc.map_zoom = newZoom;
}
@@ -1523,7 +1132,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
};
@action
- toggleShowTerrain = () => (this.showTerrain = !this.showTerrain);
+ toggleShowTerrain = () => (this._showTerrain = !this._showTerrain);
getMarkerIcon = (pinDoc: Doc): JSX.Element | null => {
const markerType = StrCast(pinDoc.markerType);
@@ -1532,25 +1141,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
return MarkerIcons.getFontAwesomeIcon(markerType, '2x', markerColor) ?? null;
};
- static _firstRender = true;
- static _rerenderDelay = 500;
- _rerenderTimeout: any;
+ _textRef = React.createRef<any>();
render() {
- // bcz: no idea what's going on here, but bings maps have some kind of bug
- // such that we need to delay rendering a second map on startup until the first map is rendered.
- this.Document[DocCss];
- if (MapBox._rerenderDelay) {
- // prettier-ignore
- this._rerenderTimeout = this._rerenderTimeout ??
- setTimeout(action(() => {
- if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) {
- MapBox._rerenderDelay = 0;
- }
- this._rerenderTimeout = undefined;
- this.Document[DocCss] = this.Document[DocCss] + 1;
- }), MapBox._rerenderDelay);
- return null;
- }
const scale = this._props.NativeDimScaling?.() || 1;
const parscale = scale === 1 ? 1 : this.ScreenToLocalBoxXf().Scale ?? 1;
@@ -1560,20 +1152,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
<div
className="mapBox-wrapper"
onWheel={e => e.stopPropagation()}
- onPointerDown={async e => {
- e.button === 0 && !e.ctrlKey && e.stopPropagation();
- }}
+ onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
style={{ transformOrigin: 'top left', transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.IsDragging ? null : renderAnnotations()}
- {!this.routeToAnimate && (
+ {!this._routeToAnimate && (
<div className="mapBox-searchbar" style={{ width: `${100 / scale}%`, zIndex: 1, position: 'relative', background: 'lightGray' }}>
- <TextField fullWidth placeholder="Enter a location" onChange={(e: any) => this.handleSearchChange(e.target.value)} />
+ <TextField ref={this._textRef} fullWidth placeholder="Enter a location" onKeyDown={this.searchbarKeyDown} onChange={(e: any) => this.handleSearchChange(e.target.value)} />
<IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
+ <div style={{ opacity: 0 }}>
+ <IconButton icon={<FontAwesomeIcon icon={faGear as IconLookup} size="1x" />} type={Type.TERT} onClick={e => this.toggleSettings()} />
+ </div>
</div>
)}
- {this.settingsOpen && !this.routeToAnimate && (
+ {this._settingsOpen && !this._routeToAnimate && (
<div className="mapbox-settings-panel" style={{ right: `${0 + this.sidebarWidth()}px` }}>
<div className="mapbox-style-select">
<div>Map Style:</div>
@@ -1605,21 +1198,21 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
</div>
<div className="mapbox-terrain-selection">
<div>Show terrain: </div>
- <input type="checkbox" checked={this.showTerrain} onChange={this.toggleShowTerrain} />
+ <input type="checkbox" checked={this._showTerrain} onChange={this.toggleShowTerrain} />
</div>
</div>
)}
- {this.routeToAnimate && (
+ {this._routeToAnimate && (
<div className="animation-panel" style={{ width: this.sidebarWidth() === 0 ? '100%' : `calc(100% - ${this.sidebarWidth()}px)` }}>
- <div id="route-to-animate-title">{StrCast(this.routeToAnimate.title)}</div>
+ <div id="route-to-animate-title">{StrCast(this._routeToAnimate.title)}</div>
<div className="route-animation-options">{this.getRouteAnimationOptions()}</div>
</div>
)}
- {this.featuresFromGeocodeResults.length > 0 && (
+ {this._featuresFromGeocodeResults.length > 0 && (
<div className="mapbox-geocoding-search-results">
- <React.Fragment>
+ <>
<h4>Choose a location for your pin: </h4>
- {this.featuresFromGeocodeResults
+ {this._featuresFromGeocodeResults
.filter(feature => feature.place_name)
.map((feature, idx) => (
<div
@@ -1632,14 +1225,14 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
<div className="search-result-place-name">{feature.place_name}</div>
</div>
))}
- </React.Fragment>
+ </>
</div>
)}
<MapProvider>
<MapboxMap
ref={this._mapRef}
mapboxAccessToken={MAPBOX_ACCESS_TOKEN}
- viewState={this.isAnimating || this.routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }}
+ viewState={this._isAnimating || this._routeToAnimate ? undefined : { ...this.mapboxMapViewState, width: NumCast(this.layoutDoc._width), height: NumCast(this.layoutDoc._height) }}
mapStyle={this.dataDoc.map_style ? StrCast(this.dataDoc.map_style) : 'mapbox://styles/mapbox/streets-v11'}
style={{
position: 'absolute',
@@ -1649,20 +1242,22 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
width: NumCast(this.layoutDoc._width) * parscale,
height: NumCast(this.layoutDoc._height) * parscale,
}}
- initialViewState={this.isAnimating ? undefined : this.mapboxMapViewState}
+ initialViewState={this._isAnimating ? undefined : this.mapboxMapViewState}
onZoom={this.onMapZoom}
onMove={this.onMapMove}
onClick={this.handleMapClick}
onDblClick={this.handleMapDblClick}
- terrain={this.showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}>
+ terrain={this._showTerrain ? { source: 'mapbox-dem', exaggeration: 2.0 } : undefined}>
<Source id="mapbox-dem" type="raster-dem" url="mapbox://mapbox.mapbox-terrain-dem-v1" tileSize={512} maxzoom={14} />
- <Source id="temporary-route" type="geojson" data={this.temporaryRouteSource} />
+ <Source id="temporary-route" type="geojson" data={this._temporaryRouteSource} />
<Source id="map-routes" type="geojson" data={this.allRoutesGeoJson} />
<Layer id="temporary-route-layer" type="line" source="temporary-route" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#36454F', 'line-width': 4, 'line-dasharray': [1, 1] }} />
- {!this.isAnimating && this.animationPhase == 0 && <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />}
- {this.routeToAnimate && (this.isAnimating || this.animationPhase > 0) && (
+ {!this._isAnimating && this._animationPhase == 0 && (
+ <Layer id="map-routes-layer" type="line" source="map-routes" layout={{ 'line-join': 'round', 'line-cap': 'round' }} paint={{ 'line-color': '#FF0000', 'line-width': 4 }} />
+ )}
+ {this._routeToAnimate && (this._isAnimating || this._animationPhase > 0) && (
<>
- {!this.isStreetViewAnimation && (
+ {!this._isStreetViewAnimation && (
<>
<Source id="animated-route" type="geojson" data={this.updatedRouteCoordinates} />
<Layer
@@ -1670,7 +1265,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
type="line"
source="animated-route"
paint={{
- 'line-color': this.animationLineColor,
+ 'line-color': this._animationLineColor,
'line-width': 5,
}}
/>
@@ -1722,10 +1317,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
)}
<>
- {!this.isAnimating &&
- this.animationPhase == 0 &&
- this.allPushpins
- // .filter(anno => !anno.layout_unrendered)
+ {!this._isAnimating &&
+ this._animationPhase == 0 &&
+ this.allPushpins // .filter(anno => !anno.layout_unrendered)
.map((pushpin, idx) => (
<Marker key={idx} longitude={NumCast(pushpin.longitude)} latitude={NumCast(pushpin.latitude)} anchor="bottom" onClick={(e: MarkerEvent<mapboxgl.Marker, MouseEvent>) => this.handleMarkerClick(e, pushpin)}>
{this.getMarkerIcon(pushpin)}
diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
index fc5b4dd18..8ebc90157 100644
--- a/src/client/views/nodes/MapBox/MapPushpinBox.tsx
+++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { FieldView, FieldViewProps } from '../FieldView';
-import { MapBox } from './MapBox';
+import { MapBoxContainer } from '../MapboxMapBox/MapboxContainer';
/**
* Map Pushpin doc class
@@ -18,7 +18,7 @@ export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
get mapBoxView() {
- return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBox;
+ return this.DocumentView?.()?.containerViewPath?.().lastElement()?.ComponentView as MapBoxContainer;
}
get mapBox() {
return this.DocumentView?.().containerViewPath?.().lastElement()?.Document;
diff --git a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
index 8a5bd7ce6..3eb051dbf 100644
--- a/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
+++ b/src/client/views/nodes/MapboxMapBox/MapboxContainer.tsx
@@ -232,7 +232,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
});
const targetCreator = (annotationOn: Doc | undefined) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn, 'yellow');
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow');
FormattedTextBox.SetSelectOnLoad(target);
return target;
};
@@ -466,7 +466,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent<FieldViewProps>
/// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER
const anchor = Docs.Create.ConfigDocument({
title: 'MapAnchor:' + this.Document.title,
- text: StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location',
+ text: (StrCast(this.selectedPin?.map) || StrCast(this.Document.map) || 'map location') as any,
config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude),
config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude),
config_map_zoom: NumCast(this.dataDoc.map_zoom),
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index f6d94ce05..1f976f926 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -11,7 +11,7 @@ import { Upload } from '../../../../server/SharedMediaTypes';
import { DocumentType } from '../../../documents/DocumentTypes';
import { Docs } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { Presentation } from '../../../util/TrackMovements';
import { undoBatch } from '../../../util/UndoManager';
@@ -65,7 +65,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
@undoBatch
- @action
public static WorkspaceStopRecording() {
const remDoc = RecordingBox.screengrabber?.Document;
if (remDoc) {
@@ -90,7 +89,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
* @returns
*/
@undoBatch
- @action
public static WorkspaceStartRecording(value: string) {
const screengrabber =
value === 'Record Workspace'
@@ -120,7 +118,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
* @param value RecordingBox rootdoc
*/
@undoBatch
- @action
public static replayWorkspace(value: Doc) {
Doc.UserDoc().currentRecording = value;
value.overlayX = 70;
@@ -138,7 +135,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
* @param value current recordingbox
*/
@undoBatch
- @action
public static addRecToWorkspace(value: RecordingBox) {
let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView);
(ffView?.ComponentView as CollectionFreeFormView)._props.addDocument?.(value.Document);
@@ -149,7 +145,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UserDoc().workspaceRecordingState = undefined;
}
- @action
public static resumeWorkspaceReplaying(doc: Doc) {
const docView = DocumentManager.Instance.getDocumentView(doc);
if (docView?.ComponentView instanceof VideoBox) {
@@ -158,7 +153,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UserDoc().workspaceReplayingState = media_state.Playing;
}
- @action
public static pauseWorkspaceReplaying(doc: Doc) {
const docView = DocumentManager.Instance.getDocumentView(doc);
const videoBox = docView?.ComponentView as VideoBox;
@@ -168,7 +162,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UserDoc().workspaceReplayingState = media_state.Paused;
}
- @action
public static stopWorkspaceReplaying(value: Doc) {
Doc.RemFromMyOverlay(value);
Doc.UserDoc().currentRecording = undefined;
@@ -178,7 +171,6 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@undoBatch
- @action
public static removeWorkspaceReplaying(value: Doc) {
Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value);
Doc.RemFromMyOverlay(value);
@@ -251,7 +243,7 @@ ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: b
ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) {
if (DocCast(value.doc)) {
- DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY);
+ DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY);
value.e.preventDefault();
return true;
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 287cccd8f..f2d5a980d 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -1,208 +1,200 @@
video {
- // flex: 100%;
- width: 100%;
- // min-height: 400px;
- //height: auto;
- height: 100%;
- //display: block;
- object-fit: cover;
- background-color: black;
-}
-
-button {
- margin: 0 .5rem
+ // flex: 100%;
+ width: 100%;
+ // min-height: 400px;
+ //height: auto;
+ height: 100%;
+ //display: block;
+ object-fit: cover;
+ background-color: black;
}
.recording-container {
- height: 100%;
- width: 100%;
- // display: flex;
- pointer-events: all;
- background-color: black;
+ height: 100%;
+ width: 100%;
+ // display: flex;
+ pointer-events: all;
+ background-color: black;
+ button {
+ margin: 0 0.5rem;
+ }
}
.video-wrapper {
- // max-width: 600px;
- // max-width: 700px;
- // position: relative;
- display: flex;
- justify-content: center;
- // overflow: hidden;
- border-radius: 10px;
- margin: 0;
+ // max-width: 600px;
+ // max-width: 700px;
+ // position: relative;
+ display: flex;
+ justify-content: center;
+ // overflow: hidden;
+ border-radius: 10px;
+ margin: 0;
}
.video-wrapper:hover .controls {
- bottom: 34.5px;
- transform: translateY(0%);
- opacity: 100%;
+ bottom: 34.5px;
+ transform: translateY(0%);
+ opacity: 100%;
}
.controls {
- display: flex;
- align-items: center;
- justify-content: center;
- position: absolute;
- width: 100%;
- flex-wrap: wrap;
- background: rgba(255, 255, 255, 0.25);
- box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
- // backdrop-filter: blur(4px);
- border-radius: 10px;
- border: 1px solid rgba(255, 255, 255, 0.18);
- transition: all 0.3s ease-in-out;
- bottom: 34.5px;
- height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ width: 100%;
+ flex-wrap: wrap;
+ background: rgba(255, 255, 255, 0.25);
+ box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
+ // backdrop-filter: blur(4px);
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ transition: all 0.3s ease-in-out;
+ bottom: 34.5px;
+ height: 60px;
}
.controls:active {
- bottom: 40px;
+ bottom: 40px;
}
.actions button {
- background: none;
- border: none;
- outline: none;
- cursor: pointer;
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
}
.actions button i {
- background-color: none;
- color: white;
- font-size: 30px;
+ background-color: none;
+ color: white;
+ font-size: 30px;
}
-
.velocity {
- appearance: none;
- background: none;
- color: white;
- outline: none;
- border: none;
- text-align: center;
- font-size: 16px;
+ appearance: none;
+ background: none;
+ color: white;
+ outline: none;
+ border: none;
+ text-align: center;
+ font-size: 16px;
}
.mute-btn {
- background: none;
- border: none;
- outline: none;
- cursor: pointer;
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
}
.mute-btn i {
- background-color: none;
- color: white;
- font-size: 20px;
+ background-color: none;
+ color: white;
+ font-size: 20px;
}
.recording-sign {
- height: 20px;
- width: auto;
- display: flex;
- flex-direction: row;
- position: absolute;
- top: 10px;
- right: 15px;
- align-items: center;
- justify-content: center;
-
- .timer {
- font-size: 15px;
- color: white;
- margin: 0;
- }
-
- .dot {
- height: 15px;
- width: 15px;
- margin: 5px;
- background-color: red;
- border-radius: 50%;
- display: inline-block;
- }
+ height: 20px;
+ width: auto;
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ align-items: center;
+ justify-content: center;
+
+ .timer {
+ font-size: 15px;
+ color: white;
+ margin: 0;
+ }
+
+ .dot {
+ height: 15px;
+ width: 15px;
+ margin: 5px;
+ background-color: red;
+ border-radius: 50%;
+ display: inline-block;
+ }
}
.controls-inner-container {
- display: flex;
- flex-direction: row;
- position: relative;
- width: 100%;
- align-items: center;
- justify-content: center;
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
}
.record-button-wrapper {
- width: 35px;
- height: 35px;
- font-size: 0;
- background-color: grey;
- border: 0px;
- border-radius: 35px;
- margin: 10px;
- display: flex;
- justify-content: center;
-
- .record-button {
- background-color: red;
- border: 0px;
- border-radius: 50%;
- height: 80%;
- width: 80%;
- align-self: center;
- margin: 0;
-
- &:hover {
- height: 85%;
- width: 85%;
- }
- }
-
- .stop-button {
- background-color: red;
- border: 0px;
- border-radius: 10%;
- height: 70%;
- width: 70%;
- align-self: center;
- margin: 0;
-
-
- // &:hover {
- // width: 40px;
- // height: 40px
- // }
- }
-
+ width: 35px;
+ height: 35px;
+ font-size: 0;
+ background-color: grey;
+ border: 0px;
+ border-radius: 35px;
+ margin: 10px;
+ display: flex;
+ justify-content: center;
+
+ .record-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 50%;
+ height: 80%;
+ width: 80%;
+ align-self: center;
+ margin: 0;
+
+ &:hover {
+ height: 85%;
+ width: 85%;
+ }
+ }
+
+ .stop-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 10%;
+ height: 70%;
+ width: 70%;
+ align-self: center;
+ margin: 0;
+
+ // &:hover {
+ // width: 40px;
+ // height: 40px
+ // }
+ }
}
.options-wrapper {
- height: 100%;
- display: flex;
- flex-direction: row;
- align-content: center;
- position: relative;
- top: 0;
- bottom: 0;
-
- &.video-edit-wrapper {
-
- // right: 50% - 15;
-
- .track-screen {
- font-weight: 200;
- }
-
- }
-
- &.track-screen-wrapper {
-
- // right: 50% - 30;
-
- .track-screen {
- font-weight: 200;
- color: aqua;
- }
-
- }
-} \ No newline at end of file
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ position: relative;
+ top: 0;
+ bottom: 0;
+
+ &.video-edit-wrapper {
+ // right: 50% - 15;
+
+ .track-screen {
+ font-weight: 200;
+ }
+ }
+
+ &.track-screen-wrapper {
+ // right: 50% - 30;
+
+ .track-screen {
+ font-weight: 200;
+ color: aqua;
+ }
+ }
+}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 89650889d..8c65fd34e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -130,7 +130,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
})
);
- observer.observe(document.getElementsByClassName('scriptingBox')[0]);
+ observer.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]);
}
@action
@@ -670,7 +670,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
const definedParameters = !this.compileParams.length ? null : (
<div className="scriptingBox-plist" style={{ width: '30%' }}>
{this.compileParams.map((parameter, i) => (
- <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
<EditableView
display={'block'}
maxHeight={72}
@@ -749,7 +749,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
{!this.compileParams.length || !this.paramsNames ? null : (
<div className="scriptingBox-plist">
{this.paramsNames.map((parameter: string, i: number) => (
- <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
<div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}>
<div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div>
{this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null}
@@ -811,22 +811,20 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
render() {
TraceMobx();
return (
- <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}>
- <div className="scriptingBox-outerDiv" onWheel={e => this._props.isSelected() && e.stopPropagation()}>
- {this._paramSuggestion ? (
- <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}>
- {' '}
- {this._scriptSuggestedParams}{' '}
- </div>
- ) : null}
- {!this._applied && !this._function ? this.renderScriptingInputs : null}
- {this._applied && !this._function ? this.renderParamsInputs() : null}
- {!this._applied && this._function ? this.renderFunctionInputs() : null}
-
- {!this._applied && !this._function ? this.renderScriptingTools() : null}
- {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null}
- {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null}
- </div>
+ <div className="scriptingBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined} onWheel={e => this._props.isSelected() && e.stopPropagation()}>
+ {this._paramSuggestion ? (
+ <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}>
+ {' '}
+ {this._scriptSuggestedParams}{' '}
+ </div>
+ ) : null}
+ {!this._applied && !this._function ? this.renderScriptingInputs : null}
+ {this._applied && !this._function ? this.renderParamsInputs() : null}
+ {!this._applied && this._function ? this.renderFunctionInputs() : null}
+
+ {!this._applied && !this._function ? this.renderScriptingTools() : null}
+ {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null}
+ {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null}
</div>
);
}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 40647feff..4773a21c9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -8,12 +8,13 @@ import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
+import { dropActionType } from '../../util/DragManager';
import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
@@ -27,7 +28,7 @@ import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { DocumentView } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
@@ -334,18 +335,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc));
this._props.addDocument?.(imageSnapshot);
const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
- link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3);
- setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
+ // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true));
};
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
- const anchor = addAsAnnotation
- ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document
- : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
- PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.Document);
+ const anchor =
+ addAsAnnotation && marquee
+ ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.Document
+ : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document);
return anchor;
};
@@ -372,7 +374,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl
getView = (doc: Doc, options: FocusViewOptions) => {
if (this._stackedTimeline?.makeDocUnfiltered(doc)) {
if (this.heightPercent === 100) {
- this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
+ // do we want to always open up the timeline when followin a link? kind of clunky visually
+ //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent;
options.didMove = true;
}
return this._stackedTimeline.getView(doc, options);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2c5398e40..033b01d24 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -14,7 +14,7 @@ import { listSpec } from '../../../fields/Schema';
import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -34,7 +34,7 @@ import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
import { SidebarAnnos } from '../SidebarAnnos';
import { StyleProp } from '../StyleProvider';
import { DocumentView, OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
import { LinkInfo } from './LinkDocPreview';
import { PinProps, PresBox } from './trails';
import './WebBox.scss';
@@ -83,7 +83,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
return this.webField?.toString() || '';
}
@computed get _urlHash() {
- return ""+ (stringHash(this._url)??'');
+ return '' + (stringHash(this._url) ?? '');
}
@computed get scrollHeight() {
return Math.max(NumCast(this.layoutDoc._height), this._scrollHeight);
@@ -179,9 +179,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
// bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created)
if (this._url) {
const reqdFuncs: { [key: string]: string } = {};
- reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"])`;
- reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"annotations"] = value`;
- reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"sidebar"])`;
+ reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`;
+ reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`;
+ reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`;
DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
}
});
@@ -341,7 +341,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations;
@action
+ iframeMove = (e: PointerEvent) => {
+ const theclick = this.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
+ this._marqueeref.current?.onMove(theclick);
+ };
+ @action
iframeUp = (e: PointerEvent) => {
+ this._iframe?.contentDocument?.removeEventListener('pointermove', this.iframeMove);
+ this.marqueeing = undefined;
this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value
this._textAnnotationCreator = undefined;
this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here.
@@ -358,6 +368,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
+ } else {
+ const theclick = this.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop));
+ if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]);
+ else {
+ if (!(e.target as any)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]);
+ this._getAnchor = AnchorMenu.Instance?.GetAnchor;
+ this.marqueeing = undefined;
+ }
+
+ ContextMenu.Instance.closeMenu();
+ ContextMenu.Instance.setIgnoreEvents(false);
+ if (e?.button === 2 || e?.altKey) {
+ e?.preventDefault();
+ e?.stopPropagation();
+ setTimeout(() => {
+ // if menu comes up right away, the down event can still be active causing a menu item to be selected
+ this.specificContextMenu(undefined as any);
+ this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]);
+ });
+ }
}
};
@action
@@ -400,6 +433,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
};
@action
iframeDown = (e: PointerEvent) => {
+ this._textAnnotationCreator = undefined;
+ const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
+ if (sel?.empty)
+ sel.empty(); // Chrome
+ else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
+
this._props.select(false);
const theclick = this.props
.ScreenToLocalTransform()
@@ -409,6 +448,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) {
this.marqueeing = theclick;
+ this._marqueeref.current?.onInitiateSelection(this.marqueeing);
+ this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove);
e.preventDefault();
}
};
@@ -739,28 +780,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
this.marqueeing = undefined;
}
};
- @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
+ @action finishMarquee = (x?: number, y?: number) => {
this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this.marqueeing = undefined;
- this._textAnnotationCreator = undefined;
- const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection();
- if (sel?.empty)
- sel.empty(); // Chrome
- else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document);
- if (x !== undefined && y !== undefined) {
- ContextMenu.Instance.closeMenu();
- ContextMenu.Instance.setIgnoreEvents(false);
- if (e?.button === 2 || e?.altKey) {
- e?.preventDefault();
- e?.stopPropagation();
- setTimeout(() => {
- // if menu comes up right away, the down event can still be active causing a menu item to be selected
- this.specificContextMenu(undefined as any);
- this.DocumentView?.().onContextMenu(undefined, x, y);
- });
- }
- }
};
@observable lighttext = false;
@@ -782,7 +805,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
className="webBox-htmlSpan"
ref={action((r: any) => {
if (r) {
- this._scrollHeight = Number(getComputedStyle(r).height.replace('px', ''));
+ this._scrollHeight = DivHeight(r);
this.lighttext = Array.from(r.children).some((c: any) => c instanceof HTMLElement && lightOrDark(getComputedStyle(c).color) !== Colors.WHITE);
}
})}
@@ -992,6 +1015,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
}
childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined);
@computed get webpage() {
+ TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any);
const scale = previewScale * (this._props.NativeDimScaling?.() || 1);
@@ -1071,6 +1095,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem
: 'none';
annotationPointerEvents = () => (this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none');
render() {
+ TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as any);
const scale = previewScale * (this._props.NativeDimScaling?.() || 1);
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index 7a0ff8776..d79df4272 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -1,20 +1,11 @@
@import '../../global/globalCssVariables.module.scss';
+.dashFieldView-active,
.dashFieldView {
position: relative;
display: inline-flex;
align-items: center;
- select {
- display: none;
- }
-
- &:hover {
- select {
- display: unset;
- }
- }
-
.dashFieldView-enumerables {
width: 10px;
height: 10px;
@@ -35,6 +26,7 @@
display: inline-block;
font-weight: normal;
background: rgba(0, 0, 0, 0.1);
+ cursor: default;
}
.dashFieldView-fieldSpan {
min-width: 8px;
@@ -50,6 +42,27 @@
}
}
}
+
+.dashFieldView,
+.dashFieldView-active {
+ .dashFieldView-select {
+ height: 10p;
+ font-size: 12px;
+ background: transparent;
+ opacity: 0;
+ width: 5px;
+ }
+}
+
+.dashFieldView {
+ &:hover {
+ .dashFieldView-select {
+ opacity: unset;
+ width: 15px !important;
+ }
+ }
+}
+
.ProseMirror-selectedNode {
outline: solid 1px $light-blue !important;
}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index b49e7dcf0..439d4785e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,15 +1,14 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
-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, StrCast } from '../../../../fields/Types';
+import { Cast, DocCast } from '../../../../fields/Types';
import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { CollectionViewType } from '../../../documents/DocumentTypes';
@@ -17,32 +16,55 @@ import { Transform } from '../../../util/Transform';
import { undoable, undoBatch } from '../../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
+import { FilterPanel } from '../../FilterPanel';
import { ObservableReactComponent } from '../../ObservableReactComponent';
import { OpenWhere } from '../DocumentView';
import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
-import { FilterPanel } from '../../FilterPanel';
+import { DocData } from '../../../../fields/DocSymbols';
+import { NodeSelection } from 'prosemirror-state';
export class DashFieldView {
dom: HTMLDivElement; // container for label and value
root: any;
node: any;
tbox: FormattedTextBox;
+ getpos: any;
+ @observable _nodeSelected = false;
+ NodeSelected = () => this._nodeSelected;
- unclickable = () => !this.tbox._props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
+ unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ makeObservable(this);
+ const self = this;
this.node = node;
this.tbox = tbox;
+ this.getpos = getPos;
this.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
this.dom.style.position = 'relative';
this.dom.style.display = 'inline-block';
- this.dom.onkeypress = function (e: any) {
+ const tBox = this.tbox;
+ this.dom.onkeypress = function (e: KeyboardEvent) {
e.stopPropagation();
};
- this.dom.onkeydown = function (e: any) {
+ this.dom.onkeydown = function (e: KeyboardEvent) {
e.stopPropagation();
+ if (e.key === 'Tab') {
+ e.preventDefault();
+ const editor = tbox.EditorView;
+ if (editor) {
+ const state = editor.state;
+ for (var i = self.getpos() + 1; i < state.doc.content.size; i++) {
+ if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) {
+ editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i))));
+ return;
+ }
+ }
+ // tBox.setFocus(state.selection.to);
+ }
+ }
};
this.dom.onkeyup = function (e: any) {
e.stopPropagation();
@@ -62,7 +84,9 @@ export class DashFieldView {
width={node.attrs.width}
height={node.attrs.height}
hideKey={node.attrs.hideKey}
+ hideValue={node.attrs.hideValue}
editable={node.attrs.editable}
+ nodeSelected={this.NodeSelected}
tbox={tbox}
/>
);
@@ -75,9 +99,11 @@ export class DashFieldView {
});
}
deselectNode() {
+ runInAction(() => (this._nodeSelected = false));
this.dom.classList.remove('ProseMirror-selectednode');
}
selectNode() {
+ setTimeout(() => runInAction(() => (this._nodeSelected = true)), 100);
this.dom.classList.add('ProseMirror-selectednode');
}
}
@@ -86,10 +112,12 @@ interface IDashFieldViewInternal {
fieldKey: string;
docId: string;
hideKey: boolean;
+ hideValue: boolean;
tbox: FormattedTextBox;
width: number;
height: number;
editable: boolean;
+ nodeSelected: () => boolean;
node: any;
getPos: any;
unclickable: () => boolean;
@@ -102,18 +130,19 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
_fieldKey: string;
_fieldRef = React.createRef<HTMLDivElement>();
@observable _dashDoc: Doc | undefined = undefined;
- @observable _expanded = false;
+ @observable _expanded = this._props.nodeSelected();
constructor(props: IDashFieldViewInternal) {
super(props);
makeObservable(this);
this._fieldKey = this._props.fieldKey;
this._textBoxDoc = this._props.tbox.Document;
+ const setDoc = action((doc: Doc) => (this._dashDoc = doc));
if (this._props.docId) {
- DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc));
} else {
- this._dashDoc = this._props.tbox.Document;
+ setDoc(this._props.tbox.Document);
}
}
@@ -127,31 +156,44 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
componentWillUnmount() {
this._reactionDisposer?.();
}
- return100 = () => 100;
+ isRowActive = () => (this._props.nodeSelected() || this._expanded) && this._props.editable;
+
+ finishEdit = action(() => {
+ if (this._expanded) {
+ this._expanded = false;
+ // if the edit finishes, then we want to lose focus on the textBox unless something else in the textBox got focus
+ // the timeout allows switching focus from one dashFieldView to another in the same text box
+ setTimeout(() => !this._props.tbox.ProseRef?.contains(document.activeElement) && this._props.tbox._props.onBlur?.());
+ }
+ });
+ selectedCell = (): [Doc, number] | undefined => (this._dashDoc ? [this._dashDoc, 0] : undefined);
+ columnWidth = () => Math.min(this._props.tbox._props.PanelWidth(), Math.max(50, this._props.tbox._props.PanelWidth() - 100)); // try to leave room for the fieldKey
// set the display of the field's value (checkbox for booleans, span of text for strings)
@computed get fieldValueContent() {
return !this._dashDoc ? null : (
- <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this._props.hideKey ? this._props.tbox._props.PanelWidth() - 20 : undefined }}>
+ <div onClick={action(e => (this._expanded = !this._props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: !this._hideKey && this._expanded ? this.columnWidth() : undefined }}>
<SchemaTableCell
Document={this._dashDoc}
col={0}
deselectCell={emptyFunction}
selectCell={emptyFunction}
- maxWidth={this._props.hideKey ? undefined : this.return100}
- columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero}
- selectedCell={() => [this._dashDoc!, 0]}
+ maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
+ columnWidth={this._expanded || this._props.nodeSelected() ? this.columnWidth : returnZero}
+ selectedCell={this.selectedCell}
fieldKey={this._fieldKey}
rowHeight={returnZero}
- isRowActive={() => this._expanded && this._props.editable}
+ isRowActive={this.isRowActive}
padding={0}
getFinfo={emptyFunction}
setColumnValues={returnFalse}
allowCRs={true}
- oneLine={!this._expanded}
- finishEdit={action(() => (this._expanded = false))}
+ oneLine={!this._expanded && !this._props.nodeSelected()}
+ finishEdit={this.finishEdit}
transform={Transform.Identity}
menuTarget={null}
+ autoFocus={true}
+ rootSelected={this._props.tbox._props.rootSelected}
/>
</div>
);
@@ -174,21 +216,50 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
}
};
+ toggleFieldHide = undoable(
+ action(() => {
+ const editor = this._props.tbox.EditorView!;
+ editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideKey: this._props.node.attrs.hideValue ? false : !this._props.node.attrs.hideKey ? true : false }));
+ }),
+ 'hideKey'
+ );
+
+ toggleValueHide = undoable(
+ action(() => {
+ const editor = this._props.tbox.EditorView!;
+ editor.dispatch(editor.state.tr.setNodeMarkup(this._props.getPos(), this._props.node.type, { ...this._props.node.attrs, hideValue: this._props.node.attrs.hideKey ? false : !this._props.node.attrs.hideValue ? true : false }));
+ }),
+ 'hideValue'
+ );
+
+ @computed get _hideKey() {
+ return this._props.hideKey && !this._expanded;
+ }
+
+ @computed get _hideValue() {
+ return this._props.hideValue && !this._props.nodeSelected();
+ }
+
// clicking on the label creates a pivot view collection of all documents
// in the same collection. The pivot field is the fieldKey of this label
- onPointerDownLabelSpan = (e: any) => {
+ onPointerDownLabelSpan = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
DashFieldViewMenu.createFieldView = this.createPivotForField;
+ DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide;
+ DashFieldViewMenu.toggleValueHide = this.toggleValueHide;
DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
+ const editor = this._props.tbox.EditorView!;
+ setTimeout(() => editor.dispatch(editor.state.tr.setSelection(new NodeSelection(editor.state.doc.resolve(this._props.getPos())))), 100);
});
};
@undoBatch
selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => {
- event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value);
+ event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value === '-unset-' ? undefined : event.target.value);
};
@computed get values() {
+ if (this._props.nodeSelected()) return [];
const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
return vals.strings.map(facet => ({ value: facet, label: facet }));
@@ -197,21 +268,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
render() {
return (
<div
- className="dashFieldView"
+ className={`dashFieldView${this.isRowActive() ? '-active' : ''}`}
ref={this._fieldRef}
style={{
width: this._props.width,
height: this._props.height,
- pointerEvents: this._props.tbox._props.isSelected() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
+ pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
}}>
- {this._props.hideKey ? null : (
+ {this._hideKey ? null : (
<span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
- {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
+ {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey}
</span>
)}
- {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
+ {this._props.fieldKey.startsWith('#') || this._hideValue ? null : this.fieldValueContent}
{!this.values.length ? null : (
- <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}>
+ <select className="dashFieldView-select" tabIndex={-1} defaultValue={this._dashDoc && Field.toKeyValueString(this._dashDoc, this._fieldKey)} onChange={this.selectVal}>
+ <option value="-unset-">-unset-</option>
{this.values.map(val => (
<option value={val.value}>{val.label}</option>
))}
@@ -225,6 +297,8 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
static Instance: DashFieldViewMenu;
static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
+ static toggleFieldHide: () => void = emptyFunction;
+ static toggleValueHide: () => void = emptyFunction;
constructor(props: any) {
super(props);
DashFieldViewMenu.Instance = this;
@@ -234,6 +308,14 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
DashFieldViewMenu.createFieldView(e);
DashFieldViewMenu.Instance.fadeOut(true);
};
+ toggleFieldHide = (e: React.MouseEvent) => {
+ DashFieldViewMenu.toggleFieldHide();
+ DashFieldViewMenu.Instance.fadeOut(true);
+ };
+ toggleValueHide = (e: React.MouseEvent) => {
+ DashFieldViewMenu.toggleValueHide();
+ DashFieldViewMenu.Instance.fadeOut(true);
+ };
@observable _fieldKey = '';
@@ -249,11 +331,27 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
};
render() {
return this.getElement(
- <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.showFields}>
- <FontAwesomeIcon icon="eye" size="lg" />
- </button>
- </Tooltip>
+ <>
+ <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
+ <button className="antimodeMenu-button" onPointerDown={this.showFields}>
+ <FontAwesomeIcon icon="eye" size="sm" />
+ </button>
+ </Tooltip>
+ {this._fieldKey.startsWith('#') ? null : (
+ <Tooltip key="key" title={<div className="dash-tooltip">Toggle view of field key</div>}>
+ <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}>
+ <FontAwesomeIcon icon="bullseye" size="sm" />
+ </button>
+ </Tooltip>
+ )}
+ {this._fieldKey.startsWith('#') ? null : (
+ <Tooltip key="val" title={<div className="dash-tooltip">Toggle view of field value</div>}>
+ <button className="antimodeMenu-button" onPointerDown={this.toggleValueHide}>
+ <FontAwesomeIcon icon="hashtag" size="sm" />
+ </button>
+ </Tooltip>
+ )}
+ </>
);
}
}
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index b786c5ffb..b90653acc 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -8,6 +8,7 @@ import { StrCast } from '../../../../fields/Types';
import './DashFieldView.scss';
import EquationEditor from './EquationEditor';
import { FormattedTextBox } from './FormattedTextBox';
+import { DocData } from '../../../../fields/DocSymbols';
export class EquationView {
dom: HTMLDivElement; // container for label and value
@@ -88,7 +89,6 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
}
e.stopPropagation();
}}
- onKeyPress={e => e.stopPropagation()}
style={{
position: 'relative',
display: 'inline-block',
@@ -96,12 +96,11 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
height: this.props.height,
background: 'white',
borderRadius: '10%',
- bottom: 3,
}}>
<EquationEditor
ref={this._ref}
- value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')}
- onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)}
+ value={StrCast(this._textBoxDoc[DocData][this._fieldKey])}
+ onChange={(str: any) => (this._textBoxDoc[DocData][this._fieldKey] = str)}
autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
autoOperatorNames="sin cos tan"
spaceBehavesLikeTab={true}
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
index cf48e1250..b327e5137 100644
--- a/src/client/views/nodes/formattedText/FootnoteView.tsx
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -23,6 +23,7 @@ export class FootnoteView {
this.dom = document.createElement('footnote');
this.dom.addEventListener('pointerup', this.toggle, true);
+ this.dom.addEventListener('mouseup', (e: MouseEvent) => e.stopPropagation(), true);
// These are used when the footnote is selected
this.innerView = null;
}
@@ -82,9 +83,10 @@ export class FootnoteView {
document.removeEventListener('pointerup', this.ignore, true);
};
- toggle = () => {
+ toggle = (e: PointerEvent) => {
if (this.innerView) this.close();
else this.open();
+ e.stopPropagation();
};
close() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 03ff0436b..38dd2e847 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -273,6 +273,7 @@ footnote::before {
height: 20px;
&::before {
content: '→';
+ cursor: default;
}
&:hover {
background: orange;
@@ -348,6 +349,8 @@ footnote::before {
touch-action: none;
span {
font-family: inherit;
+ background-color: inherit;
+ display: contents; // fixes problem where extra space is added around <ol> lists when inside a prosemirror span
}
blockquote {
@@ -397,6 +400,7 @@ footnote::before {
font-family: inherit;
}
margin-left: 0;
+ background-color: inherit;
}
.decimal2-ol {
counter-reset: deci2;
@@ -406,6 +410,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 2.1em;
+ background-color: inherit;
}
.decimal3-ol {
counter-reset: deci3;
@@ -415,6 +420,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 2.85em;
+ background-color: inherit;
}
.decimal4-ol {
counter-reset: deci4;
@@ -424,6 +430,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 3.85em;
+ background-color: inherit;
}
.decimal5-ol {
counter-reset: deci5;
@@ -432,6 +439,7 @@ footnote::before {
font-family: inherit;
}
font-size: smaller;
+ background-color: inherit;
}
.decimal6-ol {
counter-reset: deci6;
@@ -440,6 +448,7 @@ footnote::before {
font-family: inherit;
}
font-size: smaller;
+ background-color: inherit;
}
.decimal7-ol {
counter-reset: deci7;
@@ -448,6 +457,7 @@ footnote::before {
font-family: inherit;
}
font-size: smaller;
+ background-color: inherit;
}
.multi1-ol {
@@ -458,6 +468,7 @@ footnote::before {
}
margin-left: 0;
padding-left: 1.2em;
+ background-color: inherit;
}
.multi2-ol {
counter-reset: multi2;
@@ -467,6 +478,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 2em;
+ background-color: inherit;
}
.multi3-ol {
counter-reset: multi3;
@@ -476,6 +488,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 2.85em;
+ background-color: inherit;
}
.multi4-ol {
counter-reset: multi4;
@@ -485,6 +498,7 @@ footnote::before {
}
font-size: smaller;
padding-left: 3.85em;
+ background-color: inherit;
}
//.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
@@ -788,6 +802,7 @@ footnote::before {
height: 20px;
&::before {
content: '→';
+ cursor: default;
}
&:hover {
background: orange;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 40acf164f..f07c2d191 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -21,16 +21,16 @@ import { List } from '../../../../fields/List';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from '../../../../fields/RichTextField';
import { ComputedField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
import { MakeTemplate } from '../../../util/DropConverter';
import { LinkManager } from '../../../util/LinkManager';
import { RTFMarkup } from '../../../util/RTFMarkup';
@@ -51,7 +51,7 @@ import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
import { media_state } from '../AudioBox';
import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
import { LinkInfo } from '../LinkDocPreview';
import { PinProps, PresBox } from '../trails';
import { DashDocCommentView } from './DashDocCommentView';
@@ -70,8 +70,13 @@ import { SummaryView } from './SummaryView';
import { isDarkMode } from '../../../util/reportManager/reportManagerUtils';
import Select from 'react-select';
// import * as applyDevTools from 'prosemirror-dev-tools';
+
+interface FormattedTextBoxProps extends FieldViewProps {
+ onBlur?: () => void; // callback when text loses focus
+ autoFocus?: boolean; // whether text should get input focus when created
+}
@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface {
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
@@ -88,7 +93,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
private _sidebarRef = React.createRef<SidebarAnnos>();
private _sidebarTagRef = React.createRef<React.Component>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _scrollRef: HTMLDivElement | null = null;
private _editorView: Opt<EditorView>;
public _applyingChange: string = '';
private _inDrop = false;
@@ -101,8 +106,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
private _dropDisposer?: DragManager.DragDropDisposer;
private _recordingStart: number = 0;
private _ignoreScroll = false;
- private _lastText = '';
- private _hadDownFocus = false;
private _focusSpeed: Opt<number>;
private _keymap: any = undefined;
private _rules: RichTextRules | undefined;
@@ -203,7 +206,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
}
- constructor(props: FieldViewProps) {
+ constructor(props: FormattedTextBoxProps) {
super(props);
makeObservable(this);
FormattedTextBox.Instance = this;
@@ -245,8 +248,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
- if (!pinProps && this._editorView?.state.selection.empty) return this.Document;
- const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.Document.title), annotationOn: this.Document });
+ const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document);
+ if (!pinProps && this._editorView?.state.selection.empty) return rootDoc;
+ const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc });
this.addDocument(anchor);
this._finishingLink = true;
this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
@@ -274,29 +278,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let stopFunc: any;
const targetData = target[DocData];
targetData.mediaState = media_state.Recording;
- targetData.audioAnnoState = 'recording';
DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop));
- let reactionDisposer = reaction(
+ const reactionDisposer = reaction(
() => target.mediaState,
- action(dictation => {
+ dictation => {
if (!dictation) {
- targetData.audioAnnoState = 'stopped';
stopFunc();
reactionDisposer();
}
- })
+ }
);
target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
}
});
};
- AnchorMenu.Instance.Highlight = undoable(
- action((color: string, isLinkButton: boolean) => {
- this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
- return undefined;
- }),
- 'highlght text'
- );
+ AnchorMenu.Instance.Highlight = undoable((color: string) => {
+ this._editorView?.state && RichTextMenu.Instance?.setHighlight(color);
+ return undefined;
+ }, 'highlght text');
AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
@@ -307,7 +306,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
e.preventDefault();
e.stopPropagation();
const targetCreator = (annotationOn?: Doc) => {
- const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, undefined, annotationOn);
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn);
FormattedTextBox.SetSelectOnLoad(target);
return target;
};
@@ -329,8 +328,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
leafText = (node: Node) => {
if (node.type === this._editorView?.state.schema.nodes.dashField) {
- const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
- return Field.toJavascriptString(refDoc[node.attrs.fieldKey as string] as Field);
+ const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc);
+ const fieldKey = StrCast(node.attrs.fieldKey);
+ return (
+ (node.attrs.hideKey ? '' : fieldKey + ':') + //
+ (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field))
+ );
}
return '';
};
@@ -345,12 +348,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
tryUpdateDoc = (force: boolean) => {
if (this._editorView && (this._editorView as any).docView) {
const state = this._editorView.state;
- const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
+ const dataDoc = this.dataDoc;
const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
const newJson = JSON.stringify(state.toJSON());
const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template
const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+ const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype
const effectiveAcl = GetEffectiveAcl(dataDoc);
const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, '');
@@ -367,24 +371,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
let unchanged = true;
- if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
+ const textChange = newText !== prevData?.Text; // the Text string can change even if the RichText doesn't because dashFieldViews may return new strings as the data they reference changes
+ if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
this._applyingChange = this.fieldKey;
- const textChange = newText !== prevData?.Text;
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
- if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
+ if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) {
// if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) {
+ if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) {
const numstring = NumCast(dataDoc[this.fieldKey], null);
- dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
- dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited
+ dataDoc[this.fieldKey] =
+ numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined;
textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText });
+ this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false
unchanged = false;
}
} else {
// if we've deleted all the text in a note driven by a template, then restore the template data
dataDoc[this.fieldKey] = undefined;
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
- dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data)));
ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText });
unchanged = false;
}
@@ -450,16 +454,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
autoLink = () => {
const newAutoLinks = new Set<Doc>();
- const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords);
+ const oldAutoLinks = LinkManager.Links(this.Document).filter(
+ link =>
+ ((!Doc.isTemplateForField(this.Document) &&
+ (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
+ (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) ||
+ (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) &&
+ link.link_relationship === LinkManager.AutoKeywords
+ ); // prettier-ignore
if (this._editorView?.state.doc.textContent) {
- const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
const f = this._editorView.state.selection.from;
+
const t = this._editorView.state.selection.to;
var tr = this._editorView.state.tr as any;
const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
- Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
- tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+ Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
+ tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
}
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink);
@@ -469,7 +480,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text);
if (
!this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing
- (title.startsWith('-') || title.startsWith('@')) &&
+ title.startsWith('-') &&
this._editorView &&
!this.dataDoc.title_custom &&
(Doc.LayoutFieldKey(this.Document) === this.fieldKey || this.fieldKey === 'text')
@@ -477,14 +488,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let node = this._editorView.state.doc;
while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild;
const str = node.textContent;
- const prefix = str.startsWith('@') ? '' : '-';
+ const prefix = '-';
const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title));
if (!(cfield instanceof ComputedField)) {
this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim();
- if (str.startsWith('@') && str.length > 1) {
- Doc.AddToMyPublished(this.Document);
- }
}
}
};
@@ -501,7 +509,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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(/^@/, '');
+ const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, '');
var alink: Doc | undefined;
this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
if (
@@ -624,7 +632,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
docId: draggedDoc[Id],
float: 'unset',
});
- if (!['embed', 'copy'].includes((dropAction ?? '') as any)) {
+ if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) {
added = dragData.removeDocument?.(draggedDoc) ? true : false;
} else {
added = true;
@@ -748,7 +756,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const hr = Math.round(Date.now() / 1000 / 60 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
- this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView)
+ this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
};
@observable _showSidebar = false;
@@ -759,7 +767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@action
toggleSidebar = (preview: boolean = false) => {
const defaultSidebar = 250;
- const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', ''));
+ const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!);
if (preview) this._showSidebar = true;
else {
this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1;
@@ -878,7 +886,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
event: undoBatch(() => {
this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout;
this.Document.layout_fieldKey = 'layout_meta';
- setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50);
+ setTimeout(() => (this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50), 50);
}),
icon: 'eye',
});
@@ -940,9 +948,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
description: 'Make Default Layout',
event: () => {
if (!this.layoutDoc.isTemplateDoc) {
- const title = StrCast(this.Document.title);
- this.Document.title = 'text';
- MakeTemplate(this.Document, true, title);
+ MakeTemplate(this.Document);
}
Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document);
Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document);
@@ -953,6 +959,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' });
optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
this._props.renderDepth &&
@@ -967,8 +974,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
});
- optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+ const help = cm.findByDescription('Help...');
+ const helpItems = help && 'subitems' in help ? help.subitems : [];
+ helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
+ !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
@@ -1012,7 +1022,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const state = this._editorView.state;
const to = state.selection.to;
const updated = TextSelection.create(state.doc, to, to);
- this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to));
+ this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({})));
if (this._recordingDictation) {
this.recordDictation();
}
@@ -1212,7 +1222,7 @@ 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(
- () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize }),
+ () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
(autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
);
this._disposers.highlights = reaction(
@@ -1259,9 +1269,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
);
this._disposers.editorState = reaction(
() => {
- const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc;
- const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc;
- return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey]) ?? StrCast(whichDoc[this.fieldKey])) };
+ const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey];
+ const dataData = this.dataDoc[this.fieldKey];
+ const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey];
+ const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+ const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+ const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0;
+ const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData;
+ const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData;
+ return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) };
},
incomingValue => {
if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1271,11 +1287,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateScrollHeight();
}
- } else if (incomingValue?.str) {
- selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue.str)));
+ } else if (this._editorView.state.doc.textContent !== incomingValue?.str) {
+ selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
}
}
- }
+ },
+ { fireImmediately: true }
);
this._disposers.search = reaction(
@@ -1313,25 +1330,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
);
if (this._recordingDictation) setTimeout(this.recordDictation);
}
- var quickScroll: string | undefined = '';
this._disposers.scroll = reaction(
() => NumCast(this.layoutDoc._layout_scrollTop),
pos => {
- if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) {
- const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
- const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
- const durationSecStr = viewTrans.match(/([0-9.]*)s/);
- const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
- if (duration) {
- this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper);
- } else {
- this._scrollRef.current.scrollTo({ top: pos });
- }
+ if (!this._ignoreScroll && this._scrollRef) {
+ const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]+)(m?)s/);
+ const duration = Number(durationStr?.[1]) * (durationStr?.[2] ? 1 : 1000);
+ this._scrollStopper = smoothScroll(duration || 0, this._scrollRef, Math.abs(pos || 0), 'ease', this._scrollStopper);
}
},
{ fireImmediately: true }
);
- quickScroll = undefined;
this.tryUpdateScrollHeight();
setTimeout(this.tryUpdateScrollHeight, 250);
}
@@ -1371,15 +1380,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
if (pdfAnchor instanceof Doc) {
const dashField = view.state.schema.nodes.paragraph.create({}, [
- view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, hideValue: false, editable: false }, undefined, [
view.state.schema.marks.linkAnchor.create({
allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }],
- title: `from: ${DocCast(pdfAnchor.embedContainer).title}`,
+ title: StrCast(pdfAnchor.title),
noPreview: true,
- docref: false,
+ docref: true,
+ fontSize: '8px',
}),
- view.state.schema.marks.pFontSize.create({ fontSize: '8px' }),
- view.state.schema.marks.em.create({}),
]),
]);
@@ -1424,14 +1432,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
handleScrollToSelection: editorView => {
const docPos = editorView.coordsAtPos(editorView.state.selection.to);
const viewRect = self._ref.current!.getBoundingClientRect();
- const scrollRef = self._scrollRef.current;
+ const scrollRef = self._scrollRef;
const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined;
const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined;
if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) {
const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale;
if (this._focusSpeed !== undefined) {
- scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper));
+ setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)));
} else {
scrollRef.scrollTo({ top: scrollPos });
}
@@ -1482,39 +1490,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
- if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
- const selLoadChar = FormattedTextBox.SelectOnLoadChar;
+ const selLoadChar = FormattedTextBox.SelectOnLoadChar;
+ if (selectOnLoad) {
FormattedTextBox.SelectOnLoad = undefined;
+ FormattedTextBox.SelectOnLoadChar = '';
+ }
+ if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
this._props.select(false);
if (selLoadChar) {
const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const tr = this._editorView.state.tr
- .setStoredMarks(storedMarks)
- .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
- .setStoredMarks(storedMarks);
+ const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
+ const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1);
+ const tr = tr2.setStoredMarks(storedMarks);
+
this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
} else if (curText && !FormattedTextBox.DontSelectInitialText) {
selectAll(this._editorView.state, this._editorView?.dispatch);
}
}
- selectOnLoad && this._editorView!.focus();
+ if (selectOnLoad) {
+ FormattedTextBox.DontSelectInitialText = false;
+ this._editorView!.focus();
+ }
if (this._props.isContentActive()) this.prepareForTyping();
- if (this._editorView) {
- const tr = this._editorView.state.tr;
- const { from, to } = tr.selection;
- // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated.
- if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250);
-
- if (FormattedTextBox.PasteOnLoad) {
- const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
- FormattedTextBox.PasteOnLoad = undefined;
- pdfAnchorId && this.addPdfReference(pdfAnchorId);
- }
+ if (this._editorView && FormattedTextBox.PasteOnLoad) {
+ const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
+ FormattedTextBox.PasteOnLoad = undefined;
+ pdfAnchorId && this.addPdfReference(pdfAnchorId);
}
- FormattedTextBox.DontSelectInitialText = false;
+ if (this._props.autoFocus) setTimeout(() => this._editorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it.
}
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
@@ -1587,7 +1594,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
(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();
}
@@ -1596,23 +1602,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
document.removeEventListener('pointerup', this.onSelectEnd);
};
onPointerUp = (e: React.PointerEvent): void => {
- const editor = this._editorView!;
- const state = editor?.state;
- if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime) && !this._hadDownFocus) {
- (this.ProseRef?.children[0] as HTMLElement)?.blur?.();
- }
- if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
- if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
- else if (this._props.isContentActive()) {
- const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
- let xpos = pcords?.pos || 0;
- while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) {
- xpos = xpos - 1;
- }
- editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(xpos))));
+ const state = this.EditorView?.state;
+ if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) {
+ if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
+ for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement);
while (target && !target.dataset?.targethrefs) target = target.parentElement;
- FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
+ FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
}
};
@action
@@ -1633,10 +1629,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
e.stopPropagation();
}
};
- setFocus = () => {
- const pos = this._editorView?.state.selection.$from.pos || 1;
- (this.ProseRef?.children?.[0] as any).focus();
- setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+ setFocus = (ipos?: number) => {
+ const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
+ setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+ setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200);
};
@action
onFocused = (e: React.FocusEvent): void => {
@@ -1688,10 +1684,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._forceUncollapse = false;
clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
const clickPos = this._editorView!.posAtCoords({ left: x, top: y });
- let olistPos = clickPos?.pos;
+ const clickPosVal = clickPos?.pos || 1;
+ let olistPos = clickPosVal;
if (clickPos && olistPos && this._props.rootSelected?.()) {
- const clickNode = this._editorView?.state.doc.nodeAt(olistPos);
- const nodeBef = this._editorView?.state.doc.nodeAt(Math.max(0, olistPos - 1));
+ const clickNode = this._editorView?.state.doc.resolve(olistPos).node();
+ const nodeBef = this._editorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node();
olistPos = nodeBef?.type === this._editorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos;
let $olistPos = this._editorView?.state.doc.resolve(olistPos);
let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef;
@@ -1701,18 +1698,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
$olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]);
}
}
- const listPos = this._editorView?.state.doc.resolve(clickPos.pos);
- const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos);
+ const maxSize = this._editorView?.state.doc.content.size ?? 0;
+ const listPos = this._editorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal));
+ const listNode = listPos?.node();
if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
if (!highlightOnly) {
if (selectOrderedList) {
this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
} else {
- const tr = this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
- this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, clickPos.pos)));
+ const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1);
+ if (this._editorView.state.doc.nodeAt(nodePos)) {
+ const tr = this._editorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
+ this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos)));
+ }
}
}
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'lightgray' });
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'gray' });
}
}
}
@@ -1729,33 +1730,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
const stordMarks = this._editorView?.state.storedMarks?.slice();
- this.autoLink();
- if (this._editorView?.state.tr) {
- const tr = stordMarks?.reduce((tr, m) => {
- tr.addStoredMark(m);
- return tr;
- }, this._editorView.state.tr);
- tr && this._editorView.dispatch(tr);
+ if (!(this.EditorView?.state.selection instanceof NodeSelection)) {
+ this.autoLink();
+ if (this._editorView?.state.tr) {
+ const tr = stordMarks?.reduce((tr, m) => {
+ tr.addStoredMark(m);
+ return tr;
+ }, this._editorView.state.tr);
+ tr && this._editorView.dispatch(tr);
+ }
}
}
if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
}
FormattedTextBox._hadSelection = window.getSelection()?.toString() !== '';
+
+ // this is the markdown for @<published name> document publishing to Doc.myPublishedDocs
+ const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/);
+ if (match) {
+ this.dataDoc.title_custom = true;
+ this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc
+ this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1));
+ }
+
this.endUndoTypingBatch();
FormattedTextBox.LiveTextUndo?.end();
FormattedTextBox.LiveTextUndo = undefined;
const state = this._editorView!.state;
- if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) {
- UndoManager.RunInBatch(() => {
- this.dataDoc.title_custom = true;
- this.dataDoc.layout_showTitle = 'title';
- const tr = this._editorView!.state.tr;
- this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection());
- }, 'titler');
- }
+ // if the text box blurs and none of its contents are focused(), then pass the blur along
+ setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.());
};
onKeyDown = (e: React.KeyboardEvent) => {
@@ -1788,7 +1794,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
(document.activeElement as any).blur?.();
SelectionManager.DeselectAll();
- RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
+ RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
return;
case 'Enter':
this.insertTime();
@@ -1801,7 +1807,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
default:
if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
case ' ':
- if (e.code !== 'Space') {
+ if (e.code !== 'Space' && e.code !== 'Backspace') {
[AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) &&
this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
@@ -1815,28 +1821,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
};
onScroll = (e: React.UIEvent) => {
- if (!LinkInfo.Instance?.LinkInfo && this._scrollRef.current) {
- if (!this._props.dontSelectOnLoad) {
- this._ignoreScroll = true;
- this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop;
- this._ignoreScroll = false;
- e.stopPropagation();
- e.preventDefault();
- }
+ if (!LinkInfo.Instance?.LinkInfo && this._scrollRef) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._scrollRef.scrollTop;
+ this._ignoreScroll = false;
+ e.stopPropagation();
+ e.preventDefault();
}
};
tryUpdateScrollHeight = () => {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (children && !SnappingManager.IsDragging) {
- const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0'));
- const toHgt = (node: Element) => {
+ const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
+ const toNum = (val: string) => Number(val.replace('px', ''));
+ const toHgt = (node: Element): number => {
const { height, marginTop, marginBottom } = getComputedStyle(node);
- return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
+ const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height);
+ return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
};
- const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins);
+ const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children);
const scrollHeight = this.ProseRef && proseHeight;
- if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) {
+ if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);
@@ -1960,11 +1966,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
</div>
);
}
- cycleAlternateText = () => {
- if (this.layoutDoc._layout_enableAltContentUI) {
- const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
- this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
- }
+ cycleAlternateText = (skipHover?: boolean) => {
+ this.layoutDoc._layout_enableAltContentUI = true;
+ const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
+ this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined;
};
@computed get overlayAlternateIcon() {
const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
@@ -1999,7 +2004,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
);
}
get fieldKey() {
- const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
+ return this._fieldKey;
+ }
+ @computed get _fieldKey() {
+ const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : '');
}
@observable _isHovering = false;
@@ -2016,11 +2024,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
if (this._props.isContentActive()) {
const scale = this._props.NativeDimScaling?.() || 1;
- const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
+ const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
const height = Number(styleFromLayoutString.height?.replace('px', ''));
// prevent default if selected || child is active but this doc isn't scrollable
if (
- (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && //
+ !Number.isNaN(height) &&
+ (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && //
(this._props.rootSelected?.() || this.isAnyChildContentActive())
) {
e.preventDefault();
@@ -2048,12 +2057,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0);
const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
- const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
+ const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
return styleFromLayoutString?.height === '0px' ? null : (
<div
className="formattedTextBox"
- onPointerEnter={action(() => (this._isHovering = true))}
- onPointerLeave={action(() => (this._isHovering = false))}
+ onPointerEnter={action(() => {
+ this._isHovering = true;
+ this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true);
+ })}
+ onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))}
ref={r => {
this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
this._oldWheel = r;
@@ -2095,15 +2107,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onDoubleClick={this.onDoubleClick}>
<div
className="formattedTextBox-outer"
- ref={this._scrollRef}
+ ref={r => (this._scrollRef = r)}
style={{
- width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
+ width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
<div
- className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`}
+ className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`}
ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
@@ -2114,8 +2126,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}}
/>
</div>
- {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
- {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
+ {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
+ {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
{this.audioHandle}
{this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
</div>
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 47527847b..03c902580 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -1,7 +1,7 @@
import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands';
import { redo, undo } from 'prosemirror-history';
import { Schema } from 'prosemirror-model';
-import { splitListItem, wrapInList } from 'prosemirror-schema-list';
+import { splitListItem, wrapInList, sinkListItem, liftListItem } from 'prosemirror-schema-list';
import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state';
import { liftTarget } from 'prosemirror-transform';
import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
@@ -11,8 +11,8 @@ import { Docs } from '../../../documents/Documents';
import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
import { OpenWhere } from '../DocumentView';
-import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
import { Doc } from '../../../../fields/Doc';
+import { EditorView } from 'prosemirror-view';
const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -89,7 +89,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
if (!canEdit(state)) return true;
const ref = state.selection;
const range = ref.$from.blockRange(ref.$to);
- const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ const marks = state.storedMarks || state.selection.$to.parentOffset ? state.selection.$from.marks() : undefined;
if (
!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
const tx3 = updateBullets(tx2, schema);
@@ -102,11 +102,14 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
if (
!wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => {
- const tx3 = updateBullets(tx2, schema);
+ const tx25 = updateBullets(tx2, schema);
+ const ol_node = tx25.doc.nodeAt(range!.start)!;
+ const tx3 = tx25.setNodeMarkup(range!.start, ol_node.type, ol_node.attrs, marks);
// when promoting to a list, assume list will format things so don't copy the stored marks.
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
- dispatch(tx3);
+ const tx4 = tx3.setSelection(TextSelection.near(tx3.doc.resolve(state.selection.to + 2)));
+ dispatch(tx4);
})
) {
console.log('bullet promote fail');
@@ -120,7 +123,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (
- !liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ !liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
const tx3 = updateBullets(tx2, schema);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
@@ -164,12 +167,6 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
SelectionManager.DeselectAll();
});
- const splitMetadata = (marks: any, tx: Transaction) => {
- marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
- marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
- return tx;
- };
-
bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true));
bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true));
bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
@@ -260,7 +257,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
- bind('Backspace', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const backspace = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView) => {
if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
@@ -272,6 +269,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
if (
!joinBackward(state, (tx: Transaction) => {
dispatch(updateBullets(tx, schema));
+ if (view.state.selection.$anchor.node(-1)?.type === schema.nodes.list_item) {
+ // gets rid of an extra paragraph when joining two list items together.
+ joinBackward(view.state, (tx: Transaction) => view.dispatch(tx));
+ }
})
) {
if (
@@ -284,59 +285,80 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
}
}
return true;
- });
+ };
+ bind('Backspace', backspace);
//newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock
//command to break line
- bind('Enter', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+
+ const enter = (state: EditorState, dispatch: (tx: Transaction) => void, view: EditorView, once = true) => {
if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const trange = state.selection.$from.blockRange(state.selection.$to);
- const path = (state.selection.$from as any).path;
- const depth = trange ? liftTarget(trange) : undefined;
- const split = path.length > 5 && !path[path.length - 3].textContent && path[path.length - 6].type !== schema.nodes.list_item;
- if (split && trange && depth !== undefined && depth !== null) {
+ const depth = trange ? liftTarget(trange) : null;
+ if (
+ depth !== null &&
+ state.selection.$from.node(-1)?.type === state.schema.nodes.blockquote && //
+ !state.selection.$from.node().content.size &&
+ trange
+ ) {
dispatch(state.tr.lift(trange, depth) as any);
return true;
}
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- const cr = state.selection.$from.node().textContent.endsWith('\n');
- if (/*cr ||*/ !newlineInCode(state, dispatch as any)) {
- if (
+ if (!newlineInCode(state, dispatch as any)) {
+ const olNode = view.state.selection.$anchor.node(-2);
+ const liNode = view.state.selection.$anchor.node(-1);
+ // prettier-ignore
+ if (liNode?.type === schema.nodes.list_item && !liNode.textContent &&
+ olNode?.type === schema.nodes.ordered_list && once && view.state.selection.$from.depth === 3)
+ {
+ // handles case of hitting enter at then end of a top-level empty list item - the result is to create a paragraph
+ for (let i = 0; i < 10 && view.state.selection.$from.depth > 1 && liftListItem(schema.nodes.list_item)(view.state, view.dispatch); i++);
+ } else if (
!splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => {
const tx3 = updateBullets(tx2, schema);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
+ // removes an extra paragraph created when selecting text across two list items or splitting an empty list item
+ !once && view.dispatch(view.state.tr.deleteRange(view.state.selection.from - 5, view.state.selection.from - 2));
})
) {
- const fromattrs = state.selection.$from.node().attrs;
- if (
- !splitBlockKeepMarks(state, (tx3: Transaction) => {
- const tonode = tx3.selection.$to.node();
- if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
- const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
- splitMetadata(marks, tx4);
- if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as (tx: Transaction) => void)) {
+ if (once && view.state.selection.$from.node(-2)?.type === schema.nodes.ordered_list && view.state.selection.$from.node(-1)?.type === schema.nodes.list_item && view.state.selection.$from.node(-1)?.textContent === '') {
+ // handles case of hitting enter on an empty list item which needs to create a second empty paragraph, then split it by calling enter() again
+ view.dispatch(view.state.tr.insert(view.state.selection.from, schema.nodes.paragraph.create({})));
+ enter(view.state, view.dispatch, view, false);
+ } else {
+ const fromattrs = state.selection.$from.node().attrs;
+ if (
+ !splitBlockKeepMarks(state, (tx3: Transaction) => {
+ const tonode = tx3.selection.$to.node();
+ if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
+ const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
dispatch(tx4);
}
- } else dispatch(tx3.insertText('\r\n'));
- })
- ) {
- return false;
+
+ if (view.state.selection.$anchor.nodeAfter?.type === schema.nodes.text && once) {
+ // if text is selected across list items, then we need to forcibly insert a new line since the splitBlock code joins the two list items.
+ enter(view.state, dispatch, view, false);
+ }
+ })
+ ) {
+ return false;
+ }
}
}
}
return true;
- });
+ };
+ bind('Enter', enter);
//Command to create a blank space
bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
if (props.TemplateDataDocument && GetEffectiveAcl(props.TemplateDataDocument) != AclEdit && GetEffectiveAcl(props.TemplateDataDocument) != AclAugment && GetEffectiveAcl(props.TemplateDataDocument) != AclAdmin) return true;
- const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- dispatch(splitMetadata(marks, state.tr));
return false;
});
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index cd0cdaa74..cecf106a3 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -27,7 +27,10 @@ const { toggleMark } = require('prosemirror-commands');
@observer
export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
- @observable static Instance: RichTextMenu;
+ static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined });
+ static get Instance() {
+ return RichTextMenu._instance?.menu;
+ }
public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
private _linkToRef = React.createRef<HTMLInputElement>();
@@ -48,7 +51,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _activeFontSize: string = '13px';
@observable private _activeFontFamily: string = '';
- @observable private activeListType: string = '';
+ @observable private _activeListType: string = '';
@observable private _activeAlignment: string = 'left';
@observable private brushMarks: Set<Mark> = new Set();
@@ -67,7 +70,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
- RichTextMenu.Instance = this;
+ RichTextMenu._instance.menu = this;
this.updateMenu(undefined, undefined, props, this.layoutDoc);
this._canFade = false;
this.Pinned = true;
@@ -100,11 +103,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get fontSize() {
return this._activeFontSize;
}
+ @computed get listStyle() {
+ return this._activeListType;
+ }
@computed get textAlign() {
return this._activeAlignment;
}
@computed get textVcenter() {
- return BoolCast(this.layoutDoc?.layout_centered);
+ return BoolCast(this.layoutDoc?._layout_centered);
}
_disposer: IReactionDisposer | undefined;
componentDidMount() {
@@ -131,11 +137,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return;
}
- // update active marks
- const activeMarks = this.getActiveMarksOnSelection();
- this.setActiveMarkButtons(activeMarks);
-
- // update active font family and size
+ this.setActiveMarkButtons(this.getActiveMarksOnSelection());
const active = this.getActiveFontStylesOnSelection();
const activeFamilies = active.activeFamilies;
const activeSizes = active.activeSizes;
@@ -144,7 +146,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc();
const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey);
- this.activeListType = this.getActiveListStyle();
+ this._activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
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];
@@ -161,17 +163,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item);
const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list);
const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst);
- const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from;
+ const fromRange = numberRange(state.selection.from).reverse();
+ const newPos = nodeOl ? fromRange.find(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) ?? state.selection.from : state.selection.from;
const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined);
- if (node?.type === schema.nodes.ordered_list) {
- let attrs = node.attrs;
- if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family };
- if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize };
- if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color };
- const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema);
- dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to))));
- }
- {
+ if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) {
+ const hasMark = node.marks.some(m => m.type === mark.type);
+ const otherMarks = node.marks.filter(m => m.type !== mark.type);
+ const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey]));
+ const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]);
+ dispatch(updateBullets(markup, state.schema));
+ } else {
const state = this.view?.state;
const tr = this.view?.state.tr;
if (tr && state) {
@@ -201,16 +202,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// finds font sizes and families in selection
getActiveListStyle() {
- if (this.view && this.TextView?._props.rootSelected?.()) {
- const path = (this.view.state.selection.$from as any).path;
- for (let i = 0; i < path.length; i += 3) {
- if (path[i].type === this.view.state.schema.nodes.ordered_list) {
- return path[i].attrs.mapStyle;
+ const state = this.view?.state;
+ if (state) {
+ const pos = state.selection.$anchor;
+ for (let i = 0; i < pos.depth; i++) {
+ const node = pos.node(i);
+ if (node.type === schema.nodes.ordered_list) {
+ return node.attrs.mapStyle;
}
}
- if (this.view.state.selection.$from.nodeAfter?.type === this.view.state.schema.nodes.ordered_list) {
- return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle;
- }
}
return '';
}
@@ -224,11 +224,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (this.view && this.TextView?._props.rootSelected?.()) {
const state = this.view.state;
const pos = this.view.state.selection.$from;
- const marks: Mark[] = [...(state.storedMarks ?? [])];
+ var marks: Mark[] = [...(state.storedMarks ?? [])];
if (state.storedMarks !== null) {
} else if (state.selection.empty) {
- const ref_node = this.reference_node(pos);
- marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : []));
+ for (let i = 0; i <= pos.depth; i++) {
+ marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks];
+ }
} else {
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark));
@@ -255,41 +256,26 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
//finds all active marks on selection in given group
getActiveMarksOnSelection() {
- let activeMarks: MarkType[] = [];
- if (!this.view || !this.TextView?._props.rootSelected?.()) return activeMarks;
+ if (!this.view || !this.TextView?._props.rootSelected?.()) return [] as MarkType[];
- const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
- if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
- //current selection
- const { empty, ranges, $to } = this.view.state.selection as TextSelection;
const state = this.view.state;
- if (!empty) {
- activeMarks = markGroup.filter(mark => {
- const has = false;
- for (let i = 0; !has && i < ranges.length; i++) {
- return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
- }
- return false;
- });
- } else {
- const pos = this.view.state.selection.$from;
- const ref_node: ProsNode | null = this.reference_node(pos);
- if (ref_node !== null && ref_node !== this.view.state.doc) {
- if (ref_node.isText) {
- } else {
- return [];
- }
- activeMarks = markGroup.filter(mark_type => {
- // if (mark_type === state.schema.marks.pFontSize) {
- // return mark.isINSet
- // ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
- // }
- const mark = state.schema.mark(mark_type);
- return mark.isInSet(ref_node.marks);
- });
+ var marks: Mark[] = [...(state.storedMarks ?? [])];
+ const pos = this.view.state.selection.$from;
+ if (state.storedMarks !== null) {
+ } else if (state.selection.empty) {
+ for (let i = 0; i <= pos.depth; i++) {
+ marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks];
}
+ } else {
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
+ node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark));
+ });
}
- return activeMarks;
+ const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ return markGroup.filter(mark_type => {
+ const mark = state.schema.mark(mark_type);
+ return mark.isInSet(marks);
+ });
}
@action
@@ -318,6 +304,23 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
}
+ elideSelection = (txstate: EditorState | undefined = undefined, visibility = false) => {
+ const state = txstate ?? this.view?.state;
+ if (!state || state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr.addMark(state.tr.selection.from, state.selection.to, mark);
+ const text = tr.selection.content();
+ const elideNode = state.schema.nodes.summary.create({ visibility, text, textslice: text.toJSON() });
+ const summary = tr.replaceSelectionWith(elideNode).removeMark(tr.selection.from - 1, tr.selection.from, mark);
+ const expanded = () => {
+ const endOfElidableText = summary.selection.to + text.content.size;
+ const res = summary.insert(summary.selection.to, text.content).insert(endOfElidableText, state.schema.nodes.paragraph.create({}));
+ return res.setSelection(new TextSelection(res.doc.resolve(endOfElidableText + 1)));
+ };
+ this.view?.dispatch?.(visibility ? expanded() : summary);
+ return true;
+ };
+
toggleNoAutoLinkAnchor = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor);
@@ -353,7 +356,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setFontSize = (fontSize: string) => {
if (this.view) {
if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) {
- this.TextView.dataDoc.fontSize = fontSize;
+ this.TextView.dataDoc[this.TextView.fieldKey + '_fontSize'] = fontSize;
this.view.focus();
} else {
const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
@@ -368,7 +371,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setFontFamily = (family: string) => {
if (this.view) {
- const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family });
+ const fmark = this.view.state.schema.marks.pFontFamily.create({ family });
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
} else if (SelectionManager.Views.length) {
@@ -400,40 +403,24 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// TODO: remove doesn't work
// remove all node type and apply the passed-in one to the selected text
changeListType = (mapStyle: string) => {
- const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle();
- const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle });
- if (!this.view || nodeType?.attrs.mapStyle === '') return;
-
- const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list;
- let inList: any = undefined;
- let fromList = -1;
- const path: any = Array.from((this.view.state.selection.$from as any).path);
- for (let i = 0; i < path.length; i++) {
- if (path[i]?.type === schema.nodes.ordered_list) {
- inList = path[i];
- fromList = path[i - 1];
- }
- }
+ const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle();
+ const newMapStyle = active === mapStyle ? '' : mapStyle;
+ if (!this.view || newMapStyle === '') return;
+ let inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list;
const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
- if (
- inList ||
+ if (inList) {
+ const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos);
+ marks && tx2.ensureMarks([...marks]);
+ marks && tx2.setStoredMarks([...marks]);
+ this.view.dispatch(tx2);
+ } else
!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
+ const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
-
- this.view!.dispatch(tx2);
- })
- ) {
- const tx2 = this.view.state.tr;
- if (nodeType && (inList || nextIsOL)) {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
- this.view.dispatch(tx3);
- }
- }
+ this.view!.dispatch(tx3);
+ });
this.view.focus();
this.updateMenu(this.view, undefined, this.props, this.layoutDoc);
};
@@ -450,7 +437,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
vcenterToggle = (view: EditorView, dispatch: any) => {
- this.layoutDoc && (this.layoutDoc.layout_centered = !this.layoutDoc.layout_centered);
+ this.layoutDoc && (this.layoutDoc._layout_centered = !this.layoutDoc._layout_centered);
};
align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
if (this.TextView?._props.rootSelected?.()) {
@@ -548,7 +535,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// todo: add brushes to brushMap to save with a style name
onBrushNameKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
- RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
+ RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
this._brushNameRef.current!.style.background = 'lightGray';
}
};
@@ -556,7 +543,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@action
clearBrush() {
- RichTextMenu.Instance.brushMarks = new Set();
+ RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set());
}
@action
@@ -692,118 +679,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
};
- linkExtend($start: ResolvedPos, href: string) {
- const mark = this.view!.state.schema.marks.linkAnchor;
-
- let startIndex = $start.index();
- let endIndex = $start.indexAfter();
-
- while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) startIndex--;
- while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.allAnchors.find((item: { href: string }) => item.href === href)).length) endIndex++;
-
- let startPos = $start.start();
- let endPos = startPos;
- for (let i = 0; i < endIndex; i++) {
- const size = $start.parent.child(i).nodeSize;
- if (i < startIndex) startPos += size;
- endPos += size;
- }
- return { from: startPos, to: endPos };
- }
-
- reference_node(pos: ResolvedPos): ProsNode | null {
- if (!this.view) return null;
-
- let ref_node: ProsNode = this.view.state.doc;
- if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
- ref_node = pos.nodeBefore;
- }
- if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
- if (!pos.nodeBefore || this.view.state.selection.$from.pos !== this.view.state.selection.$to.pos) {
- ref_node = pos.nodeAfter;
- }
- }
- if (!ref_node && pos.pos > 0) {
- let skip = false;
- for (let i: number = pos.pos - 1; i > 0; i--) {
- this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
- if (node.isLeaf && !skip) {
- ref_node = node;
- skip = true;
- }
- });
- }
- }
- if (!ref_node.isLeaf && ref_node.childCount > 0) {
- ref_node = ref_node.child(0);
- }
- return ref_node;
- }
-
render() {
return null;
- // TraceMobx();
- // const row1 = <div className="antimodeMenu-row" key="row 1" style={{ display: this.collapsed ? "none" : undefined }}>{[
- // //!this.collapsed ? this.getDragger() : (null),
- // // !this.Pinned ? (null) : <div key="frag1"> {[
- // // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
- // // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
- // // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
- // // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
- // // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
- // // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
- // // <div className="richTextMenu-divider" key="divider" />
- // // ]}</div>,
- // this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
- // this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
- // this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
- // this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
- // this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
- // this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
- // this.createColorButton(),
- // this.createHighlighterButton(),
- // this.createLinkButton(),
- // this.createBrushButton(),
- // <div className="collectionMenu-divider" key="divider 2" />,
- // this.createButton("align-left", "Align Left", this.activeAlignment === "left", this.alignLeft),
- // this.createButton("align-center", "Align Center", this.activeAlignment === "center", this.alignCenter),
- // this.createButton("align-right", "Align Right", this.activeAlignment === "right", this.alignRight),
- // this.createButton("indent", "Inset More", undefined, this.insetParagraph),
- // this.createButton("outdent", "Inset Less", undefined, this.outsetParagraph),
- // this.createButton("hand-point-left", "Hanging Indent", undefined, this.hangingIndentParagraph),
- // this.createButton("hand-point-right", "Indent", undefined, this.indentParagraph),
- // ]}</div>;
-
- // const row2 = <div className="antimodeMenu-row row-2" key="row2">
- // {this.collapsed ? this.getDragger() : (null)}
- // <div key="row 2" style={{ display: this.collapsed ? "none" : undefined }}>
- // <div className="collectionMenu-divider" key="divider 3" />
- // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => {
- // this.activeFontSize = val;
- // SelectionManager.Views.map(dv => dv.Document._text_fontSize = val);
- // })),
- // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => {
- // this.activeFontFamily = val;
- // SelectionManager.Views.map(dv => dv.Document._text_fontFamily = val);
- // })),
- // <div className="collectionMenu-divider" key="divider 4" />,
- // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})),
- // this.createButton("sort-amount-down", "Summarize", undefined, this.insertSummarizer),
- // this.createButton("quote-left", "Blockquote", undefined, this.insertBlockquote),
- // this.createButton("minus", "Horizontal Rule", undefined, this.insertHorizontalRule)
- // ]}
- // </div>
- // {/* <div key="collapser">
- // {<div key="collapser">
- // <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}>
- // <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} />
- // </button>
- // </div> }
- // <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}>
- // <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} />
- // </button>
- // </div> */}
- // </div>;
}
}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 9bd41f42c..42665830f 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,6 +1,6 @@
import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
@@ -8,13 +8,14 @@ import { NumCast, StrCast } from '../../../../fields/Types';
import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { CollectionView } from '../../collections/CollectionView';
+import { ContextMenu } from '../../ContextMenu';
+import { KeyValueBox } from '../KeyValueBox';
import { FormattedTextBox } from './FormattedTextBox';
import { wrappingInputRule } from './prosemirrorPatches';
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;
@@ -78,7 +79,7 @@ export class RichTextRules {
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.DocumentView?.().setToggleDetail('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);
@@ -136,6 +137,7 @@ export class RichTextRules {
textDocInline.title = inlineFieldKey; // give the annotation its own title
textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.isDataDoc = true;
textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
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
@@ -242,10 +244,19 @@ export class RichTextRules {
}),
// activate a style by name using prefix '%<color name>'
- new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => {
+ new InputRule(new RegExp(/%[a-zA-Z_]+$/), (state, match, start, end) => {
const color = match[0].substring(1, match[0].length);
- const marks = RichTextMenu.Instance._brushMap.get(color);
-
+ const marks = RichTextMenu.Instance?._brushMap.get(color);
+
+ if (
+ DocListCast((Doc.UserDoc().template_notes as Doc).data)
+ .concat(DocListCast((Doc.UserDoc().template_user as Doc).data))
+ .map(d => StrCast(d.title))
+ .includes(color)
+ ) {
+ setTimeout(() => this.TextBox.DocumentView?.().switchViews(true, color, undefined, true));
+ return state.tr.deleteRange(start, end);
+ }
if (marks) {
const tr = state.tr.deleteRange(start, end);
return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
@@ -266,7 +277,7 @@ export class RichTextRules {
// toggle alternate text UI %/
new InputRule(new RegExp(/%\//), (state, match, start, end) => {
- setTimeout(this.TextBox.cycleAlternateText);
+ setTimeout(() => this.TextBox.cycleAlternateText(true));
return state.tr.deleteRange(start, end);
}),
@@ -282,85 +293,108 @@ export class RichTextRules {
: tr;
}),
- // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
- // [[<fieldKey> : <Doc>]]
- // [[:docTitle]] => hyperlink
- // [[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-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);
+ // create a hyperlink to a titled document
+ // @(<doctitle>)
+ new InputRule(new RegExp(/@\(([a-zA-Z_@\.\? \-0-9]+)\)/), (state, match, start, end) => {
+ const docTitle = match[1];
+ const prefixLength = '@('.length;
+ if (docTitle) {
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))));
+ const editor = this.TextBox.EditorView;
+ const selection = editor?.state?.selection.$from.pos;
+ if (editor) {
+ const estate = editor.state;
+ editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength))));
}
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 teditor = this.TextBox.EditorView;
+ if (teditor && selection) {
+ const tstate = teditor.state;
+ teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection))));
}
};
const getTitledDoc = (docTitle: string) => {
if (!DocServer.FindDocByTitle(docTitle)) {
- Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
+ Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_fitWidth: true, _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;
+ const target = getTitledDoc(docTitle);
+ if (target) {
+ setTimeout(() => linkToDoc(target));
+ return state.tr.insertText(' ').deleteRange(start, start + prefixLength);
}
- if (value?.includes(',')) {
+ }
+ return state.tr;
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value]
+ // [@{this,doctitle,}.fieldKey]
+ new InputRule(
+ new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_\(\)\.@\?\+\-\*\/\ 0-9\(\)]*))?\]/),
+ (state, match, start, end) => {
+ const docTitle = match[1].substring(1).replace(/\.$/, '');
+ const fieldKey = match[2];
+ const assign = match[4] === ':' ? (match[4] = '') : match[4];
+ const value = match[5];
+ const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('=');
+ const getTitledDoc = (docTitle: string) => DocServer.FindDocByTitle(docTitle);
+ // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' )
+ if (value?.includes(',') && !value.startsWith('((')) {
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;
+ } else if (value) {
+ KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined:
+ (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore
+ if (fieldKey === this.TextBox.fieldKey) return this.TextBox.EditorView!.state.tr;
}
- const target = getTitledDoc(docTitle);
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+ const target = docTitle ? getTitledDoc(docTitle) : undefined;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, hideValue: false });
return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
},
{ inCode: true }
),
+ // pass the contents between '((' and '))' to chatGPT and append the result
+ new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))$/), (state, match, start, end) => {
+ var count = 0; // ignore first return value which will be the notation that chat is pending a result
+ KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
+ if (count) {
+ const tr = this.TextBox.EditorView?.state.tr.insertText(' ' + (gptval as string));
+ tr && this.TextBox.EditorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(end + 2), tr.doc.resolve(end + 2 + (gptval as string).length))));
+ RichTextMenu.Instance?.elideSelection(this.TextBox.EditorView?.state, true);
+ }
+ count++;
+ });
+ return null;
+ }),
+
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
- // wiki:title
- new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => {
- const title = match[1];
+ // @(wiki:title)
+ new InputRule(new RegExp(/@\(wiki:([a-zA-Z_@:\.\?\-0-9 ]+)\)$/), (state, match, start, end) => {
+ const title = match[1].trim().replace(/ /g, '_');
this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))));
this.TextBox.makeLinkAnchor(undefined, 'add:right', `https://en.wikipedia.org/wiki/${title.trim()}`, 'wikipedia reference');
const fstate = this.TextBox.EditorView?.state;
if (fstate) {
- const tr = fstate?.tr.deleteRange(start, start + 5);
- return tr.setSelection(new TextSelection(tr.doc.resolve(end - 5))).insertText(' ');
+ const tr = fstate?.tr.deleteRange(start, start + '@(wiki:'.length);
+ return tr.setSelection(new TextSelection(tr.doc.resolve(end - '@(wiki:'.length))).insertText(' ');
}
return state.tr;
}),
// create an inline equation node
- // eq:<equation>>
- new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+ // %eq
+ new InputRule(new RegExp(/%eq/), (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
- this.TextBox.dataDoc[fieldKey] = match[1];
+ this.TextBox.dataDoc[fieldKey] = 'y=';
const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
}),
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index a141ef041..ccf7de4a1 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -74,6 +74,7 @@ export const marks: { [index: string]: MarkSpec } = {
allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
title: { default: null },
noPreview: { default: false },
+ fontSize: { default: null },
docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
},
inclusive: false,
@@ -93,14 +94,16 @@ export const marks: { [index: string]: MarkSpec } = {
const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
return node.attrs.docref && node.attrs.title
? [
- 'div',
+ 'a',
['span', 0],
[
'span',
{
...node.attrs,
class: 'prosemirror-attribution',
+ 'data-targethrefs': targethrefs,
href: node.attrs.allAnchors[0].href,
+ style: `font-size: ${node.attrs.fontSize}`,
},
node.attrs.title,
],
@@ -232,22 +235,6 @@ export const marks: { [index: string]: MarkSpec } = {
},
},
- metadata: {
- toDOM() {
- return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
- },
- },
- metadataKey: {
- toDOM() {
- return ['span', { style: 'font-style:italic; ' }];
- },
- },
- metadataVal: {
- toDOM() {
- return ['span'];
- },
- },
-
summarizeInclusive: {
parseDOM: [
{
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 4706a97fa..62b8b03d6 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -1,4 +1,3 @@
-import * as React from 'react';
import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
import { listItem, orderedList } from 'prosemirror-schema-list';
import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
@@ -25,6 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = {
// :: NodeSpec The top level document node.
doc: {
content: 'block+',
+ marks: '_',
},
paragraph: ParagraphNodeSpec,
@@ -121,7 +121,6 @@ export const nodes: { [index: string]: NodeSpec } = {
...ParagraphNodeSpec.attrs,
level: { default: 1 },
},
- defining: true,
parseDOM: [
{ tag: 'h1', attrs: { level: 1 } },
{ tag: 'h2', attrs: { level: 2 } },
@@ -132,8 +131,7 @@ export const nodes: { [index: string]: NodeSpec } = {
],
toDOM(node) {
const dom = toParagraphDOM(node) as any;
- const level = node.attrs.level || 1;
- dom[0] = 'h' + level;
+ dom[0] = `h${node.attrs.level || 1}`;
return dom;
},
getAttrs(dom: any) {
@@ -265,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = {
fieldKey: { default: '' },
docId: { default: '' },
hideKey: { default: false },
+ hideValue: { default: false },
editable: { default: true },
},
leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
@@ -331,12 +330,10 @@ export const nodes: { [index: string]: NodeSpec } = {
...orderedList,
content: 'list_item+',
group: 'block',
+ marks: '_',
attrs: {
bulletStyle: { default: 0 },
- mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet"
- fontColor: { default: 'inherit' },
- fontSize: { default: undefined },
- fontFamily: { default: undefined },
+ mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet",
visibility: { default: true },
indent: { default: undefined },
},
@@ -376,9 +373,10 @@ export const nodes: { [index: string]: NodeSpec } = {
],
toDOM(node: Node) {
const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : '';
- const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : '';
- const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : '';
- const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : '';
+ const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight);
+ const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize);
+ const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family);
+ const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color);
const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : '';
if (node.attrs.mapStyle === 'bullet') {
return [
@@ -386,7 +384,7 @@ export const nodes: { [index: string]: NodeSpec } = {
{
'data-mapStyle': node.attrs.mapStyle,
'data-bulletStyle': node.attrs.bulletStyle,
- style: `${fsize} ${ffam} ${fcol} ${marg}`,
+ style: `${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`,
},
0,
];
@@ -398,7 +396,7 @@ export const nodes: { [index: string]: NodeSpec } = {
class: `${map}-ol`,
'data-mapStyle': node.attrs.mapStyle,
'data-bulletStyle': node.attrs.bulletStyle,
- style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`,
+ style: `list-style: none; ${fhigh} ${fsize} ${ffam} ${fcol} ${marg}`,
},
0,
]
@@ -422,16 +420,22 @@ export const nodes: { [index: string]: NodeSpec } = {
},
},
],
- toDOM(node: any) {
+ toDOM(node: Node) {
+ const fhigh = (found => (found ? `background-color: ${found};` : ''))(node.marks.find(m => m.type.name === 'marker')?.attrs.highlight);
+ const fsize = (found => (found ? `font-size: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontSize')?.attrs.fontSize);
+ const ffam = (found => (found ? `font-family: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontFamily')?.attrs.family);
+ const fcol = (found => (found ? `color: ${found};` : ''))(node.marks.find(m => m.type.name === 'pFontColor')?.attrs.color);
const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : '';
return [
'li',
- { class: `${map}`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle },
+ { class: `${map}`, style: `${fhigh} ${fsize} ${ffam} ${fcol} `, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle },
node.attrs.visibility
? 0
: [
'span',
- { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre` },
+ {
+ style: `${fhigh} ${fsize} ${ffam} ${fcol} position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre`,
+ },
`${node.firstChild?.textContent}...`,
],
];
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index 87e1b69c3..a485ea4c3 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -13,7 +13,7 @@ import { DocumentManager } from '../../../util/DocumentManager';
import { CollectionDockingView } from '../../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { OpenWhereMod } from '../DocumentView';
-import { ImageBox } from '../ImageBox';
+import { ImageBox, ImageEditorData } from '../ImageBox';
import './GenerativeFill.scss';
import Buttons from './GenerativeFillButtons';
import { BrushHandler } from './generativeFillUtils/BrushHandler';
@@ -419,8 +419,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
// Closes the editor view
const handleViewClose = () => {
- ImageBox.setImageEditorOpen(false);
- ImageBox.setImageEditorSource('');
+ ImageEditorData.Open = false;
+ ImageEditorData.Source = '';
if (newCollectionRef.current) {
DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce());
}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 9019f9945..d3abaddd9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -18,6 +18,7 @@ import { DocServer } from '../../../DocServer';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
+import { dropActionType } from '../../../util/DragManager';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SelectionManager } from '../../../util/SelectionManager';
import { SerializationHelper } from '../../../util/SerializationHelper';
@@ -32,7 +33,7 @@ import { ViewBoxBaseComponent, ViewBoxInterface } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from '../FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
import { ScriptingBox } from '../ScriptingBox';
import './PresBox.scss';
import ReactLoading from 'react-loading';
@@ -875,7 +876,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (pinProps.pinData.temporal) {
pinDoc.config_clipStart = targetDoc._layout_currentTimecode;
const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1);
- pinDoc.config_clipEnd = NumCast(targetDoc.clipEnd, duration);
+ pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration);
}
}
if (pinProps?.pinViewport) {
@@ -911,7 +912,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
const finished = () => {
afterNav?.();
- console.log('Finish Slide Nav: ' + targetDoc.title);
targetDoc[Animation] = undefined;
};
const selViewCache = Array.from(this.selectedArray);
@@ -1250,7 +1250,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
childLayoutTemplate = () => Docs.Create.PresElementBoxDocument();
- removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.Document, this.fieldKey, doc);
+ removeDocument = (doc: Doc | Doc[]) => !(doc instanceof Doc ? [doc] : doc).map(d => Doc.RemoveDocFromList(this.Document, this.fieldKey, d)).some(p => !p);
getTransform = () => this.ScreenToLocalBoxXf().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
panelHeight = () => this._props.PanelHeight() - 40;
/**
@@ -2682,10 +2682,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// prettier-ignore
switch (layout) {
case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y });
- case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
- case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
- case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
- case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y })
+ case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+ case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+ case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y });
+ case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _freeform_fitContentsToBox: true, x, y })
}
};
@@ -3067,7 +3067,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
childIgnoreNativeSize={true}
moveDocument={returnFalse}
ignoreUnrendered={true}
- childDragAction="move"
+ childDragAction={dropActionType.move}
setContentViewBox={emptyFunction}
//childLayoutFitWidth={returnTrue}
childOpacity={returnOne}