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.tsx109
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx83
-rw-r--r--src/client/views/nodes/ColorBox.tsx6
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx68
-rw-r--r--src/client/views/nodes/DataViz.tsx20
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx180
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss41
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx322
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.scss (renamed from src/client/views/nodes/DataVizBox/TableBox.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx105
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts67
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx30
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx56
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx83
-rw-r--r--src/client/views/nodes/DocumentView.scss7
-rw-r--r--src/client/views/nodes/DocumentView.tsx1289
-rw-r--r--src/client/views/nodes/EquationBox.tsx28
-rw-r--r--src/client/views/nodes/FieldView.tsx53
-rw-r--r--src/client/views/nodes/FilterBox.scss193
-rw-r--r--src/client/views/nodes/FilterBox.tsx588
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx31
-rw-r--r--src/client/views/nodes/ImageBox.scss12
-rw-r--r--src/client/views/nodes/ImageBox.tsx236
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx84
-rw-r--r--src/client/views/nodes/KeyValuePair.scss64
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx27
-rw-r--r--src/client/views/nodes/LabelBox.tsx13
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx48
-rw-r--r--src/client/views/nodes/LinkBox.tsx55
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx40
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx204
-rw-r--r--src/client/views/nodes/MapBox/MapBox2.tsx641
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx14
-rw-r--r--src/client/views/nodes/PDFBox.tsx266
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx4533
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx324
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx106
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx181
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx38
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx35
-rw-r--r--src/client/views/nodes/VideoBox.tsx233
-rw-r--r--src/client/views/nodes/WebBox.scss1
-rw-r--r--src/client/views/nodes/WebBox.tsx527
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js4
-rw-r--r--src/client/views/nodes/button/ButtonScripts.ts16
-rw-r--r--src/client/views/nodes/button/FontIconBox.scss30
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx607
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx57
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx16
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx66
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx209
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx1
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss70
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx628
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts78
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx50
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts109
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx1
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts4
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts10
-rw-r--r--src/client/views/nodes/trails/PresBox.scss3
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx1336
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss6
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx230
72 files changed, 7424 insertions, 7621 deletions
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1d59d3356..0cb849923 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,10 +7,11 @@ import { Doc, DocListCast } from '../../../fields/Doc';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
import { undoBatch } from '../../util/UndoManager';
import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
@@ -18,6 +19,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import './AudioBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
/**
* AudioBox
@@ -37,7 +39,7 @@ declare class MediaRecorder {
constructor(e: any); // whatever MediaRecorder has
}
-enum media_state {
+export enum media_state {
PendingRecording = 'pendingRecording',
Recording = 'recording',
Paused = 'paused',
@@ -70,17 +72,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _muted: boolean = false;
@observable _paused: boolean = false; // is recording paused
// @observable rawDuration: number = 0; // computed from the length of the audio element when loaded
-
- constructor(props: any) {
- super(props);
- this.props.setContentView?.(this);
- }
-
@computed get recordingStart() {
return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime();
}
@computed get rawDuration() {
- return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
+ return NumCast(this.dataDoc[`${this.fieldKey}_duration`]);
} // bcz: shouldn't be needed since it's computed from audio element
// mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^
// if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly
@@ -89,7 +85,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return this.props.PanelHeight() < 50;
} // used to collapse timeline when node is shrunk
@computed get links() {
- return DocListCast(this.dataDoc.links);
+ return LinkManager.Links(this.dataDoc);
}
@computed get mediaState() {
return this.dataDoc.mediaState as media_state;
@@ -117,6 +113,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
componentDidMount() {
+ this.props.setContentView?.(this);
if (this.path) {
this.mediaState = media_state.Paused;
this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
@@ -126,30 +123,29 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
getLinkData(l: Doc) {
- let la1 = l.anchor1 as Doc;
- let la2 = l.anchor2 as Doc;
+ let la1 = l.link_anchor_1 as Doc;
+ let la2 = l.link_anchor_2 as Doc;
const linkTime = this.timeline?.anchorStart(la2) || this.timeline?.anchorStart(la1) || 0;
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
- la1 = l.anchor2 as Doc;
- la2 = l.anchor1 as Doc;
+ la1 = l.link_anchor_2 as Doc;
+ la2 = l.link_anchor_1 as Doc;
}
return { la1, la2, linkTime };
}
- getAnchor = (addAsAnnotation: boolean) => {
- return (
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor =
CollectionStackedTimeline.createAnchor(
this.rootDoc,
this.dataDoc,
this.annotationKey,
- '_timecodeToShow' /* audioStart */,
- '_timecodeToHide' /* audioEnd */,
- this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
+ this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
undefined,
undefined,
addAsAnnotation
- ) || this.rootDoc
- );
+ ) || this.rootDoc;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+ return anchor;
};
// updates timecode and shows it in timeline, follows links at time
@@ -159,12 +155,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.links
.map(l => this.getLinkData(l))
.forEach(({ la1, la2, linkTime }) => {
- if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < this._ele!.currentTime) {
+ if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) {
Doc.linkFollowHighlight(la1);
}
});
- this.layoutDoc._currentTimecode = this._ele.currentTime;
- this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode));
+ this.layoutDoc._layout_currentTimecode = this._ele.currentTime;
+ this.timeline?.scrollToTime(NumCast(this.layoutDoc._layout_currentTimecode));
}
};
@@ -201,8 +197,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// removes from currently playing display
@action
removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ const docView = this.props.DocumentView?.();
+ if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
};
@@ -210,11 +207,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// adds doc to currently playing display
@action
addCurrentlyPlaying = () => {
+ const docView = this.props.DocumentView?.();
if (!CollectionStackedTimeline.CurrentlyPlaying) {
CollectionStackedTimeline.CurrentlyPlaying = [];
}
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(docView);
}
};
@@ -223,7 +221,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (this.mediaState === media_state.Recording) {
setTimeout(this.updateRecordTime, 30);
if (!this._paused) {
- this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
+ this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
}
}
};
@@ -255,7 +253,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._recorder = undefined;
const now = new Date().getTime();
this._paused && (this._pausedTime += now - this._pauseStart);
- this.dataDoc[this.fieldKey + '-duration'] = (now - this._recordStart - this._pausedTime) / 1000;
+ this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000;
this.mediaState = media_state.Paused;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
@@ -329,18 +327,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
};
- // pause play back
+ IsPlaying = () => this.mediaState === media_state.Playing;
+ TogglePause = () => {
+ if (this.mediaState === media_state.Paused) this.Play();
+ else this.pause();
+ };
+ // pause playback without removing from the playback list to allow user to play it again.
@action
- Pause = () => {
+ pause = () => {
if (this._ele) {
this._ele.pause();
this.mediaState = media_state.Paused;
// if paused in the middle of playback, prevents restart on next play
if (!this._finished) clearTimeout(this._play);
- this.removeCurrentlyPlaying();
}
};
+ // pause playback and remove from playback list
+ @action
+ Pause = () => {
+ this.pause();
+ this.removeCurrentlyPlaying();
+ };
// for dictation button, creates a text document for dictation
onFile = (e: any) => {
@@ -354,10 +362,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
Doc.GetProto(newDoc).recordingSource = this.dataDoc;
Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`);
Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+ if (Doc.IsInMyOverlay(this.rootDoc)) {
newDoc.overlayX = this.rootDoc.x;
newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+ Doc.AddToMyOverlay(newDoc);
} else {
this.props.addDocument?.(newDoc);
}
@@ -414,11 +422,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode = this.timeline?.anchorStart(link) || 0;
+ this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = this.timeline?.anchorStart(link) || 0;
}
} else {
this.links
- .filter(l => l.anchor1 === link || l.anchor2 === link)
+ .filter(l => l.link_anchor_1 === link || l.link_anchor_2 === link)
.forEach(l => {
const { la1, la2 } = this.getLinkData(l);
const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2);
@@ -427,7 +435,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
this.playFrom(startTime, endTime);
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
+ this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = startTime;
}
}
});
@@ -437,9 +445,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
- setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+ setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
playing = () => this.mediaState === media_state.Playing;
@@ -539,7 +547,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}>
<FontAwesomeIcon size="2x" icon={this._paused ? 'play' : 'pause'} />
</div>
- <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div>
+ <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode)))}</div>
</div>
) : (
<div className="audiobox-start-record" onPointerDown={this.Record}>
@@ -615,7 +623,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{this.audio}
<div className="audiobox-timecodes">
- <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
+ <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
{this.miniPlayer ? (
<div>/</div>
) : (
@@ -626,15 +634,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
step="0.1"
min="1"
max="5"
- value={this.timeline?._zoomFactor}
+ 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: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))}
/>
</div>
)}
@@ -650,7 +654,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return (
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
- {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
+ {...this.props}
+ CollectionFreeFormDocumentView={undefined}
+ dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.path}
@@ -658,7 +664,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
startTag={'_timecodeToShow' /* audioStart */}
endTag={'_timecodeToHide' /* audioEnd */}
bringToFront={emptyFunction}
- CollectionView={undefined}
playFrom={this.playFrom}
setTime={this.setPlayheadTime}
playing={this.playing}
@@ -685,7 +690,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<audio
ref={this.setRef}
className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}
- onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '-duration'] = this._ele.duration))}>
+ onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}>
<source src={this.path} type="audio/mpeg" />
Not supported.
</audio>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 724394025..f99011b8f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -5,5 +5,5 @@
touch-action: manipulation;
top: 0;
left: 0;
- pointer-events: none;
-} \ No newline at end of file
+ //pointer-events: none;
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f8ef87fb1..6710cee63 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,30 +1,28 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
-import { InkField } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { numberRange } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { DocComponent } from '../DocComponent';
+import { InkingStroke } from '../InkingStroke';
import { StyleProp } from '../StyleProvider';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import React = require('react');
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+ dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
renderCutoffProvider: (doc: Doc) => boolean;
zIndex?: number;
- rotation: number;
dataTransition?: string;
replica: string;
CollectionFreeFormView: CollectionFreeFormView;
@@ -38,13 +36,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
{ key: 'x' },
{ key: 'y' },
{ key: '_rotation', val: 0 },
- { key: '_scrollTop' },
+ { key: '_layout_scrollTop' },
{ key: 'opacity', val: 1 },
- { key: 'viewScale', val: 1 },
- { key: 'panX' },
- { key: 'panY' },
+ { key: '_currentFrame' },
+ { key: 'freeform_scale', val: 1 },
+ { 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']; // 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
@observable _animPos: number[] | undefined = undefined;
@observable _contentView: DocumentView | undefined | null;
@@ -54,7 +54,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
get transform() {
- return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.rotation, this.props.rotation)}deg)`;
+ return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rot, this.Rot)}deg)`;
}
get X() {
return this.dataProvider?.x ?? NumCast(this.Document.x);
@@ -65,6 +65,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
get ZInd() {
return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
}
+ get Rot() {
+ return this.dataProvider?.rotation ?? NumCast(this.Document._rotation);
+ }
get Opacity() {
return this.dataProvider?.opacity;
}
@@ -82,13 +85,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
- if (doc === this.layoutDoc)
+ if (doc === this.layoutDoc) {
// prettier-ignore
switch (property) {
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;
}
+ }
return this.props.styleProvider?.(doc, props, property);
};
@@ -106,39 +110,22 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}, {} as { [val: string]: Opt<string> });
}
- public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
+ public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
- const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice();
- findexed[timecode] = vals[val] as any as number;
- d[`${val}-indexed`] = new List<number>(findexed);
+ const findexed = Cast(d[`${val}-indexed`], listSpec('string'), []).slice();
+ findexed[timecode] = vals[val] as any as string;
+ d[`${val}-indexed`] = new List<string>(findexed);
});
}
- public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) {
- if (timer) clearTimeout(timer);
- const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
+ public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
const timecode = Math.round(time);
- docs.forEach(doc => {
- CollectionFreeFormDocumentView.animFields.forEach(val => {
- const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
- });
- CollectionFreeFormDocumentView.animStringFields.forEach(val => {
- const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
- });
- CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
- const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any);
- });
+ Object.keys(vals).forEach(val => {
+ const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice();
+ findexed[timecode] = vals[val] as any as number;
+ d[`${val}-indexed`] = new List<number>(findexed);
});
- return newTimer;
- }
-
- public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) {
- if (timer) clearTimeout(timer);
- return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
}
public static setupZoom(doc: Doc, targDoc: Doc) {
@@ -165,17 +152,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
- CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
- const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
- doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
- targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
+ CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode)));
+ const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so
+ //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
+ targetDoc.activeFrame = ComputedField.MakeFunction('self.embedContainer?._currentFrame||0');
targetDoc.dataTransition = 'inherit';
});
}
@action public float = () => {
- const { Document: topDoc, ContainingCollectionView: container } = this.props;
- const screenXf = container?.screenToLocalTransform();
+ const topDoc = this.rootDoc;
+ const containerDocView = this.props.docViewPath().lastElement();
+ const screenXf = containerDocView?.screenToLocalTransform();
if (screenXf) {
SelectionManager.DeselectAll();
if (topDoc.z) {
@@ -192,7 +180,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
topDoc.x = fpt[0];
topDoc.y = fpt[1];
}
- setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0);
+ setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0);
}
};
@@ -203,7 +191,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.();
panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.();
screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y);
- focusDoc = (doc: Doc) => this.props.focus(doc, {});
returnThis = () => this;
render() {
TraceMobx();
@@ -215,9 +202,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
};
+ const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._stroke_isInkMask;
return (
<div
- className={'collectionFreeFormDocumentView-container'}
+ className="collectionFreeFormDocumentView-container"
style={{
width: this.panelWidth(),
height: this.panelHeight(),
@@ -225,7 +213,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transformOrigin: '50% 50%',
transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
zIndex: this.ZInd,
- display: this.ZInd === -99 ? 'none' : undefined,
+ display: this.sizeProvider?.width ? undefined : 'none',
+ pointerEvents: isInk ? 'none' : undefined,
}}>
{this.props.renderCutoffProvider(this.props.Document) ? (
<div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} />
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index c229a966a..330cc3971 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -48,10 +48,10 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
render() {
- const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]());
+ const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]());
return (
<div
- className={`colorBox-container${this.isContentActive() ? '-interactive' : ''}`}
+ className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`}
onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
onClick={e => e.stopPropagation()}
style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}>
@@ -72,7 +72,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
SetActiveInkWidth(e.target.value);
SelectionManager.Views()
.filter(i => StrCast(i.rootDoc.type) === DocumentType.INK)
- .map(i => (i.rootDoc.strokeWidth = Number(e.target.value)));
+ .map(i => (i.rootDoc.stroke_width = Number(e.target.value)));
}}
/>
</div>
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index dd03b9b99..1cc09a63c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,9 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
@@ -12,6 +13,7 @@ import { StyleProp } from '../StyleProvider';
import './ComparisonBox.scss';
import { DocumentView, DocumentViewProps } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
import React = require('react');
@observer
@@ -24,22 +26,32 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
@observable _animating = '';
+ @computed get clipWidth() {
+ return NumCast(this.layoutDoc[this.clipWidthKey], 50);
+ }
+ get clipWidthKey() {
+ return '_' + this.props.fieldKey + '_clipWidth';
+ }
+
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
this._disposers[disposerId]?.();
if (ele) {
// create disposers identified by disposerId to remove drag & drop listeners
- this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc);
+ this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
}
};
@undoBatch
- private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
+ private internalDrop = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
if (dropEvent.complete.docDragData) {
event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments;
- if (droppedDocs?.length) {
- this.dataDoc[fieldKey] = droppedDocs[0];
- }
+ droppedDocs.lastElement().embedContainer = this.dataDoc;
+ this.dataDoc[fieldKey] = droppedDocs.lastElement();
}
};
@@ -53,7 +65,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
action(() => {
// on click, animate slider movement to the targetWidth
this._animating = 'all 200ms';
- this.layoutDoc._clipWidth = (targetWidth * 100) / this.props.PanelWidth();
+ this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth();
setTimeout(
action(() => (this._animating = '')),
200
@@ -65,13 +77,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
@action
private onPointerMove = ({ movementX }: PointerEvent) => {
- const width = movementX * this.props.ScreenToLocalTransform().Scale + (NumCast(this.layoutDoc._clipWidth) / 100) * this.props.PanelWidth();
+ const width = movementX * this.props.ScreenToLocalTransform().Scale + (this.clipWidth / 100) * this.props.PanelWidth();
if (width && width > 5 && width < this.props.PanelWidth()) {
- this.layoutDoc._clipWidth = (width * 100) / this.props.PanelWidth();
+ this.layoutDoc[this.clipWidthKey] = (width * 100) / this.props.PanelWidth();
}
return false;
};
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ComparisonConfigDocument({
+ title: 'ImgAnchor:' + this.rootDoc.title,
+ // set presentation timing properties for restoring view
+ presTransition: 1000,
+ annotationOn: this.rootDoc,
+ });
+ if (anchor) {
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ /* addAsAnnotation &&*/ this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
+ };
+
@undoBatch
clearDoc = (e: React.MouseEvent, fieldKey: string) => {
e.stopPropagation; // prevent click event action (slider movement) in registerSliding
@@ -84,7 +112,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
render() {
- const clipWidth = NumCast(this.layoutDoc._clipWidth) + '%';
const clearButton = (which: string) => {
return (
<div
@@ -102,10 +129,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
return whichDoc ? (
<>
<DocumentView
- ref={r => {
- whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
- }}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
isContentActive={returnFalse}
isDocumentActive={returnFalse}
styleProvider={this.docStyleProvider}
@@ -124,7 +150,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
const displayBox = (which: string, index: number, cover: number) => {
return (
- <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
+ <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
{displayDoc(which)}
</div>
);
@@ -132,16 +158,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
return (
<div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
- {displayBox(this.fieldKey === 'data' ? 'compareBox-after' : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)}
- <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
- {displayBox(this.fieldKey === 'data' ? 'compareBox-before' : `${this.fieldKey}1`, 0, 0)}
+ {displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)}
+ <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
+ {displayBox(`${this.fieldKey}_1`, 0, 0)}
</div>
<div
className="slide-bar"
style={{
- left: `calc(${clipWidth} - 0.5px)`,
- cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? 'e-resize' : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined,
+ left: `calc(${this.clipWidth + '%'} - 0.5px)`,
+ cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined,
}}
onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
>
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
deleted file mode 100644
index df4c8f937..000000000
--- a/src/client/views/nodes/DataViz.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import './DataViz.scss';
-import { FieldView, FieldViewProps } from './FieldView';
-
-@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(DataVizBox, fieldKey);
- }
-
- render() {
- return (
- <div>
- <div>Hi</div>
- </div>
- );
- }
-}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..cd500e9ae 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -0,0 +1,4 @@
+.dataviz {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 592723ee9..baa45e278 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,90 +1,150 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { StrCast } from "../../../../fields/Types";
-import { ViewBoxBaseComponent } from "../../DocComponent";
-import { FieldViewProps, FieldView } from "../FieldView";
-import "./DataVizBox.scss";
-import { HistogramBox } from "./HistogramBox";
-import { TableBox } from "./TableBox";
-
-enum DataVizView {
- TABLE = "table",
- HISTOGRAM= "histogram"
-}
+import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { Cast, CsvCast, NumCast, StrCast } from '../../../../fields/Types';
+import { CsvField } from '../../../../fields/URLField';
+import { Docs } from '../../../documents/Documents';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import { LineChart } from './components/LineChart';
+import { TableBox } from './components/TableBox';
+import './DataVizBox.scss';
+export enum DataVizView {
+ TABLE = 'table',
+ LINECHART = 'lineChart',
+}
@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
-
- // TODO: nda - make this use enum values instead
- // @observable private currView: DataVizView = DataVizView.TABLE;
- @computed get currView() {
- if (this.rootDoc._dataVizView) {
- return StrCast(this.rootDoc._dataVizView);
- } else {
- return "table";
- }
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DataVizBox, fieldKey);
}
-
- constructor(props: any) {
- super(props);
- if (!this.rootDoc._dataVizView) {
- // TODO: nda - this might not always want to default to "table"
- this.rootDoc._dataVizView = "table";
- }
+ // says we have an object and any string
+ // 2 ways of doing it
+ // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
+ // @observable private pairs: { [key: string]: FieldResult }[] = [];
+ static pairSet = new ObservableMap<string, { [key: string]: string }[]>();
+ @computed.struct get pairs() {
+ return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href);
}
+ private _chartRenderer: LineChart | undefined;
+ // // another way would be store a schema that defines the type of data we are expecting from an imported doc
+
+ // method1() {
+ // this.pairs[0].x = 3;
+ // }
+
+ // method() {
+ // // this.pairs[0].x = 3;
+ // // go through the pairs
+ // const x = this.pairs[0].x;
+ // if (typeof x == 'number') {
+ // let x1 = Number(x);
+ // // let x1 = NumCast(x);
+ // }
+ // }
+
+ // could use field result
+ // [key: string]: FieldResult;
+ // instead of numeric x,y in there,
+
+ // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+ @computed get dataVizView(): DataVizView {
+ return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
+ }
@action
- private createPairs() {
- const xVals: number[] = [0, 1, 2, 3, 4, 5];
- // const yVals: number[] = [10, 20, 30, 40, 50, 60];
- const yVals: number[] = [1, 2, 3, 4, 5, 6];
- let pairs: {
- x: number,
- y:number
- }[] = [];
- if (xVals.length != yVals.length) return pairs;
- for (let i = 0; i < xVals.length; i++) {
- pairs.push({x: xVals[i], y: yVals[i]});
+ restoreView = (data: Doc) => {
+ const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView);
+ const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes)));
+ const func = () => this._chartRenderer?.restoreView(data);
+ if (changedView || changedAxes) {
+ setTimeout(func, 100);
+ return true;
}
- this.pairs = pairs;
- return pairs;
+ return func() ?? false;
+ };
+
+ getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
+ const anchor =
+ this._chartRenderer?.getAnchor(pinProps) ??
+ Docs.Create.DataVizConfigDocument({
+ // when we clear selection -> we should have it so chartBox getAnchor returns undefined
+ // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
+ /*put in some options*/
+ });
+
+ anchor.presDataVizView = this.dataVizView;
+ anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
+
+ this.addDocument(anchor);
+ return anchor;
+ };
+
+ @computed.struct get axes() {
+ return StrListCast(this.layoutDoc.data_vizAxes);
}
+ selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes));
@computed get selectView() {
- switch(this.currView) {
- case "table":
- return (<TableBox pairs={this.pairs} />)
- case "histogram":
- return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ const width = this.props.PanelWidth() * 0.9;
+ const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
+ const margin = { top: 10, right: 25, bottom: 50, left: 25 };
+ if (!this.pairs) return 'no data';
+ // prettier-ignore
+ switch (this.dataVizView) {
+ case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>;
+ case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
}
}
-
- @computed get pairVals() {
- return this.createPairs();
+ @computed get dataUrl() {
+ return Cast(this.dataDoc[this.fieldKey], CsvField);
}
componentDidMount() {
- this.createPairs();
+ this.props.setContentView?.(this);
+ this.fetchData();
+ }
+
+ fetchData() {
+ if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return;
+ DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []);
+ fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+ .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res))));
}
// handle changing the view using a button
@action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
- this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table";
+ this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
}
render() {
- return (
- <div className="dataViz">
- <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ return !this.pairs?.length ? (
+ <div>Loading...</div>
+ ) : (
+ <div
+ className="dataViz"
+ onWheel={e => e.stopPropagation()}
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // 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)
+ (e: WheelEvent) => {
+ if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ },
+ { passive: false }
+ )
+ }>
+ <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
{this.selectView}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts
deleted file mode 100644
index 595cecebf..000000000
--- a/src/client/views/nodes/DataVizBox/DrawHelper.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-export class PIXIPoint {
- public get x() { return this.coords[0]; }
- public get y() { return this.coords[1]; }
- public set x(value: number) { this.coords[0] = value; }
- public set y(value: number) { this.coords[1] = value; }
- public coords: number[] = [0, 0];
- constructor(x: number, y: number) {
- this.coords[0] = x;
- this.coords[1] = y;
- }
-}
-
-export class PIXIRectangle {
- public x: number;
- public y: number;
- public width: number;
- public height: number;
- public get left() { return this.x; }
- public get right() { return this.x + this.width; }
- public get top() { return this.y; }
- public get bottom() { return this.top + this.height; }
- public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
- constructor(x: number, y: number, width: number, height: number) {
- this.x = x;
- this.y = y;
- this.width = width;
- this.height = height;
- }
-}
-
-export class MathUtil {
-
- public static EPSILON: number = 0.001;
-
- public static Sign(value: number): number {
- return value >= 0 ? 1 : -1;
- }
-
- public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x += p2.x;
- p1.y += p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
- }
- }
-
- public static Perp(p1: PIXIPoint): PIXIPoint {
- return new PIXIPoint(-p1.y, p1.x);
- }
-
- public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x /= by;
- p1.y /= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x / by, p1.y / by);
- }
- }
-
- public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
- if (inline) {
- p1.x *= by;
- p1.y *= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x * by, p1.y * by);
- }
- }
-
- public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x -= p2.x;
- p1.y -= p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
- }
- }
-
- public static Area(rect: PIXIRectangle): number {
- return rect.width * rect.height;
- }
-
- public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
- // Return minimum distance between line segment vw and point p
- var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
- if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case
- // Consider the line extending the segment, parameterized as v + t (w - v).
- // We find projection of point p onto the line.
- // It falls where t = [(p-v) . (w-v)] / |w-v|^2
- // We clamp t from [0,1] to handle points outside the segment vw.
- var dot = MathUtil.Dot(
- MathUtil.SubtractPoint(p, v),
- MathUtil.SubtractPoint(w, v)) / l2;
- var t = Math.max(0, Math.min(1, dot));
- // Projection falls on the segment
- var projection = MathUtil.AddPoint(v,
- MathUtil.MultiplyConstant(
- MathUtil.SubtractPoint(w, v), t));
- return MathUtil.Dist(p, projection);
- }
-
- public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
- var a1 = pe1.y - ps1.y;
- var b1 = ps1.x - pe1.x;
-
- var a2 = pe2.y - ps2.y;
- var b2 = ps2.x - pe2.x;
-
- var delta = a1 * b2 - a2 * b1;
- if (delta === 0) {
- return undefined;
- }
- var c2 = a2 * ps2.x + b2 * ps2.y;
- var c1 = a1 * ps1.x + b1 * ps1.y;
- var invdelta = 1 / delta;
- return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
- }
-
- public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
- if (p.x < rect.left - this.EPSILON) {
- return false;
- }
- if (p.x > rect.right + this.EPSILON) {
- return false;
- }
- if (p.y < rect.top - this.EPSILON) {
- return false;
- }
- if (p.y > rect.bottom + this.EPSILON) {
- return false;
- }
-
- return true;
- }
-
- public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
- var r1 = new PIXIPoint(rect.left, rect.top);
- var r2 = new PIXIPoint(rect.right, rect.top);
- var r3 = new PIXIPoint(rect.right, rect.bottom);
- var r4 = new PIXIPoint(rect.left, rect.bottom);
- var ret = new Array<PIXIPoint>();
- var dist = this.Dist(lineFrom, lineTo);
- var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- return ret;
- }
-
- public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
- const left = Math.max(rect1.x, rect2.x);
- const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
- const top = Math.max(rect1.y, rect2.y);
- const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
- return new PIXIRectangle(left, top, right - left, bottom - top);
- }
-
- public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
- return Math.sqrt(MathUtil.DistSquared(p1, p2));
- }
-
- public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
- return p1.x * p2.x + p1.y * p2.y;
- }
-
- public static Normalize(p1: PIXIPoint) {
- var d = this.Length(p1);
- return new PIXIPoint(p1.x / d, p1.y / d);
- }
-
- public static Length(p1: PIXIPoint): number {
- return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
- }
-
- public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
- const a = p1.x - p2.x;
- const b = p1.y - p2.y;
- return (a * a + b * b);
- }
-
- public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
- return !(r2.x > r1.x + r1.width ||
- r2.x + r2.width < r1.x ||
- r2.y > r1.y + r1.height ||
- r2.y + r2.height < r1.y);
- }
-
- public static ArgMin(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] < value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static ArgMax(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] > value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static Combinations<T>(chars: T[]) {
- let result = new Array<T>();
- let f = (prefix: any, chars: any) => {
- for (let i = 0; i < chars.length; i++) {
- result.push(prefix.concat(chars[i]));
- f(prefix.concat(chars[i]), chars.slice(i + 1));
- }
- };
- f([], chars);
- return result;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss
deleted file mode 100644
index 5aac9dc77..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// change the stroke color of line-svg class
-.svgLine {
- position: absolute;
- background: darkGray;
- stroke: #000;
- stroke-width: 1px;
- width:100%;
- height:100%;
- opacity: 0.4;
-}
-
-.svgContainer {
- position: absolute;
- top:0;
- left:0;
- width:100%;
- height: 100%;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
deleted file mode 100644
index 00dc2ef46..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc } from "../../../../fields/Doc";
-import { NumCast } from "../../../../fields/Types";
-import "./HistogramBox.scss";
-
-interface HistogramBoxProps {
- rootDoc: Doc;
- pairs: {
- x: number,
- y: number
- }[]
-}
-
-
-export class HistogramBox extends React.Component<HistogramBoxProps> {
-
- private origin = {x: 0.1 * this.width, y: 0.9 * this.height};
-
- @computed get width() {
- return NumCast(this.props.rootDoc.width);
- }
-
- @computed get height() {
- return NumCast(this.props.rootDoc.height);
- }
-
- @computed get x() {
- return NumCast(this.props.rootDoc.x);
- }
-
- @computed get y() {
- return NumCast(this.props.rootDoc.y);
- }
-
- @computed get generatePoints() {
- // evenly distribute points along the x axis
- const xVals: number[] = this.props.pairs.map(p => p.x);
- const yVals: number[] = this.props.pairs.map(p => p.y);
-
- const xMin = Math.min(...xVals);
- const xMax = Math.max(...xVals);
- const yMin = Math.min(...yVals);
- const yMax = Math.max(...yVals);
-
- const xRange = xMax - xMin;
- const yRange = yMax - yMin;
-
- const xScale = this.width / xRange;
- const yScale = this.height / yRange;
-
- const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale;
- const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale;
-
- const points: {
- x: number,
- y: number
- }[] = this.props.pairs.map(p => {
- return {
- x: (p.x * xScale + xOffset) + this.origin.x,
- y: (p.y * yScale + yOffset)
- }
- });
-
- return points;
- }
-
- @computed get generateGraphLine() {
- const points = this.generatePoints;
- // loop through points and create a line from each point to the next
- let lines: {
- x1: number,
- y1: number,
- x2: number,
- y2: number
- }[] = [];
- for (let i = 0; i < points.length - 1; i++) {
- lines.push({
- x1: points[i].x,
- y1: points[i].y,
- x2: points[i + 1].x,
- y2: points[i + 1].y
- });
- }
- // generate array of svg with lines
- let svgLines: JSX.Element[] = [];
- for (let i = 0; i < lines.length; i++) {
- svgLines.push(
- <line
- className="svgLine"
- key={i}
- x1={lines[i].x1}
- y1={lines[i].y1}
- x2={lines[i].x2}
- y2={lines[i].y2}
- stroke="black"
- strokeWidth={2}
- />
- );
- }
-
- let res = [];
- for (let i = 0; i < svgLines.length; i++) {
- res.push(<svg className="svgContainer">{svgLines[i]}</svg>)
- }
- return res;
- }
-
- @computed get generateAxes() {
-
- const xAxis = {
- x1: 0.1 * this.width,
- x2: 0.9 * this.width,
- y1: 0.9 * this.height,
- y2: 0.9 * this.height,
- };
-
- const yAxis = {
- x1: 0.1 * this.width,
- x2: 0.1 * this.width,
- y1: 0.25 * this.height,
- y2: 0.9 * this.height,
- };
-
-
- return (
- [
- (<svg className="svgContainer">
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */}
- <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/>
-
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>),
- (
- <svg className="svgContainer">
- <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} />
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>)
- ]
- )
- }
-
-
- render() {
- return (
- <div>histogram box
- {/* <svg className="svgContainer">
- {this.generateSVGLine}
- </svg> */}
- {this.generateAxes[0]}
- {this.generateAxes[1]}
- {this.generateGraphLine.map(line => line)}
- </div>
- )
-
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
deleted file mode 100644
index dfa8262d8..000000000
--- a/src/client/views/nodes/DataVizBox/TableBox.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-
-interface TableBoxProps {
- pairs: {x: number, y:number}[]
-}
-
-
-export class TableBox extends React.Component<TableBoxProps> {
-
-
-
- render() {
- return (
- <div className="table-container">
- <table className="table">
- <thead>
- <tr className="table-row">
- <th>x</th>
- <th>y</th>
- </tr>
- </thead>
- <tbody>
- {this.props.pairs.map(p => {
- return (<tr className="table-row">
- <td>{p.x}</td>
- <td>{p.y}</td>
- </tr>)
- })}
- </tbody>
- </table>
- </div>
- )
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
new file mode 100644
index 000000000..d4f7bfb32
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -0,0 +1,41 @@
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+
+ .tooltip {
+ // make the height width bigger
+ width: fit-content;
+ height: fit-content;
+ }
+
+ .hoverHighlight-selected,
+ .selected {
+ // change the color of the circle element to be red
+ fill: transparent;
+ outline: red solid 2px;
+ border-radius: 100%;
+ position: absolute;
+ transform-box: fill-box;
+ transform-origin: center;
+ }
+ .hoverHighlight {
+ fill: transparent;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .hoverHighlight-selected {
+ fill: transparent;
+ scale: 1;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .datapoint {
+ fill: black;
+ }
+ .brushed {
+ // change the color of the circle element to be red
+ fill: red;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
new file mode 100644
index 000000000..661061d51
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -0,0 +1,322 @@
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+// import d3
+import * as d3 from 'd3';
+import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { LinkManager } from '../../../../util/LinkManager';
+import { PinProps, PresBox } from '../../trails';
+import { DataVizBox } from '../DataVizBox';
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
+import './Chart.scss';
+
+export interface DataPoint {
+ x: number;
+ y: number;
+}
+interface SelectedDataPoint extends DataPoint {
+ elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
+}
+export interface LineChartProps {
+ rootDoc: Doc;
+ axes: string[];
+ pairs: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class LineChart extends React.Component<LineChartProps> {
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ @observable _currSelected: SelectedDataPoint | undefined = undefined;
+ // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+
+ @computed get _lineChartData() {
+ if (this.props.axes.length <= 1) return [];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))
+ .sort((a, b) => (a.x < b.x ? -1 : 1));
+ }
+ @computed get incomingLinks() {
+ return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ .filter(link => link.link_anchor_1 !== this.props.rootDoc) // get links where this chart doc is the target of the link
+ .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+ @computed get incomingSelected() {
+ return this.incomingLinks // all links that are pointing to this node
+ .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes
+ .filter(dvb => dvb)
+ .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor
+ .lastElement();
+ }
+ @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
+ return minMaxRange([this._lineChartData]);
+ }
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet) {
+ this.drawChart([dataSet], this.rangeVals, w, h);
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true);
+ this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.annos = reaction(
+ () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']),
+ annotations => {
+ // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
+ // could be blue colored to make it look like anchor
+ // this.drawAnnotations()
+ // loop through annotations and draw them
+ annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
+ // this.drawAnnotations(annotations.x, annotations.y);
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.highlights = reaction(
+ () => ({
+ selected: this._currSelected,
+ incomingSelected: this.incomingSelected,
+ }),
+ ({ selected, incomingSelected }) => {
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
+ incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that
+
+ clearAnnotations = () => {
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ element.classList.remove('brushed');
+ element.classList.remove('selected');
+ }
+ };
+ // gets called whenever the "data_annotations" fields gets updated
+ drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => {
+ // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
+ // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY
+ // if it exists, then highlight it
+ // if it doesn't exist, then remove the highlight
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ const x = element.getAttribute('data-x');
+ const y = element.getAttribute('data-y');
+ if (x === dataX.toString() && y === dataY.toString()) {
+ element.classList.add(selected ? 'selected' : 'brushed');
+ }
+ // TODO: nda - this remove highlight code should go where we remove the links
+ // } else {
+ // }
+ }
+ };
+
+ removeAnnotations(dataX: number, dataY: number) {
+ // loop through and remove any annotations that no longer exist
+ }
+
+ @action
+ restoreView = (data: Doc) => {
+ const coords = Cast(data.presDataVizSelection, listSpec('number'), null);
+ if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) {
+ this.setCurrSelected(coords[0], coords[1]);
+ return true;
+ }
+ if (this._currSelected) {
+ this.setCurrSelected();
+ return true;
+ }
+ return false;
+ };
+
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.LineChartConfigDocument({
+ //
+ title: 'line doc selection' + this._currSelected?.x,
+ });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
+ anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
+ return anchor;
+ };
+
+ @computed get height() {
+ return this.props.height - this.props.margin.top - this.props.margin.bottom;
+ }
+
+ @computed get width() {
+ return this.props.width - this.props.margin.left - this.props.margin.right;
+ }
+
+ setupTooltip() {
+ return d3
+ .select(this._lineChartRef.current)
+ .append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0)
+ .style('background', '#fff')
+ .style('border', '1px solid #ccc')
+ .style('padding', '5px')
+ .style('position', 'absolute')
+ .style('font-size', '12px');
+ }
+
+ // TODO: nda - use this everyewhere we update currSelected?
+ @action
+ setCurrSelected(x?: number, y?: number) {
+ // TODO: nda - get rid of svg element in the list?
+ this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined;
+ this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true));
+ this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined));
+ }
+
+ drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
+ if (this._lineChartSvg) {
+ const circleClass = '.circle-' + idx;
+ this._lineChartSvg
+ .selectAll(circleClass)
+ .data(data)
+ .join('circle') // enter append
+ .attr('class', `${circleClass} datapoint`)
+ .attr('r', '3') // radius
+ .attr('cx', d => xScale(d.x))
+ .attr('cy', d => yScale(d.y))
+ .attr('data-x', d => d.x)
+ .attr('data-y', d => d.y);
+ }
+ }
+
+ // TODO: nda - can use d3.create() to create html element instead of appending
+ drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ // clearing tooltip and the current chart
+ d3.select(this._lineChartRef.current).select('svg').remove();
+ d3.select(this._lineChartRef.current).select('.tooltip').remove();
+
+ const { xMin, xMax, yMin, yMax } = rangeVals;
+ if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
+ return;
+ }
+
+ // creating the x and y scales
+ const xScale = scaleCreatorNumerical(xMin, xMax, 0, width);
+ const yScale = scaleCreatorNumerical(0, yMax, height, 0);
+
+ // adding svg
+ const margin = this.props.margin;
+ const svg = (this._lineChartSvg = d3
+ .select(this._lineChartRef.current)
+ .append('svg')
+ .attr('width', `${width + margin.left + margin.right}`)
+ .attr('height', `${height + margin.top + margin.bottom}`)
+ .append('g')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`));
+
+ // create x and y grids
+ xGrid(svg.append('g'), height, xScale);
+ yGrid(svg.append('g'), width, yScale);
+ xAxisCreator(svg.append('g'), height, xScale);
+ yAxisCreator(svg.append('g'), width, yScale);
+
+ // draw the plot line
+ const data = dataSet[0];
+ const lineGen = createLineGenerator(xScale, yScale);
+ drawLine(svg.append('path'), data, lineGen);
+
+ // draw the datapoint circle
+ this.drawDataPoints(data, 0, xScale, yScale);
+
+ const higlightFocusPt = svg.append('g').style('display', 'none');
+ higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
+ const tooltip = this.setupTooltip();
+ // add all the tooltipContent to the tooltip
+ const mousemove = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ if (!d0) return;
+
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ const onPointClick = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ // find .circle-d1 with data-x = d0.x and data-y = d0.y
+ const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
+ this.setCurrSelected(d0.x, d0.y);
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ svg.append('rect')
+ .attr('class', 'overlay')
+ .attr('width', width)
+ .attr('height', this.height + margin.top + margin.bottom)
+ .attr('fill', 'none')
+ .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
+ .style('opacity', 0)
+ .on('mouseover', () => higlightFocusPt.style('display', null))
+ .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
+ .on('mousemove', mousemove)
+ .on('click', onPointClick);
+ };
+
+ private updateTooltip(
+ higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+ xScale: d3.ScaleLinear<number, number, never>,
+ d0: DataPoint,
+ yScale: d3.ScaleLinear<number, number, never>,
+ tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+ ) {
+ higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'hoverHighlight-selected' : 'hoverHighlight');
+ tooltip.transition().duration(300).style('opacity', 0.9);
+ // TODO: nda - updating the inner html could be deadly cause injection attacks!
+ tooltip
+ .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip
+ .style('pointer-events', 'none')
+ .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`);
+ }
+
+ render() {
+ const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none';
+ return (
+ <div ref={this._lineChartRef} className="chart-container">
+ <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss
index 1264d6a46..1264d6a46 100644
--- a/src/client/views/nodes/DataVizBox/TableBox.scss
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.scss
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
new file mode 100644
index 000000000..3816bddfa
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -0,0 +1,105 @@
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AnimationSym, Doc } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
+import { DragManager } from '../../../../util/DragManager';
+import { DocumentView } from '../../DocumentView';
+import { DataVizView } from '../DataVizBox';
+
+interface TableBoxProps {
+ pairs: { [key: string]: any }[];
+ selectAxes: (axes: string[]) => void;
+ axes: string[];
+ docView?: () => DocumentView | undefined;
+}
+
+@observer
+export class TableBox extends React.Component<TableBoxProps> {
+ @computed get columns() {
+ return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : [];
+ }
+ render() {
+ return (
+ <div className="table-container">
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ {this.columns
+ .filter(col => !col.startsWith('select'))
+ .map(col => {
+ const header = React.createRef<HTMLElement>();
+ return (
+ <th
+ ref={header as any}
+ style={{
+ color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined,
+ fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal',
+ }}
+ onPointerDown={e => {
+ const downX = e.clientX;
+ const downY = e.clientY;
+ setupMoveUpEvents(
+ {},
+ e,
+ e => {
+ const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!);
+ embedding._dataVizView = DataVizView.LINECHART;
+ embedding._data_vizAxes = new List<string>([col, col]);
+ embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!;
+ return embedding;
+ };
+ if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+ DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.linkDocument.layout_linkDisplay = true;
+ // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ });
+ return true;
+ }
+ return false;
+ },
+ emptyFunction,
+ action(e => {
+ const newAxes = this.props.axes;
+ if (newAxes.includes(col)) {
+ newAxes.splice(newAxes.indexOf(col), 1);
+ } else if (newAxes.length >= 1) {
+ newAxes[1] = col;
+ } else {
+ newAxes[0] = col;
+ }
+ this.props.selectAxes(newAxes);
+ })
+ );
+ }}>
+ {col}
+ </th>
+ );
+ })}
+ </tr>
+ </thead>
+ <tbody>
+ {this.props.pairs?.map((p, i) => {
+ return (
+ <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}>
+ {this.columns.map(col => (
+ <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td>
+ ))}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
new file mode 100644
index 000000000..e1ff6f8eb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -0,0 +1,67 @@
+import * as d3 from 'd3';
+import { DataPoint } from '../components/LineChart';
+
+// TODO: nda - implement function that can handle range for strings
+
+export const minMaxRange = (dataPts: DataPoint[][]) => {
+ // find the max and min of all the data points
+ const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y)));
+ const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y)));
+
+ const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x)));
+ const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x)));
+
+ return { xMin, xMax, yMin, yMax };
+};
+
+export const scaleCreatorCategorical = (labels: string[], range: number[]) => {
+ const scale = d3.scaleBand().domain(labels).range(range);
+
+ return scale;
+};
+
+export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => {
+ return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]);
+};
+
+export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => {
+ // TODO: nda - look into the different types of curves
+ return d3
+ .line<DataPoint>()
+ .x(d => xScale(d.x))
+ .y(d => yScale(d.y))
+ .curve(d3.curveMonotoneX);
+};
+
+export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => {
+ console.log('x axis creator being called');
+ g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15));
+};
+
+export const yAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, marginLeft: number, yScale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'y-axis').call(d3.axisLeft(yScale));
+};
+
+export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'xGrid')
+ .attr('transform', `translate(0,${height})`)
+ .call(
+ d3
+ .axisBottom(scale)
+ .tickSize(-height)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, width: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'yGrid').call(
+ d3
+ .axisLeft(scale)
+ .tickSize(-width)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => {
+ p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen);
+};
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 5d72ca019..05a3b56f7 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,4 @@
-import { computed, trace } from 'mobx';
+import { computed } from 'mobx';
import { observer } from 'mobx-react';
import { AclPrivate, Doc, Opt } from '../../../fields/Doc';
import { ScriptField } from '../../../fields/ScriptField';
@@ -9,6 +9,7 @@ import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBo
import { CollectionDockingView } from '../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
+import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
import { CollectionView } from '../collections/CollectionView';
import { InkingStroke } from '../InkingStroke';
import { PresElementBox } from '../nodes/trails/PresElementBox';
@@ -24,7 +25,6 @@ import { DocumentViewProps } from './DocumentView';
import './DocumentView.scss';
import { EquationBox } from './EquationBox';
import { FieldView, FieldViewProps } from './FieldView';
-import { FilterBox } from './FilterBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { FunctionPlotBox } from './FunctionPlotBox';
import { ImageBox } from './ImageBox';
@@ -32,9 +32,10 @@ import { KeyValueBox } from './KeyValueBox';
import { LabelBox } from './LabelBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { LinkBox } from './LinkBox';
+import { LoadingBox } from './LoadingBox';
import { MapBox } from './MapBox/MapBox';
import { PDFBox } from './PDFBox';
-import PhysicsSimulationBox from './PhysicsBox/PhysicsSimulationBox'
+import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox';
import { RecordingBox } from './RecordingBox';
import { ScreenshotBox } from './ScreenshotBox';
import { ScriptingBox } from './ScriptingBox';
@@ -44,7 +45,6 @@ import { VideoBox } from './VideoBox';
import { WebBox } from './WebBox';
import React = require('react');
import XRegExp = require('xregexp');
-import { LoadingBox } from './LoadingBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -121,16 +121,16 @@ export class DocumentContentsView extends React.Component<
select: (ctrl: boolean) => void;
NativeDimScaling?: () => number;
setHeight?: (height: number) => void;
- layoutKey: string;
+ layout_fieldKey: string;
}
> {
@computed get layout(): string {
TraceMobx();
if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
if (!this.layoutDoc) return '<p>awaiting layout</p>';
- if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString('data'));
- const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, 'layout')], 'string');
- if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(this.layoutDoc.proto ? 'proto' : '');
+ if (this.props.layout_fieldKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString());
+ const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layout_fieldKey ? this.props.layout_fieldKey : StrCast(this.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>';
}
@@ -140,15 +140,14 @@ export class DocumentContentsView extends React.Component<
return proto instanceof Promise ? undefined : proto;
}
get layoutDoc() {
- const params = StrCast(this.props.Document.PARAMS);
// 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.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined);
const template: Doc =
this.props.LayoutTemplate?.() ||
(this.props.LayoutTemplateString && this.props.Document) ||
- (this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) ||
- Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
- return Doc.expandTemplateLayout(template, this.props.Document, params ? '(' + params + ')' : this.props.layoutKey);
+ (this.props.layout_fieldKey && StrCast(this.props.Document[this.props.layout_fieldKey]) && this.props.Document) ||
+ Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined);
+ return Doc.expandTemplateLayout(template, this.props.Document);
}
CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
@@ -156,7 +155,6 @@ export class DocumentContentsView extends React.Component<
// these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews
'hideResizeHandles',
'hideTitle',
- 'treeViewDoc',
'contentPointerEvents',
'radialMenu',
'LayoutTemplateString',
@@ -255,7 +253,6 @@ export class DocumentContentsView extends React.Component<
YoutubeBox,
PresElementBox,
SearchBox,
- FilterBox,
FunctionPlotBox,
ColorBox,
DashWebRTCVideo,
@@ -270,12 +267,13 @@ export class DocumentContentsView extends React.Component<
ComparisonBox,
LoadingBox,
PhysicsSimulationBox,
+ SchemaRowBox,
}}
bindings={bindings}
jsx={layoutFrame}
showWarnings={true}
onError={(test: any) => {
- console.log('DocumentContentsView:' + test);
+ console.log('DocumentContentsView:' + test, bindings, layoutFrame);
}}
/>
);
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 56de2d1fc..6e2ed72b8 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -1,23 +1,39 @@
-
-import { observer } from "mobx-react";
-import * as React from "react";
-import { DocumentView } from "./DocumentView";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Transformer, ts } from "../../util/Scripting";
-import { Field } from "../../../fields/Doc";
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { DocumentView } from './DocumentView';
+import { DocumentManager } from '../../util/DocumentManager';
+import { Transformer, ts } from '../../util/Scripting';
+import { Field } from '../../../fields/Doc';
+import { Tooltip } from '@material-ui/core';
+import { action, observable } from 'mobx';
+import { Id } from '../../../fields/FieldSymbols';
+import { factory } from 'typescript';
+import { LightboxView } from '../LightboxView';
@observer
-export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
+export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> {
+ @observable _hovered = false;
+ static get DocViews() {
+ return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews;
+ }
render() {
const view = this.props.view;
const { left, top, right, bottom } = view.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 };
return (
- <div className="documentIcon-outerDiv" style={{
- position: "absolute",
- transform: `translate(${(left + right) / 2}px, ${top}px)`,
- }}>
- <p>d{this.props.index}</p>
+ <div
+ className="documentIcon-outerDiv"
+ onPointerEnter={action(e => (this._hovered = true))}
+ onPointerLeave={action(e => (this._hovered = false))}
+ style={{
+ pointerEvents: 'all',
+ opacity: this._hovered ? 0.3 : 1,
+ position: 'absolute',
+ transform: `translate(${(left + right) / 2}px, ${top}px)`,
+ }}>
+ <Tooltip title={<>{this.props.view.rootDoc.title}</>}>
+ <p>d{this.props.index}</p>
+ </Tooltip>
</div>
);
}
@@ -41,7 +57,9 @@ export class DocumentIconContainer extends React.Component {
const match = node.text.match(/d([0-9]+)/);
if (match) {
const m = parseInt(match[1]);
+ const doc = DocumentIcon.DocViews[m].rootDoc;
usedDocuments.add(m);
+ return factory.createIdentifier(`idToDoc("${doc[Id]}")`);
}
}
}
@@ -52,14 +70,14 @@ export class DocumentIconContainer extends React.Component {
};
},
getVars() {
- const docs = Array.from(DocumentManager.Instance.DocumentViews);
+ const docs = DocumentIcon.DocViews;
const capturedVariables: { [name: string]: Field } = {};
- usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document);
- return { capturedVariables };
- }
+ usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`));
+ return capturedVariables;
+ },
};
}
render() {
- return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+ return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index a39e0f65f..bd1952ecb 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -9,13 +9,14 @@ import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { Hypothesis } from '../../util/HypothesisUtils';
import { LinkManager } from '../../util/LinkManager';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import './DocumentLinksButton.scss';
import { DocumentView } from './DocumentView';
import { LinkDescriptionPopup } from './LinkDescriptionPopup';
import { TaskCompletionBox } from './TaskCompletedBox';
import React = require('react');
import _ = require('lodash');
+import { PinProps } from './trails';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
@@ -51,7 +52,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
dragComplete: dropEv => {
if (this.props.View && dropEv.linkDocument) {
// dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
- !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = 'hyperlink');
+ !dropEv.linkDocument.link_relationship && (Doc.GetProto(dropEv.linkDocument).link_relationship = 'hyperlink');
}
linkDrag?.end();
},
@@ -70,18 +71,13 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
this.onLinkButtonMoved,
emptyFunction,
- action((e, doubleTap) => {
- if (doubleTap) {
- DocumentView.showBackLinks(this.props.View.rootDoc);
- }
- }),
+ action((e, doubleTap) => doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc)),
undefined,
undefined,
action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View))
);
};
- @undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(
this,
@@ -126,66 +122,23 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
returnFalse,
emptyFunction,
- undoBatch(
- action((e, doubleTap) => {
- if (doubleTap && !this.props.StartLink) {
- if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
- DocumentLinksButton.StartLink = undefined;
- DocumentLinksButton.StartLinkView = undefined;
- DocumentLinksButton.AnnotationId = undefined;
- } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) {
- const sourceDoc = DocumentLinksButton.StartLink;
- const targetDoc = this.props.View.ComponentView?.getAnchor?.(true) || this.props.View.Document;
- const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, 'links'); //why is long drag here when this is used for completing links by clicking?
-
- LinkManager.currentLink = linkDoc;
-
- runInAction(() => {
- if (linkDoc) {
- TaskCompletionBox.textDisplayed = 'Link Created';
- TaskCompletionBox.popupX = e.screenX;
- TaskCompletionBox.popupY = e.screenY - 133;
- TaskCompletionBox.taskCompleted = true;
-
- LinkDescriptionPopup.popupX = e.screenX;
- LinkDescriptionPopup.popupY = e.screenY - 100;
- LinkDescriptionPopup.descriptionPopup = true;
-
- const rect = document.body.getBoundingClientRect();
- if (LinkDescriptionPopup.popupX + 200 > rect.width) {
- LinkDescriptionPopup.popupX -= 190;
- TaskCompletionBox.popupX -= 40;
- }
- if (LinkDescriptionPopup.popupY + 100 > rect.height) {
- LinkDescriptionPopup.popupY -= 40;
- TaskCompletionBox.popupY -= 40;
- }
-
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2500
- );
- }
- });
- }
- }
- })
- )
+ action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View))
);
};
- public static finishLinkClick = undoBatch(
- action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView) => {
- if (startLink === endLink) {
+ @undoBatch
+ public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) {
+ runInAction(() => {
+ if (startLink === endLink || !startLink) {
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
DocumentLinksButton.AnnotationId = undefined;
DocumentLinksButton.AnnotationUri = undefined;
//!this.props.StartLink
} else if (startLink !== endLink) {
- endLink = endLinkView?.docView?._componentView?.getAnchor?.(true) || endLink;
+ endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
- const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
+ const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined });
LinkManager.currentLink = linkDoc;
@@ -228,8 +181,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
);
}
}
- })
- );
+ });
+ }
@action clearLinks() {
DocumentLinksButton.StartLink = undefined;
@@ -240,7 +193,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
const results = [] as Doc[];
const filters = this.props.View.props.docFilters();
Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => {
- if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) {
+ if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) {
results.push(link);
}
});
@@ -279,11 +232,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
) : null}
{!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node
- <div
- className={'documentLinksButton-endLink'}
- ref={this._linkButton}
- onPointerDown={DocumentLinksButton.StartLink && this.completeLink}
- onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
+ <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
) : null}
@@ -305,7 +254,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
top: 0,
pointerEvents: 'none',
}}>
- <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
+ {DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>}
</div>
);
}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 453bdac8e..1265651ad 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -17,8 +17,7 @@
top: 0;
}
-.documentView-node,
-.documentView-node-topmost {
+.documentView-node {
position: inherit;
top: 0;
left: 0;
@@ -162,6 +161,7 @@
width: 100%;
height: 100%;
border-radius: inherit;
+ white-space: normal;
.documentView-styleContentWrapper {
width: 100%;
@@ -208,8 +208,7 @@
}
}
-.documentView-node:hover,
-.documentView-node-topmost:hover {
+.documentView-node:hover {
> .documentView-styleWrapper {
> .documentView-titleWrapper-hover {
display: inline-block;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b94db2c6b..cc105326f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,23 +1,19 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { ObserverJsxParser } from './DocumentContentsView';
+import { computedFn } from 'mobx-utils';
import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
-import { Document } from '../../../fields/documentSchemas';
+import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
-import { ObjectField } from '../../../fields/ObjectField';
+import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
-import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
-import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from '../../documents/Documents';
@@ -27,34 +23,32 @@ import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
-import { LinkFollower } from '../../util/LinkFollower';
+import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { CollectionView } from '../collections/CollectionView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
+import { GestureOverlay } from '../GestureOverlay';
+import { InkingStroke } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
+import { UndoStack } from '../UndoStack';
import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
-import { DocumentContentsView } from './DocumentContentsView';
+import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
import { FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { LinkAnchorBox } from './LinkAnchorBox';
-import { LinkDocPreview } from './LinkDocPreview';
-import { RadialMenu } from './RadialMenu';
-import { ScriptingBox } from './ScriptingBox';
import { PresEffect, PresEffectDirection } from './trails';
-import { PinProps } from './trails/PresBox';
+import { PinProps, PresBox } from './trails/PresBox';
import React = require('react');
const { Howl } = require('howler');
@@ -67,13 +61,7 @@ declare class MediaRecorder {
constructor(e: any);
}
-export enum ViewAdjustment {
- resetView = 1,
- doNothing = 0,
-}
-
export enum OpenWhere {
- inPlace = 'inPlace',
lightbox = 'lightbox',
add = 'add',
addLeft = 'add:left',
@@ -83,11 +71,13 @@ export enum OpenWhere {
close = 'close',
fullScreen = 'fullScreen',
toggle = 'toggle',
+ toggleRight = 'toggle:right',
replace = 'replace',
replaceRight = 'replace:right',
replaceLeft = 'replace:left',
inParent = 'inParent',
inParentFromScreen = 'inParentFromScreen',
+ overlay = 'overlay',
}
export enum OpenWhereMod {
none = '',
@@ -95,51 +85,59 @@ export enum OpenWhereMod {
right = 'right',
top = 'top',
bottom = 'bottom',
+ rightKeyValue = 'rightKeyValue',
}
-export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
-
export interface DocFocusOptions {
- originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
willPan?: boolean; // determines whether to pan to target document
- willPanZoom?: boolean; // determines whether to zoom in on target document
+ willZoomCentered?: boolean; // determines whether to zoom in on target document. if zoomScale is 0, this just centers the document
zoomScale?: number; // percent of containing frame to zoom into document
zoomTime?: number;
- afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
+ didMove?: boolean; // whether a document was changed during the showDocument process
docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+ preview?: boolean; // whether changes should be previewed by the componentView or written to the document
effect?: Doc; // animation effect for focus
noSelect?: boolean; // whether target should be selected after focusing
playAudio?: boolean; // whether to play audio annotation on focus
+ openLocation?: OpenWhere; // where to open a missing document
zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
toggleTarget?: boolean; // whether to toggle target on and off
- originatingDoc?: Doc; // document that triggered the focus
+ anchorDoc?: Doc; // doc containing anchor info to apply at end of focus to target doc
easeFunc?: 'linear' | 'ease'; // transition method for scrolling
}
-export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
-export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
+export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>;
export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
export interface DocComponentView {
updateIcon?: () => void; // updates the icon representation of the document
- getAnchor?: (addAsAnnotation: boolean) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
- scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
+ getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+ restoreView?: (viewSpec: Doc) => boolean;
+ scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
- setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+ getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
+ addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
+ addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
+ select?: (ctrlKey: boolean, shiftKey: boolean) => void;
menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
isAnyChildContentActive?: () => boolean; // is any child content of the document active
+ onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
playFrom?: (time: number, endTime?: number) => void;
- Pause?: () => void;
- setFocus?: () => void;
+ Pause?: () => void; // pause a media document (eg, audio/video)
+ IsPlaying?: () => boolean; // is a media document playing
+ TogglePause?: (keep?: boolean) => void; // toggle media document playing state
+ setFocus?: () => void; // sets input focus to the componentView
+ setData?: (data: Field | Promise<RefField | undefined>) => boolean;
componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
incrementalRendering?: () => void;
+ layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox)
+ overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document
fieldKey?: string;
annotationKey?: string;
getTitle?: () => string;
- getScrollHeight?: () => number;
getCenter?: (xf: Transform) => { X: number; Y: number };
ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
@@ -154,12 +152,9 @@ export interface DocumentViewSharedProps {
Document: Doc;
DataDoc?: Doc;
contentBounds?: () => undefined | { x: number; y: number; r: number; b: number };
- fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document
- ContainingCollectionView: Opt<CollectionView>;
- ContainingCollectionDoc: Opt<Doc>;
+ fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
suppressSetHeight?: boolean;
thumbShown?: () => boolean;
- isHovering?: () => boolean;
setContentView?: (view: DocComponentView) => any;
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
PanelWidth: () => number;
@@ -169,23 +164,25 @@ export interface DocumentViewSharedProps {
childHideResizeHandles?: () => boolean;
dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
styleProvider: Opt<StyleProviderFunc>;
+ setTitleFocus?: () => void;
focus: DocFocusFunc;
- fitWidth?: (doc: Doc) => boolean;
+ layout_fitWidth?: (doc: Doc) => boolean | undefined;
docFilters: () => string[];
docRangeFilters: () => string[];
searchFilterDocs: () => Doc[];
- showTitle?: () => string;
+ layout_showTitle?: () => string;
whenChildContentsActiveChanged: (isActive: boolean) => void;
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
addDocTab: (doc: Doc, where: OpenWhere) => boolean;
filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
- addDocument?: (doc: Doc | Doc[]) => boolean;
- removeDocument?: (doc: Doc | Doc[]) => boolean;
- moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+ addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
+ removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
+ moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => boolean;
pinToPres: (document: Doc, pinProps: PinProps) => void;
ScreenToLocalTransform: () => Transform;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
canEmbedOnDrag?: boolean;
+ treeViewDoc?: Doc;
xPadding?: number;
yPadding?: number;
dropAction?: dropActionType;
@@ -195,6 +192,10 @@ export interface DocumentViewSharedProps {
ignoreAutoHeight?: boolean;
forceAutoHeight?: boolean;
disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
+ onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
+ enableDragWhenActive?: boolean;
+ waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
+ defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<string>;
scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
createNewFilterDoc?: () => void;
@@ -212,14 +213,13 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
hideDocumentButtonBar?: boolean;
hideOpenButton?: boolean;
hideDeleteButton?: boolean;
- treeViewDoc?: Doc;
+ hideLinkAnchors?: boolean;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
radialMenu?: String[];
LayoutTemplateString?: string;
dontCenter?: 'x' | 'y' | 'xy';
- dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling
NativeWidth?: () => number;
NativeHeight?: () => number;
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps
@@ -238,8 +238,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
NativeWidth: () => number;
NativeHeight: () => number;
isSelected: (outsideReaction?: boolean) => boolean;
- isHovering: () => boolean;
- select: (ctrlPressed: boolean) => void;
+ select: (ctrlPressed: boolean, shiftPress?: boolean) => void;
DocumentView: () => DocumentView;
viewPath: () => DocumentView[];
}
@@ -247,18 +246,17 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
@observer
export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
- private _cursorTimer: NodeJS.Timeout | undefined;
- private _longPress = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _doubleClickTimeout: NodeJS.Timeout | undefined;
+ private _singleClickFunc: undefined | (() => any);
+ private _longPressSelector: NodeJS.Timeout | undefined;
private _downX: number = 0;
private _downY: number = 0;
- private _firstX: number = -1;
- private _firstY: number = -1;
+ private _downTime: number = 0;
private _lastTap: number = 0;
private _doubleTap = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
- private _timeout: NodeJS.Timeout | undefined;
private _dropDisposer?: DragManager.DragDropDisposer;
private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -266,14 +264,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
@observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset
@observable _animateScalingTo = 0;
- @observable _pendingDoubleClick = false;
- @observable _cursorPress = false;
- private get topMost() {
- return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
- }
public get animateScaleTime() {
- return this._animateScaleTime ?? 300;
+ return this._animateScaleTime ?? 100;
}
public get displayName() {
return 'DocumentView(' + this.props.Document.title + ')';
@@ -284,8 +277,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
public get LayoutFieldKey() {
return Doc.LayoutFieldKey(this.layoutDoc);
}
- @computed get ShowTitle() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>;
+ @computed get layout_showTitle() {
+ return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.layout_showTitle) as Opt<string>;
}
@computed get NativeDimScaling() {
return this.props.NativeDimScaling?.() || 1;
@@ -293,9 +286,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get thumb() {
return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
}
- @computed get hidden() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
- }
@computed get opacity() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity);
}
@@ -308,8 +298,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get widgetDecorations() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : ''));
}
- @computed get backgroundColor() {
- return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor);
+ @computed get backgroundBoxColor() {
+ return this.thumbShown() ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
}
@computed get docContents() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents);
@@ -317,14 +307,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get headerMargin() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
}
+ @computed get layout_showCaption() {
+ return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.showCaption) || 0;
+ }
@computed get titleHeight() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
}
- @computed get pointerEvents() {
+ @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
}
@computed get finalLayoutKey() {
- return StrCast(this.Document.layoutKey, 'layout');
+ return StrCast(this.Document.layout_fieldKey, 'layout');
}
@computed get nativeWidth() {
return this.props.NativeWidth();
@@ -332,6 +325,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get nativeHeight() {
return this.props.NativeHeight();
}
+ @computed get disableClickScriptFunc() {
+ const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
+ // prettier-ignore
+ return (
+ DocumentView.LongPress ||
+ onScriptDisable === 'always' ||
+ (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
+ this._componentView?.isAnyChildContentActive?.()
+ );
+ }
@computed get onClickHandler() {
return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
}
@@ -351,7 +354,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentDidMount() {
this.setupHandlers();
}
- //componentDidUpdate() { this.setupHandlers(); }
+
setupHandlers() {
this.cleanupHandlers(false);
if (this._mainCont.current) {
@@ -369,327 +372,129 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
- this.removeMoveListeners();
- this.removeEndListeners();
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- if (RadialMenu.Instance._display === false) {
- this.addHoldMoveListeners();
- this.addHoldEndListeners();
- this.onRadialMenu(e, me);
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- this._firstX = pt.pageX;
- this._firstY = pt.pageY;
- }
- };
-
- handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-
- if (this._firstX === -1 || this._firstY === -1) {
- return;
- }
- if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
- this.handle1PointerHoldEnd(e, me);
- }
- };
-
- handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- this.removeHoldMoveListeners();
- this.removeHoldEndListeners();
- RadialMenu.Instance.closeMenu();
- this._firstX = -1;
- this._firstY = -1;
- SelectionManager.DeselectAll();
- me.touchEvent.stopPropagation();
- me.touchEvent.preventDefault();
- e.stopPropagation();
- if (RadialMenu.Instance.used) {
- this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
- }
- };
-
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (!this.props.isSelected()) {
- e.stopPropagation();
- e.preventDefault();
-
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- }
- };
-
- handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- SelectionManager.DeselectAll();
- if (this.Document.onPointerDown) return;
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch) {
- this._downX = touch.clientX;
- this._downY = touch.clientY;
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- e.stopPropagation();
- }
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
- }
- };
-
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (e.cancelBubble && this.props.isDocumentActive?.()) {
- this.removeMoveListeners();
- } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
- this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined);
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- };
-
- @action
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
- if (pinching !== 0 && oldPoint1 && oldPoint2) {
- const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX);
- const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY);
- const dX = -1 * Math.sign(dW);
- const dY = -1 * Math.sign(dH);
-
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(this.props.Document);
- const layoutDoc = Document(Doc.Layout(this.props.Document));
- let nwidth = Doc.NativeWidth(layoutDoc);
- let nheight = Doc.NativeHeight(layoutDoc);
- const width = layoutDoc._width || 0;
- const height = layoutDoc._height || (nheight / nwidth) * width;
- const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling;
- const actualdW = Math.max(width + dW * scale, 20);
- const actualdH = Math.max(height + dH * scale, 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (nwidth && nheight);
- if (fixedAspect && (!nwidth || !nheight)) {
- Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0));
- Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0));
- }
- if (nwidth > 0 && nheight > 0) {
- if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) {
- Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc));
- }
- layoutDoc._width = actualdW;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width;
- else layoutDoc._height = actualdH;
- } else {
- if (!fixedAspect) {
- Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc));
- }
- layoutDoc._height = actualdH;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height;
- else layoutDoc._width = actualdW;
- }
- } else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
- }
- }
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
- @action
- onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
-
- // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 });
- const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) &&
- RadialMenu.Instance.addItem({
- description: 'Delete',
- event: () => {
- this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu();
- },
- icon: 'external-link-square-alt',
- selected: -1,
- });
- // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 });
-
- SelectionManager.DeselectAll();
- };
-
startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
if (this._mainCont.current) {
- const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current);
+ const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()];
+ const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc));
const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0);
dragData.offset = this.props
.ScreenToLocalTransform()
.scale(this.NativeDimScaling)
.transformDirection(x - left, y - top);
- // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
- // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
dragData.dropAction = dropAction;
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;
dragData.canEmbed = this.props.canEmbedOnDrag;
- //dragData.dimSource :
- // dragEffects field, set dim
- // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
- // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
- // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
- DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
- setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
+ DragManager.StartDocumentDrag(
+ selected.map(dv => dv.docView!._mainCont.current!),
+ dragData,
+ x,
+ y,
+ { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) },
+ () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
); // this needs to happen after the drop event is processed.
ffview?.setupDragLines(false);
}
}
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- if (e.key === '†' || e.key === 't') {
- if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
- if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true));
- else if (!this._titleRef.current.setIsFocused(true)) {
- // if focus didn't change, focus on interior text...
- this._titleRef.current?.setIsFocused(false);
- this._componentView?.setFocus?.();
- }
- }
- }
+ defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
+ const targetMatch =
+ Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
+ (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document
+ ? true
+ : false;
+ return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
};
- focus = (anchor: Doc, options: DocFocusOptions) => {
- LightboxView.SetCookie(StrCast(anchor['cookies-set']));
- // copying over VIEW fields immediately allows the view type to switch to create the right _componentView
- Array.from(Object.keys(Doc.GetProto(anchor)))
- .filter(key => key.startsWith(ViewSpecPrefix))
- .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec])));
- // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs
- setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
- const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
- const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
- const startTime = Date.now();
- this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
- ...options,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(async res =>
- setTimeout(
- async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), //
- didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0
- )
- ),
- });
+ // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been)
+ setTitleFocus = () => {
+ if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title';
+ setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
};
+
+ public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
+
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
- const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
(this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
- if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {
- // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._pendingDoubleClick = false;
- this._timeout = undefined;
+ if (this._doubleTap) {
+ const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick;
+ if (this.onDoubleClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables
+ // prettier-ignore
+ const func = () => this.onDoubleClickHandler.script.run( {
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ documentView: this.props.DocumentView(),
+ clientX, clientY, altKey, shiftKey, ctrlKey,
+ value: undefined,
+ }, console.log );
+ UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
+ } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) {
+ UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
+ } else {
+ this._singleClickFunc?.();
}
- if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey, ctrlKey } = e;
- const func = () =>
- this.onDoubleClickHandler.script.run(
+ this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+ this._doubleClickTimeout = undefined;
+ this._singleClickFunc = undefined;
+ } else {
+ let clickFunc: undefined | (() => any);
+ if (!this.disableClickScriptFunc && this.onClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, metaKey } = e;
+ const func = () => {
+ // replace default add doc func with this view's add doc func.
+ // to allow override behaviors for how to display links to undisplayed documents.
+ // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place
+ // instead of in the global lightbox
+ const oldFunc = DocumentViewInternal.addDocTabFunc;
+ DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
+ this.onClickHandler?.script.run(
{
this: this.layoutDoc,
self: this.rootDoc,
+ _readOnly_: false,
scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
documentView: this.props.DocumentView(),
clientX,
clientY,
- altKey,
shiftKey,
- ctrlKey,
+ altKey,
+ metaKey,
},
console.log
- );
- UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
- } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
- UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
- }
- } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey } = e;
- const func = () =>
- this.onClickHandler.script.run(
- {
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX,
- clientY,
- shiftKey,
- altKey,
- },
- console.log
- ).result?.select === true
- ? this.props.select(false)
- : '';
- const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
- if (this.onDoubleClickHandler) {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(() => {
- this._timeout = undefined;
- clickFunc();
- }, 350);
- } else clickFunc();
- } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- SelectionManager.DeselectAll();
- this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
- } else {
- if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
- // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
- stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ ).result?.select === true
+ ? this.props.select(false)
+ : '';
+ DocumentViewInternal.addDocTabFunc = oldFunc;
+ };
+ clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title));
} else {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(
- action(() => {
- this._pendingDoubleClick = false;
- this._timeout = undefined;
- }),
- 350
- );
- this.props.select(e.ctrlKey || e.shiftKey);
+ // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
+ if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
+ stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ }
+ preventDefault = false;
+ }
+
+ this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey));
+ const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
+ if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
+ this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+ this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
+ } else if (!DocumentView.LongPress) {
+ this._singleClickFunc();
+ this._singleClickFunc = undefined;
}
- preventDefault = false;
}
stopPropagate && e.stopPropagation();
preventDefault && e.preventDefault();
@@ -698,68 +503,52 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
- // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
- if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
- if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- e.stopPropagation();
- if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
- // TODO: check here for panning/inking
+ this._longPressSelector = setTimeout(() => {
+ if (DocumentView.LongPress) {
+ if (this.rootDoc.dontUndo) {
+ runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline));
+ } else {
+ this.props.select(false);
+ }
}
- return;
- }
- this._cursorTimer = setTimeout(
- action(() => {
- this._cursorPress = true;
- this.props.select(false);
- }),
- 1000 // long press required duration
- );
+ }, 1000);
+ if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
+
this._downX = e.clientX;
this._downY = e.clientY;
+ this._downTime = Date.now();
if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
- // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
+ // click events stop here if the document is active and no modes are overriding it
+ // if this is part of a template, let the event go up to the template root unless right/ctrl clicking
if (
- (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ // prettier-ignore
+ this.props.isDocumentActive?.() &&
!this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
- !e.ctrlKey &&
- (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
- !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
+ e.button === 0 &&
+ this.pointerEvents !== 'none' &&
+ !Doc.IsInMyOverlay(this.layoutDoc)
) {
e.stopPropagation();
// don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though
- //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
- }
- if (this.props.isDocumentActive?.()) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.addEventListener('pointermove', this.onPointerMove);
+ //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault();
+
+ // listen to move events if document content isn't active or document is draggable
+ if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) {
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
}
- document.removeEventListener('pointerup', this.onPointerUp);
document.addEventListener('pointerup', this.onPointerUp);
- } else {
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
}
};
@action
onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) return;
- if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
-
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
+ if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+
+ if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
+ this.cleanupPointerEvents();
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
}
};
@@ -772,71 +561,44 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerUp = (e: PointerEvent): void => {
this.cleanupPointerEvents();
- this._longPress = this._cursorPress;
- this._cursorTimer && clearTimeout(this._cursorTimer);
- this._cursorPress = false;
+ this._longPressSelector && clearTimeout(this._longPressSelector);
- if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ if (this.onPointerUpHandler?.script) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
- } else {
- this._doubleTap = Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
- // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
- if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
+ } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+ this._doubleTap = this.rootDoc.defaultDoubleClick !== 'ignore' && Date.now() - this._lastTap < Utils.CLICK_TIME;
+ if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
}
+ if (DocumentView.LongPress) e.preventDefault();
};
@undoBatch
@action
- toggleFollowLink = (location: Opt<string>, zoom?: boolean, setTargetToggle?: boolean): void => {
- this.Document.ignoreClick = false;
- if (setTargetToggle) {
- this.Document.followLinkToggle = !this.Document.followLinkToggle;
- this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton;
- } else {
- this.Document._isLinkButton = !this.Document._isLinkButton;
- }
- if (this.Document._isLinkButton && !this.onClickHandler) {
- zoom !== undefined && (this.Document.followLinkZoom = zoom);
- this.Document.followLinkLocation = location;
- } else if (this.Document._isLinkButton && this.onClickHandler) {
- this.Document._isLinkButton = false;
- this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
- };
- @undoBatch
- @action
- toggleTargetOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.followLinkToggle = true;
+ toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
+ const hadOnClick = this.rootDoc.onClick;
+ this.noOnClick();
+ this.Document.onClick = hadOnClick ? undefined : FollowLinkScript();
+ this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never';
};
@undoBatch
@action
- followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
+ followLinkOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
+ this.Document.onClick = FollowLinkScript();
this.Document.followLinkToggle = false;
- this.Document.followLinkZoom = zoom;
- this.Document.followLinkLocation = location;
- };
- @undoBatch
- @action
- selectOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
- this.Document.followLinkToggle = false;
- this.Document.onClick = this.layoutDoc.onClick = undefined;
+ this.Document.followLinkZoom = false;
+ this.Document.followLinkLocation = undefined;
};
@undoBatch
noOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
+ this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined;
};
@undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
@undoBatch setToggleDetail = () =>
(this.Document.onClick = ScriptField.MakeScript(
- `toggleDetail(documentView, "${StrCast(this.Document.layoutKey)
+ `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
.replace('layout_', '')
.replace(/^layout$/, 'detail')}")`,
{ documentView: 'any' }
@@ -844,37 +606,59 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch
@action
- drop = async (e: Event, de: DragManager.DropEvent) => {
+ drop = (e: Event, de: DragManager.DropEvent) => {
if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
if (this.props.Document === Doc.ActiveDashboard) {
alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
return;
}
const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
- if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
- if (linkdrag?.linkSourceDoc) {
- e.stopPropagation();
- if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
- de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
- }
- if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
- const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document;
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
+ if (linkdrag) {
+ linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+ if (linkdrag.linkSourceDoc) {
+ e.stopPropagation();
+ if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+ de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+ }
+ if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) {
+ const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
+ de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
+ }
}
}
};
@undoBatch
@action
- makeIntoPortal = async () => {
- const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
+ makeIntoPortal = () => {
+ const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.props.Document && d.link_relationship === 'portal to:portal from');
if (!portalLink) {
- const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' });
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from');
+ DocUtils.MakeLink(
+ this.props.Document,
+ Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _layout_fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }),
+ { link_relationship: 'portal to:portal from' }
+ );
}
- this.Document.followLinkLocation = OpenWhere.inPlace;
- this.Document.followLinkZoom = true;
- this.Document._isLinkButton = true;
+ this.Document.followLinkLocation = OpenWhere.lightbox;
+ this.Document.onClick = FollowLinkScript();
+ };
+
+ importDocument = () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.zip';
+ input.onchange = _e => {
+ if (input.files) {
+ const batch = UndoManager.StartBatch('importing');
+ Doc.importDocument(input.files[0]).then(doc => {
+ if (doc instanceof Doc) {
+ this.props.addDocTab(doc, OpenWhere.addRight);
+ batch.end();
+ }
+ });
+ }
+ };
+ input.click();
};
@action
@@ -924,79 +708,52 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
.forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp }));
if (!this.props.Document.isFolder) {
- const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+ const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null);
const appearance = cm.findByDescription('UI Controls...');
const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
!Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
- !Doc.noviceMode &&
- appearanceItems.push({
- description: 'Add a Field',
- event: () => {
- const alias = Doc.MakeAlias(this.rootDoc);
- alias.layout = FormattedTextBox.LayoutString('newfield');
- alias.title = 'newfield';
- alias._height = 35;
- alias._width = 100;
- alias.syncLayoutFieldWithTitle = true;
- alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
- alias.y = NumCast(this.rootDoc.y);
- this.props.addDocument?.(alias);
- },
- icon: 'eye',
- });
- DocListCast(this.Document.links).length &&
- appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
- !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
+ !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
- if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) {
const existingOnClick = cm.findByDescription('OnClick...');
const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
if (this.props.bringToFront !== emptyFunction) {
const zorders = cm.findByDescription('ZOrder...');
const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
- zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'expand-arrows-alt' });
- zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'expand-arrows-alt' });
+ zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' });
+ zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' });
zorderItems.push({
description: this.rootDoc._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))),
- icon: 'expand-arrows-alt',
+ icon: 'hand-point-up',
});
- !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
+ !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' });
}
- !Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
+ onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
!Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
- this.props.CollectionFreeFormDocumentView &&
- onClicks.push({
- description: (this.Document.followLinkZoom ? "Don't" : '') + ' zoom following link',
- event: () => (this.Document.followLinkZoom = !this.Document.followLinkZoom),
- icon: this.Document.ignoreClick ? 'unlock' : 'lock',
- });
- if (!this.Document.annotationOn) {
- const options = cm.findByDescription('Options...');
- const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
- !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
-
- onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' });
- onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' });
- !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' });
- onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' });
- onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
- onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
- } else if (DocListCast(this.Document.links).length) {
- onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' });
- onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' });
- onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' });
+ if (!this.props.treeViewDoc) {
+ if (!this.Document.annotationOn) {
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
+
+ onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
+ onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
+ !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+ } else if (LinkManager.Links(this.Document).length) {
+ onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' });
+ onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
+ !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
+ }
}
}
const funcs: ContextMenuProps[] = [];
if (!Doc.noviceMode && this.layoutDoc.onDragStart) {
- funcs.push({ description: 'Drag an Alias', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
+ funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) });
funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) });
cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' });
@@ -1005,7 +762,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const more = cm.findByDescription('More...');
const moreItems = more && 'subitems' in more ? more.subitems : [];
if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
if (!Doc.noviceMode) {
moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' });
moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' });
@@ -1019,15 +775,23 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
}
- if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
+ !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+ }
+ const constantItems: ContextMenuProps[] = [];
+ constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
+ if (!Doc.IsSystem(this.rootDoc)) {
+ constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+ constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
+ (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+ if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
// need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
- moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
+ constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
}
- !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
}
+ cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' });
+
const help = cm.findByDescription('Help...');
const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
- helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
!Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
!Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
!Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
@@ -1065,44 +829,33 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
break;
}
// Add link to help documentation
- if (documentationDescription && documentationLink) {
- console.log('add documentation item');
+ if (!this.props.treeViewDoc && documentationDescription && documentationLink) {
helpItems.push({
description: documentationDescription,
- event: () => {
- window.open(documentationLink, '_blank');
- },
+ event: () => window.open(documentationLink, '_blank'),
icon: 'book',
});
}
- cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+ if (!help) cm.addItem({ description: 'Help...', noexpand: !Doc.noviceMode, subitems: helpItems, icon: 'question' });
+ else cm.moveAfter(help);
- if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
+ e?.stopPropagation(); // DocumentViews should stop propagation of this event
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
};
- collectionFilters = () => StrListCast(this.props.Document._docFilters);
- collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
- @computed get showFilterIcon() {
- return this.collectionFilters().length || this.collectionRangeDocFilters().length
- ? 'hasFilter'
- : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length
- ? 'inheritsFilter'
- : undefined;
- }
rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
- onClickFunc = () => this.onClickHandler;
+ onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
setHeight = (height: number) => (this.layoutDoc._height = height);
setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- isContentActive = (outsideReaction?: boolean) => {
+ isContentActive = (outsideReaction?: boolean): boolean | undefined => {
return this.props.isContentActive() === false
? false
: Doc.ActiveTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.rootSelected() ||
- this.props.Document.forceActive ||
+ this.rootDoc.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
this.props.isContentActive()
@@ -1116,48 +869,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
.concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
.some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
- return !this.props.isSelected() &&
+ return !this.props.LayoutTemplateString &&
+ !this.props.isSelected() &&
LightboxView.LightboxDoc !== this.rootDoc &&
this.thumb &&
!Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
- ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) &&
+ ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) &&
!this._componentView?.isAnyChildContentActive?.()
? true
: false;
};
- get audioAnnoState() {
- return this.dataDoc.audioAnnoState ?? 'stopped';
- }
- @computed get audioAnnoView() {
- const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
- const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
- const audioIconColors = new Map<string, string>([
- ['recording', 'red'],
- ['playing', 'green'],
- ['stopped', audioAnnosCount ? 'blue' : 'gray'],
- ]);
- return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : (
- <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
- <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
- <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
- </div>
- </Tooltip>
- );
- }
+ docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)];
+ contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
@computed get contents() {
TraceMobx();
+ const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
return (
<div
className="documentView-contentsView"
style={{
- pointerEvents:
- (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'
- ? 'none'
- : (this.props.contentPointerEvents as any)
- ? (this.props.contentPointerEvents as any)
- : this.rootDoc.type !== DocumentType.INK && this.isContentActive()
- ? 'all'
- : 'none',
+ pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all',
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
{!this._retryThumb || !this.thumbShown() ? null : (
@@ -1168,20 +899,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
height={this.props.PanelHeight()}
onError={(e: any) => {
setTimeout(action(() => (this._retryThumb = 0)));
- setTimeout(
- action(() => (this._retryThumb = 1)),
- 150
- );
+ // prettier-ignore
+ setTimeout(action(() => (this._retryThumb = 1)), 150 );
}}
/>
)}
<DocumentContentsView
key={1}
{...this.props}
+ pointerEvents={this.contentPointerEvents}
docViewPath={this.props.viewPath}
thumbShown={this.thumbShown}
- isHovering={this.props.isHovering}
setContentView={this.setContentView}
+ docFilters={this.docFilters}
NativeDimScaling={this.props.NativeDimScaling}
PanelHeight={this.panelHeight}
setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
@@ -1189,36 +919,28 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
ScreenToLocalTransform={this.screenToLocal}
rootSelected={this.rootSelected}
onClick={this.onClickFunc}
- focus={this.focus}
- layoutKey={this.finalLayoutKey}
+ focus={this.props.focus}
+ setTitleFocus={this.setTitleFocus}
+ layout_fieldKey={this.finalLayoutKey}
/>
{this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
- {this.audioAnnoView}
</div>
);
}
- get indicatorIcon() {
- if (this.props.Document['acl-Public'] !== SharingPermissions.None) return 'globe-americas';
- else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return 'users';
- else return 'user';
- }
-
- @undoBatch
- hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true);
anchorPanelWidth = () => this.props.PanelWidth() || 1;
anchorPanelHeight = () => this.props.PanelHeight() || 1;
anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
// prettier-ignore
switch (property.split(':')[0]) {
- case StyleProp.ShowTitle: return '';
+ case StyleProp.layout_showTitle: return '';
case StyleProp.PointerEvents: return 'none';
- case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox
+ case StyleProp.Highlighting: return undefined;
}
return this.props.styleProvider?.(doc, props, property);
};
// We need to use allrelatedLinks to get not just links to the document as a whole, but links to
- // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g.,
+ // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g.,
// - PDF text regions are rendered as an Annotations without generating a DocumentView, '
// - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
// - and links to PDF/Web docs at a certain scroll location never create an explicit view.
@@ -1227,63 +949,45 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(
link =>
- Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) ||
- Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) ||
- ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) ||
- ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc))
+ Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) ||
+ Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) ||
+ ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
+ ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc))
);
}
@computed get allLinks() {
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc);
}
+ hideLink = computedFn((link: Doc) => () => (link.layout_linkDisplay = false));
@computed get allLinkEndpoints() {
// the small blue dots that mark the endpoints of links
TraceMobx();
- if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
- if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null;
- const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay);
- return filtered.map((link, i) => (
+ if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
+ const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.layout_linkDisplay);
+ return filtered.map(link => (
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView
{...this.props}
isContentActive={returnFalse}
Document={link}
+ docViewPath={this.props.viewPath}
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
dontRegisterView={false}
- showTitle={returnEmptyString}
+ layout_showTitle={returnEmptyString}
hideCaptions={true}
- fitWidth={returnTrue}
+ hideLinkAnchors={true}
+ layout_fitWidth={returnTrue}
+ removeDocument={this.hideLink(link)}
styleProvider={this.anchorStyleProvider}
- removeDocument={this.hideLinkAnchor}
LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${Doc.LinkEndpoint(link, this.rootDoc)}`)}
/>
</div>
));
}
- @action
- playAnnotation = () => {
- const self = this;
- const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
- const anno = audioAnnos?.lastElement();
- if (anno instanceof AudioField && this.audioAnnoState === 'stopped') {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => (self.dataDoc.audioAnnoState = 'stopped'));
- },
- });
- this.dataDoc.audioAnnoState = 'playing';
- }
- };
-
static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
let gumStream: any;
let recorder: any;
@@ -1331,57 +1035,74 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
else setTimeout(stopFunc, 5000);
});
}
+ playAnnotation = () => {
+ const self = this;
+ const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
+ const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+ const anno = audioAnnos?.lastElement();
+ if (anno instanceof AudioField && audioAnnoState === 'stopped') {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
+ });
+ this.dataDoc.audioAnnoState = 'playing';
+ }
+ };
- captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+ captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
@computed get innards() {
TraceMobx();
const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
- const showTitle = this.ShowTitle?.split(':')[0];
- const showTitleHover = this.ShowTitle?.includes(':hover');
- const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
- const captionView = !showCaption ? null : (
+ const layout_showTitle = this.layout_showTitle?.split(':')[0];
+ const layout_showTitleHover = this.layout_showTitle?.includes(':hover');
+ const captionView = !this.layout_showCaption ? null : (
<div
className="documentView-captionWrapper"
style={{
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
maxHeight: `max(100%, ${20 * ffscale()}px)`,
}}>
<FormattedTextBox
- {...OmitKeys(this.props, ['children']).omit}
+ {...this.props}
yPadding={10}
xPadding={10}
- fieldKey={showCaption}
+ fieldKey={this.layout_showCaption}
fontSize={12 * Math.max(1, (2 * ffscale()) / 3)}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
noSidebar={true}
dontScale={true}
+ renderDepth={this.props.renderDepth}
isContentActive={this.isContentActive}
- onClick={this.onClickFunc}
/>
</div>
);
- const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
+ const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
const background = StrCast(
SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
- Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
+ Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
);
- const titleView = !showTitle ? null : (
+ const layout_sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
+ const titleView = !layout_showTitle ? null : (
<div
- className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
+ className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`}
key="title"
style={{
position: this.headerMargin ? 'relative' : 'absolute',
height: this.titleHeight,
- width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
+ width: !this.headerMargin ? `calc(${layout_sidebarWidthPercent || 100}% - 18px)` : (layout_sidebarWidthPercent || 100) + '%', // leave room for annotation button
color: lightOrDark(background),
background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
<EditableView
ref={this._titleRef}
- contents={showTitle
+ contents={layout_showTitle
.split(';')
.map(field => field.trim())
.map(field => targetDoc[field]?.toString())
@@ -1390,27 +1111,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
fontSize={10}
GetValue={() => {
this.props.select(false);
- return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
+ return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle;
}}
SetValue={undoBatch((input: string) => {
if (input?.startsWith('#')) {
- if (this.props.showTitle) {
- this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
+ if (this.props.layout_showTitle) {
+ this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else {
- Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
+ Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date';
}
} else {
- var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
- if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
- if (showTitle.includes('Date') || showTitle === 'author') return true;
- Doc.SetInPlace(targetDoc, showTitle, value, true);
+ var value = input.replace(new RegExp(layout_showTitle + '='), '') as string | number;
+ if (layout_showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
+ if (layout_showTitle.includes('Date') || layout_showTitle === 'author') return true;
+ Doc.SetInPlace(targetDoc, layout_showTitle, value, true);
}
return true;
})}
/>
</div>
);
- return this.props.hideTitle || (!showTitle && !showCaption) ? (
+ return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? (
this.contents
) : (
<div className="documentView-styleWrapper">
@@ -1422,34 +1143,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
}
- @observable _: string = '';
+
renderDoc = (style: object) => {
TraceMobx();
- const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
- const isButton = this.props.Document.type === DocumentType.FONTICON;
- if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
- return (
- this.docContents ?? (
- <div
- className={`documentView-node${this.topMost ? '-topmost' : ''}`}
- id={this.props.Document[Id]}
- style={{
- ...style,
- background: isButton || thumb ? undefined : this.backgroundColor,
- opacity: this.opacity,
- cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
- color: StrCast(this.layoutDoc.color, 'inherit'),
- fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
- fontSize: Cast(this.Document._fontSize, 'string', null),
- transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
- }}>
- {this.innards}
- {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
- {this.widgetDecorations ?? null}
- </div>
- )
- );
+ return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate
+ ? null
+ : this.docContents ?? (
+ <div
+ className="documentView-node"
+ id={this.Document[Id]}
+ style={{
+ ...style,
+ background: this.backgroundBoxColor,
+ opacity: this.opacity,
+ cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
+ color: StrCast(this.layoutDoc.color, 'inherit'),
+ fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
+ fontSize: Cast(this.Document._fontSize, 'string', null),
+ transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
+ transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+ }}>
+ {this.innards}
+ {this.widgetDecorations ?? null}
+ </div>
+ );
};
/**
@@ -1458,7 +1175,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
* @returns a function that will wrap a JSX animation element wrapping any JSX element
*/
public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
- const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.linkAnimDirection;
+ const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.followLinkAnimDirection;
const effectProps = {
left: dir === PresEffectDirection.Left,
right: dir === PresEffectDirection.Right,
@@ -1466,7 +1183,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
bottom: dir === PresEffectDirection.Bottom,
opposite: true,
delay: 0,
- duration: Cast(presEffectDoc?.presTransition, 'number', null),
+ duration: Cast(presEffectDoc?.presTransition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
};
//prettier-ignore
switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
@@ -1484,7 +1201,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
render() {
TraceMobx();
const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
- const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
+ const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
const boxShadow =
this.props.treeViewDoc || !highlighting
? this.boxShadow
@@ -1496,50 +1213,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
+ clipPath: borderPath?.clipPath,
});
- const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
return (
<div
className={`${DocumentView.ROOT_DIV} docView-hack`}
ref={this._mainCont}
onContextMenu={this.onContextMenu}
- onKeyDown={this.onKeyDown}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)}
style={{
- display: this.hidden ? 'inline' : undefined,
borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents,
+ pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
}}>
- {!borderPath.path ? (
- animRenderDoc
- ) : (
- <>
- {animRenderDoc}
- <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
- <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
- <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
- </svg>
- </div>
- </>
- )}
- {this.showFilterIcon ? (
- <FontAwesomeIcon
- icon={'filter'}
- size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: this.showFilterIcon === 'hasFilter' ? '#18c718bd' : 'orange', zIndex: 1 }}
- onPointerDown={action(e => {
- this.props.select(false);
- SettingsManager.propertiesWidth = 250;
- e.stopPropagation();
- })}
- />
- ) : null}
+ <>
+ {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)}
+ {borderPath?.jsx}
+ </>
</div>
);
}
@@ -1548,16 +1242,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
public static ROOT_DIV = 'documentView-effectsWrapper';
+ @observable public static LongPress = false;
+ @observable public docView: DocumentViewInternal | undefined | null;
+ @observable public textHtmlOverlay: Opt<string>;
+ @observable private _isHovering = false;
+
+ public htmlOverlayEffect = '';
public get displayName() {
return 'DocumentView(' + this.props.Document?.title + ')';
} // this makes mobx trace() statements more descriptive
public ContentRef = React.createRef<HTMLDivElement>();
public ViewTimer: NodeJS.Timeout | undefined; // timer for res
+ public AnimEffectTimer: NodeJS.Timeout | undefined; // timer for res
private _disposers: { [name: string]: IReactionDisposer } = {};
public clearViewTransition = () => {
this.ViewTimer && clearTimeout(this.ViewTimer);
this.rootDoc._viewTransition = undefined;
};
+ public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+
+ public showContextMenu = (pageX: number, pageY: number) => this.docView?.onContextMenu(undefined, pageX, pageY);
+
+ public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
+ this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer);
+ this.rootDoc[AnimationSym] = presEffect;
+ this.AnimEffectTimer = setTimeout(() => (this.rootDoc[AnimationSym] = undefined), timeInMs);
+ };
public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`;
if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`;
@@ -1584,34 +1294,18 @@ export class DocumentView extends React.Component<DocumentViewProps> {
);
}
+ // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
public static showBackLinks(linkSource: Doc) {
- const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
- DocServer.GetRefField(docid).then(docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(linkSource);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- const linkCollection =
- (docx instanceof Doc && docx) ||
- Docs.Create.StackingDocument(
- [
- /*rootAlias()*/
- ],
- { title: linkSource.title + '-pivot', _width: 500, _height: 500 },
- docid
- );
- linkCollection.linkSource = linkSource;
- if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)');
- LightboxView.SetLightboxDoc(linkCollection);
- });
+ const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
+ // prettier-ignore
+ DocServer.GetRefField(docId).then(docx => docx instanceof Doc &&
+ LightboxView.SetLightboxDoc(
+ docx || // reuse existing pivot view of documents, or else create a new collection
+ Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId)
+ )
+ );
}
- @observable public docView: DocumentViewInternal | undefined | null;
-
- showContextMenu(pageX: number, pageY: number) {
- return this.docView?.onContextMenu(undefined, pageX, pageY);
- }
get Document() {
return this.props.Document;
}
@@ -1619,13 +1313,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.renderDepth === 0;
}
get rootDoc() {
- return this.docView?.rootDoc || this.Document;
+ return this.docView?.rootDoc ?? this.Document;
}
get dataDoc() {
- return this.docView?.dataDoc || this.Document;
- }
- get finalLayoutKey() {
- return this.docView?.finalLayoutKey || 'layout';
+ return this.docView?.dataDoc ?? this.Document;
}
get ContentDiv() {
return this.docView?.ContentDiv;
@@ -1639,20 +1330,19 @@ export class DocumentView extends React.Component<DocumentViewProps> {
get LayoutFieldKey() {
return this.docView?.LayoutFieldKey || 'layout';
}
- get fitWidth() {
- return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth;
+ @computed get layout_fitWidth() {
+ return this.docView?._componentView?.layout_fitWidth?.() ?? this.props.layout_fitWidth?.(this.rootDoc) ?? this.layoutDoc?.layout_fitWidth;
+ }
+ @computed get anchorViewDoc() {
+ return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.rootDoc['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.rootDoc['link_anchor_1']) : undefined;
}
-
@computed get hideLinkButton() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : ''));
}
- linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale;
-
@computed get linkCountView() {
const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
- return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
+ return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
}
-
@computed get docViewPath(): DocumentView[] {
return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
}
@@ -1660,13 +1350,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return Doc.Layout(this.Document, this.props.LayoutTemplate?.());
}
@computed get nativeWidth() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth));
}
@computed get nativeHeight() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth));
}
@computed get shouldNotScale() {
- return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+ return (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any);
}
@computed get effectiveNativeWidth() {
return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
@@ -1677,19 +1367,17 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get nativeScaling() {
if (this.shouldNotScale) return 1;
const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
- if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
- return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth
+ if (this.layout_fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
+ return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
}
return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
-
@computed get panelWidth() {
return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth();
}
@computed get panelHeight() {
- if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
- const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
- return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
+ if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) {
+ return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
}
return this.props.PanelHeight();
}
@@ -1697,7 +1385,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0;
}
@computed get Yshift() {
- return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight()))
+ return this.effectiveNativeWidth &&
+ this.effectiveNativeHeight &&
+ Math.abs(this.Xshift) < 0.001 &&
+ (!this.layoutDoc.nativeHeightUnfrozen || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight()))
? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2)
: 0;
}
@@ -1708,13 +1399,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.dontCenter?.includes('y') ? 0 : this.Yshift;
}
- toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
- focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options);
- getBounds = () => {
- if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+ public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
+ public getBounds = () => {
+ if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
}
- const xf = this.docView?.props
+ const xf = this.docView.props
.ScreenToLocalTransform()
.scale(this.trueNativeWidth() ? this.nativeScaling : 1)
.inverse();
@@ -1734,10 +1424,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
finished?.();
this.docView && (this.docView._animateScaleTime = animTime);
});
- const layoutKey = Cast(this.Document.layoutKey, 'string', null);
- if (layoutKey !== 'layout_icon') {
+ const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null);
+ if (layout_fieldKey !== 'layout_icon') {
this.switchViews(true, 'icon', finalFinished);
- if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
+ 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);
@@ -1751,12 +1441,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Doc.setNativeView(this.props.Document);
custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
};
- switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+ @action
+ switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
setTimeout(
action(() => {
if (useExistingLayout && custom && this.rootDoc['layout_' + view]) {
- this.rootDoc.layoutKey = 'layout_' + view;
+ this.rootDoc.layout_fieldKey = 'layout_' + view;
} else {
this.setCustomView(custom, view);
}
@@ -1771,17 +1462,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}),
this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
);
- });
-
- startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+ };
- @observable textHtmlOverlay: Opt<string>;
- @computed get anchorViewDoc() {
- return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
- }
+ scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale;
docViewPathFunc = () => this.docViewPath;
isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
- select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
+ select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection);
NativeWidth = () => this.effectiveNativeWidth;
NativeHeight = () => this.effectiveNativeHeight;
PanelWidth = () => this.panelWidth;
@@ -1795,11 +1481,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
.translate(-this.centeringX, -this.centeringY)
.scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1);
componentDidMount() {
- this._disposers.reactionScript = reaction(
- () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
+ this._disposers.updateContentsScript = reaction(
+ () => ScriptCast(this.rootDoc.updateContentsScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
output => output
);
this._disposers.height = reaction(
+ // increase max auto height if document has been resized to be greater than current max
() => NumCast(this.layoutDoc._height),
action(height => {
const docMax = NumCast(this.layoutDoc.docMaxAutoHeight);
@@ -1812,24 +1499,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Object.values(this._disposers).forEach(disposer => disposer?.());
!BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
}
- _hoverTimeout: any = undefined;
- isHovering = () => this._isHovering;
- @observable _isHovering = false;
-
- htmlOverlayEffect = '';
@computed get htmlOverlay() {
- const effectProps = {
- delay: 0,
- duration: 500,
- };
- const highlight = (
- <div className="webBox-textHighlight">
- <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
- </div>
- );
return !this.textHtmlOverlay ? null : (
<div className="documentView-htmlOverlay">
- <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div>
+ <div className="documentView-htmlOverlayInner">
+ <Fade delay={0} duration={500}>
+ {DocumentViewInternal.AnimationEffect(
+ <div className="webBox-textHighlight">
+ <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
+ </div>,
+ { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc,
+ this.rootDoc
+ )}{' '}
+ </Fade>
+ </div>
</div>
);
}
@@ -1838,34 +1521,20 @@ export class DocumentView extends React.Component<DocumentViewProps> {
TraceMobx();
const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
- const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
return (
- <div
- className="contentFittingDocumentView"
- onPointerEnter={action(() => {
- clearTimeout(this._hoverTimeout);
- this._isHovering = true;
- })}
- onPointerLeave={action(() => {
- clearTimeout(this._hoverTimeout);
- this._hoverTimeout = setTimeout(
- action(() => (this._isHovering = false)),
- 500
- );
- })}>
+ <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
{!this.props.Document || !this.props.PanelWidth() ? null : (
<div
className="contentFittingDocumentView-previewDoc"
ref={this.ContentRef}
style={{
transition: this.props.dataTransition,
- transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
- height:
- isButton || this.props.forceAutoHeight
- ? undefined
- : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
+ transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
+ height: this.props.forceAutoHeight
+ ? undefined
+ : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
}}>
<DocumentViewInternal
{...this.props}
@@ -1878,7 +1547,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
NativeDimScaling={this.NativeDimScaling}
isSelected={this.isSelected}
select={this.select}
- isHovering={this.isHovering}
ScreenToLocalTransform={this.screenToLocalTransform}
focus={this.props.focus || emptyFunction}
ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
@@ -1893,17 +1561,18 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
}
-export function deiconifyViewFunc(documentView: DocumentView) {
- documentView.iconify();
- //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
-}
ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
documentView.iconify();
documentView.select(false);
});
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+ //documentView.iconify(() =>
+ LightboxView.AddDocTab(documentView.rootDoc, OpenWhere.lightbox, 'layout'); //, 0);
+});
+
ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
- if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
});
@@ -1911,17 +1580,17 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) {
const linkSource = Cast(linkCollection.linkSource, Doc, null);
const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
let wid = linkSource[WidthSym]();
- const links = DocListCast(linkSource.links);
+ const links = LinkManager.Links(linkSource);
links.forEach(link => {
const other = LinkManager.getOppositeAnchor(link, linkSource);
const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- const alias = Doc.MakeAlias(otherdoc);
- alias.x = wid;
- alias.y = 0;
- alias._lockedPosition = false;
+ const embedding = Doc.MakeEmbedding(otherdoc);
+ embedding.x = wid;
+ embedding.y = 0;
+ embedding._lockedPosition = false;
wid += otherdoc[WidthSym]();
- Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', alias);
+ Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding);
}
});
return links;
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index c279341cc..f17ab06e7 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -21,6 +21,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static SelectOnLoad: string = '';
_ref: React.RefObject<EquationEditor> = React.createRef();
componentDidMount() {
+ this.props.setContentView?.(this);
if (EquationBox.SelectOnLoad === this.rootDoc[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
this.props.select(false);
@@ -78,31 +79,21 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc);
};
@undoBatch
- onChange = (str: string) => {
- this.dataDoc.text = str;
+ onChange = (str: string) => (this.dataDoc.text = str);
+
+ updateSize = () => {
const style = this._ref.current && getComputedStyle(this._ref.current.element.current);
- if (style) {
- const _height = Number(style.height.replace('px', ''));
- const _width = Number(style.width.replace('px', ''));
- this.layoutDoc._width = Math.max(35, _width);
- this.layoutDoc._height = Math.max(25, _height);
+ if (style?.width.endsWith('px') && style?.height.endsWith('px')) {
+ this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', '')));
+ this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', '')));
}
};
render() {
TraceMobx();
- const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
return (
<div
- ref={r => {
- r instanceof HTMLDivElement &&
- new ResizeObserver(
- action((entries: any) => {
- if (entries[0].contentBoxSize[0].inlineSize) {
- this.rootDoc._width = entries[0].contentBoxSize[0].inlineSize;
- }
- })
- ).observe(r);
- }}
+ ref={r => this.updateSize()}
className="equationBox-cont"
onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
style={{
@@ -110,6 +101,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
width: 'fit-content', // `${100 / scale}%`,
height: `${100 / scale}%`,
pointerEvents: !this.props.isSelected() ? 'none' : undefined,
+ fontSize: StrCast(this.rootDoc._fontSize),
}}
onKeyDown={e => e.stopPropagation()}>
<EquationEditor ref={this._ref} value={this.dataDoc.text || 'x'} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e53422ab7..85dd779fc 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
select: (isCtrlPressed: boolean) => void;
isContentActive: (outsideReaction?: boolean) => boolean | undefined;
- isDocumentActive?: () => boolean;
+ isDocumentActive?: () => boolean | undefined;
isSelected: (outsideReaction?: boolean) => boolean;
setHeight?: (height: number) => void;
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
@@ -32,6 +32,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
// See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields
// Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields
backgroundColor?: string;
+ treeViewDoc?: Doc;
color?: string;
fontSize?: number;
height?: number;
@@ -49,50 +50,18 @@ export class FieldView extends React.Component<FieldViewProps> {
@computed
get field(): FieldResult {
- const { Document, fieldKey } = this.props;
+ const { Document, fieldKey: fieldKey } = this.props;
return Document[fieldKey];
}
render() {
const field = this.field;
- if (field === undefined) {
- return <p>{'<null>'}</p>;
- }
- // if (typeof field === "string") {
- // return <p>{field}</p>;
- // }
- // else if (field instanceof RichTextField) {
- // return <FormattedTextBox {...this.props} />;
- // }
- // else if (field instanceof ImageField) {
- // return <ImageBox {...this.props} />;
- // }
- // else if (field instaceof PresBox) {
- // return <PresBox {...this.props} />;
- // }
- // else if (field instanceof VideoField) {
- // return <VideoBox {...this.props} />;
- // }
- // else if (field instanceof AudioField) {
- // return <AudioBox {...this.props} />;
- //}
- else if (field instanceof DateField) {
- return <p>{field.date.toLocaleString()}</p>;
- } else if (field instanceof Doc) {
- return (
- <p>
- <b>{field.title?.toString()}</b>
- </p>
- );
- } else if (field instanceof List) {
- return <div> {field.length ? field.map(f => Field.toString(f)).join(', ') : ''} </div>;
- }
- // bcz: this belongs here, but it doesn't render well so taking it out for now
- else if (field instanceof WebField) {
- return <p>{Field.toString(field.url.href)}</p>;
- } else if (!(field instanceof Promise)) {
- return <p>{Field.toString(field)}</p>;
- } else {
- return <p> {'Waiting for server...'} </p>;
- }
+ // prettier-ignore
+ if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>;
+ if (field === undefined) return <p>{'<null>'}</p>;
+ if (field instanceof DateField) return <p>{field.date.toLocaleString()}</p>;
+ if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>;
+ if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>;
+ if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>;
+ return <p> {'Waiting for server...'} </p>;
}
}
diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/nodes/FilterBox.scss
deleted file mode 100644
index 107ad2e36..000000000
--- a/src/client/views/nodes/FilterBox.scss
+++ /dev/null
@@ -1,193 +0,0 @@
-.filterBox-flyout {
- display: block;
- text-align: left;
- font-weight: 100;
-
- .filterBox-flyout-facet {
- background-color: white;
- text-align: left;
- display: inline-block;
- position: relative;
- width: 100%;
-
- .filterBox-flyout-facet-check {
- margin-right: 6px;
- }
- }
-}
-
-
-.filter-bookmark {
- //display: flex;
-
- .filter-bookmark-icon {
- float: right;
- margin-right: 10px;
- margin-top: 7px;
- }
-}
-
-// .filterBox-bottom {
-// // position: fixed;
-// // bottom: 0;
-// // width: 100%;
-// }
-
-.filterBox-select {
- // width: 90%;
- margin-top: 5px;
- // margin-bottom: 15px;
-}
-
-
-.filterBox-saveBookmark {
- background-color: #e9e9e9;
- border-radius: 11px;
- padding-left: 8px;
- padding-right: 8px;
- padding-top: 5px;
- padding-bottom: 5px;
- margin: 8px;
- display: flex;
- font-size: 11px;
- cursor: pointer;
-
- &:hover {
- background-color: white;
- }
-
- .filterBox-saveBookmark-icon {
- margin-right: 6px;
- margin-top: 4px;
- margin-left: 2px;
- }
-
-}
-
-.filterBox-select-scope,
-.filterBox-select-bool,
-.filterBox-addWrapper,
-.filterBox-select-matched,
-.filterBox-saveWrapper {
- font-size: 10px;
- justify-content: center;
- justify-items: center;
- padding-bottom: 10px;
- display: flex;
-}
-
-.filterBox-addWrapper {
- font-size: 11px;
- width: 100%;
-}
-
-.filterBox-saveWrapper {
- width: 100%;
-}
-
-// .filterBox-top {
-// padding-bottom: 20px;
-// border-bottom: 2px solid black;
-// position: fixed;
-// top: 0;
-// width: 100%;
-// }
-
-.filterBox-select-scope {
- padding-bottom: 20px;
- border-bottom: 2px solid black;
-}
-
-.filterBox-title {
- font-size: 15;
- // border: 2px solid black;
- width: 100%;
- align-self: center;
- text-align: center;
- background-color: #d3d3d3;
-}
-
-.filterBox-select-bool {
- margin-top: 6px;
-}
-
-.filterBox-select-text {
- margin-right: 8px;
- margin-left: 8px;
- margin-top: 3px;
-}
-
-.filterBox-select-box {
- margin-right: 2px;
- font-size: 30px;
- border: 0;
- background: transparent;
-}
-
-.filterBox-selection {
- border-radius: 6px;
- border: none;
- background-color: #e9e9e9;
- padding: 2px;
-
- &:hover {
- background-color: white;
- }
-}
-
-.filterBox-addFilter {
- width: 120px;
- background-color: #e9e9e9;
- border-radius: 12px;
- padding: 5px;
- margin: 5px;
- display: flex;
- text-align: center;
- justify-content: center;
-
- &:hover {
- background-color: white;
- }
-}
-
-.filterBox-treeView {
- display: flex;
- flex-direction: column;
- width: 200px;
- height: 100%;
- position: absolute;
- right: 0;
- top: 0;
- z-index: 1;
- background-color: #9F9F9F;
-
- .filterBox-tree {
- z-index: 0;
- }
-
- .filterBox-addfacet {
- display: inline-block;
- width: 200px;
- height: 30px;
- text-align: left;
-
- .filterBox-addFacetButton {
- display: flex;
- margin: auto;
- cursor: pointer;
- }
-
- >div,
- >div>div {
- width: 100%;
- height: 100%;
- }
- }
-
- .filterBox-tree {
- display: inline-block;
- width: 100%;
- margin-bottom: 10px;
- //height: calc(100% - 30px);
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
deleted file mode 100644
index 78ba499ec..000000000
--- a/src/client/views/nodes/FilterBox.tsx
+++ /dev/null
@@ -1,588 +0,0 @@
-import React = require('react');
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, reaction, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import Select from 'react-select';
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt } from '../../../fields/Doc';
-import { List } from '../../../fields/List';
-import { RichTextField } from '../../../fields/RichTextField';
-import { listSpec } from '../../../fields/Schema';
-import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
-import { Docs, DocumentOptions } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { UserOptions } from '../../util/GroupManager';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
-import { CollectionTreeView } from '../collections/CollectionTreeView';
-import { CollectionView } from '../collections/CollectionView';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import { EditableView } from '../EditableView';
-import { SearchBox } from '../search/SearchBox';
-import { DashboardToggleButton, DefaultStyleProvider, StyleProp } from '../StyleProvider';
-import { DocumentViewProps } from './DocumentView';
-import { FieldView, FieldViewProps } from './FieldView';
-import './FilterBox.scss';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-@observer
-export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
- constructor(props: Readonly<FieldViewProps>) {
- super(props);
- const targetDoc = FilterBox.targetDoc;
- if (targetDoc && !targetDoc.currentFilter) runInAction(() => (targetDoc.currentFilter = FilterBox.createFilterDoc()));
- }
-
- /// creates a new, empty filter doc
- static createFilterDoc() {
- const clearAll = `getProto(self).data = new List([])`;
- const reqdOpts: DocumentOptions = {
- _lockedPosition: true,
- _autoHeight: true,
- _fitWidth: true,
- _height: 150,
- _xPadding: 5,
- _yPadding: 5,
- _gridGap: 5,
- _forceActive: true,
- title: 'Unnamed Filter',
- filterBoolean: 'AND',
- boxShadow: '0 0',
- childDontRegisterViews: true,
- targetDropAction: 'same',
- ignoreClick: true,
- system: true,
- childDropAction: 'none',
- treeViewHideTitle: true,
- treeViewTruncateTitleWidth: 150,
- childContextMenuLabels: new List<string>(['Clear All']),
- childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]),
- };
- return Docs.Create.FilterDocument(reqdOpts);
- }
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(FilterBox, fieldKey);
- }
-
- public _filterSelected = false;
- public _filterMatch = 'matched';
-
- @computed static get currentContainingCollectionDoc() {
- let docView: any = SelectionManager.Views()[0];
- let doc = docView.Document;
-
- while (docView && doc && doc.type !== 'collection') {
- doc = docView.props.ContainingCollectionDoc;
- docView = docView.props.ContainingCollectionView;
- }
-
- return doc;
- }
-
- // /**
- // * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
- // */
-
- // trying to get this to work rather than the version below this
-
- // @computed static get targetDoc() {
- // console.log(FilterBox.currentContainingCollectionDoc.type);
- // if (FilterBox._filterScope === "Current Collection") {
- // return FilterBox.currentContainingCollectionDoc;
- // }
- // else return CollectionDockingView.Instance.props.Document;
- // // return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document;
- // }
-
- /**
- * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
- */
- @computed static get targetDoc() {
- return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : Doc.ActiveDashboard;
- }
- @computed static get targetDocChildKey() {
- if (SelectionManager.Views().length) {
- return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || 'data';
- }
- return 'data';
- }
- @computed static get targetDocChildren() {
- return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || Doc.ActiveDashboard?.data);
- }
-
- @observable _loaded = false;
- componentDidMount() {
- reaction(
- () => DocListCastAsync(this.layoutDoc[this.fieldKey]),
- async activeTabsAsync => {
- const activeTabs = await activeTabsAsync;
- activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction));
- runInAction(() => (this._loaded = true));
- },
- { fireImmediately: true }
- );
- }
-
- @computed get allDocs() {
- // trace();
- const allDocs = new Set<Doc>();
- const targetDoc = FilterBox.targetDoc;
- if (this._loaded && targetDoc) {
- SearchBox.foreachRecursiveDoc(FilterBox.targetDocChildren, (depth, doc) => allDocs.add(doc));
- }
- return Array.from(allDocs);
- }
-
- @computed get _allFacets() {
- // trace();
- const noviceReqFields = ['author', 'tags', 'text', 'type'];
- const noviceLayoutFields: string[] = []; //["_curPage"];
- const noviceFields = [...noviceReqFields, ...noviceLayoutFields];
-
- const keys = new Set<string>(noviceFields);
- this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key)));
- return Array.from(keys.keys())
- .filter(key => key[0])
- .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode)
- .sort();
- }
-
- /**
- * The current attributes selected to filter based on
- */
- @computed get activeAttributes() {
- return DocListCast(this.dataDoc[this.props.fieldKey]);
- }
-
- /**
- * @returns a string array of the current attributes
- */
- @computed get currentFacets() {
- return this.activeAttributes.map(attribute => StrCast(attribute.title));
- }
-
- gatherFieldValues(childDocs: Doc[], facetKey: string) {
- const valueSet = new Set<string>();
- let rtFields = 0;
- childDocs.forEach(d => {
- const facetVal = d[facetKey];
- if (facetVal instanceof RichTextField) rtFields++;
- valueSet.add(Field.toString(facetVal as Field));
- const fieldKey = Doc.LayoutFieldKey(d);
- const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
- const data = d[annos ? fieldKey + '-annotations' : fieldKey];
- if (data !== undefined) {
- let subDocs = DocListCast(data);
- if (subDocs.length > 0) {
- let newarray: Doc[] = [];
- while (subDocs.length > 0) {
- newarray = [];
- subDocs.forEach(t => {
- const facetVal = t[facetKey];
- if (facetVal instanceof RichTextField) rtFields++;
- facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field));
- const fieldKey = Doc.LayoutFieldKey(t);
- const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
- DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
- });
- subDocs = newarray;
- }
- }
- }
- });
- return { strings: Array.from(valueSet.keys()), rtFields };
- }
- removeFilterDoc = (doc: Doc | Doc[]) => ((doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false);
- public removeFilter = (filterName: string) => {
- const targetDoc = FilterBox.targetDoc;
- if (targetDoc) {
- const filterDoc = targetDoc.currentFilter as Doc;
- const attributes = DocListCast(filterDoc.data);
- const found = attributes.findIndex(doc => doc.title === filterName);
- if (found !== -1) {
- (filterDoc.data as List<Doc>).splice(found, 1);
- const docFilter = Cast(targetDoc._docFilters, listSpec('string'));
- if (docFilter) {
- let index: number;
- while ((index = docFilter.findIndex(item => item.split(':')[0] === filterName)) !== -1) {
- docFilter.splice(index, 1);
- }
- }
- const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec('string'));
- if (docRangeFilters) {
- let index: number;
- while ((index = docRangeFilters.findIndex(item => item.split(':')[0] === filterName)) !== -1) {
- docRangeFilters.splice(index, 3);
- }
- }
- }
- }
- return true;
- };
- /**
- * Responds to clicking the check box in the flyout menu
- */
- facetClick = (facetHeader: string) => {
- const { targetDoc, targetDocChildren } = FilterBox;
- if (!targetDoc) return;
- const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader);
- if (found !== -1) {
- this.removeFilter(facetHeader);
- } else {
- const allCollectionDocs = targetDocChildren;
- const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader);
-
- let nonNumbers = 0;
- let minVal = Number.MAX_VALUE,
- maxVal = -Number.MAX_VALUE;
- facetValues.strings.map(val => {
- const num = val ? Number(val) : Number.NaN;
- if (Number.isNaN(num)) {
- val && nonNumbers++;
- } else {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
- }
- });
- let newFacet: Opt<Doc>;
- if (facetHeader === 'text' || facetValues.rtFields / allCollectionDocs.length > 0.1) {
- newFacet = Docs.Create.TextDocument('', {
- title: facetHeader,
- system: true,
- target: targetDoc,
- _width: 100,
- _height: 25,
- _stayInCollection: true,
- treeViewExpandedView: 'layout',
- _treeViewOpen: true,
- _forceActive: true,
- ignoreClick: true,
- });
- Doc.GetProto(newFacet).forceActive = true; // required for FormattedTextBox to be able to gain focus since it will never be 'selected'
- Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
- const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
- newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: 'string' });
- } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
- newFacet = Docs.Create.SliderDocument({
- title: facetHeader,
- system: true,
- target: targetDoc,
- _fitWidth: true,
- _height: 40,
- _stayInCollection: true,
- treeViewExpandedView: 'layout',
- _treeViewOpen: true,
- _forceActive: true,
- _overflow: 'visible',
- });
- const newFacetField = Doc.LayoutFieldKey(newFacet);
- const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader);
- Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
- const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
- newFacet[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0];
- newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1];
- Doc.GetProto(newFacet)[newFacetField + '-minThumb'] = extendedMinVal;
- Doc.GetProto(newFacet)[newFacetField + '-maxThumb'] = extendedMaxVal;
- const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`;
- newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: 'number' });
- newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
- } else {
- newFacet = new Doc();
- newFacet.system = true;
- newFacet.title = facetHeader;
- newFacet.treeViewOpen = true;
- newFacet.layout = CollectionView.LayoutString('data');
- newFacet.layoutKey = 'layout';
- newFacet.type = DocumentType.COL;
- newFacet.target = targetDoc;
- newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
- }
- newFacet.hideContextMenu = true;
- Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet);
- }
- };
-
- @computed get scriptField() {
- const scriptText = 'setDocFilter(this?.target, heading, this.title, checked)';
- const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name });
- return script ? () => script : undefined;
- }
-
- /**
- * Sets whether filters are ANDed or ORed together
- */
- @action
- changeBool = (e: any) => {
- FilterBox.targetDoc && (DocCast(FilterBox.targetDoc.currentFilter).filterBoolean = e.currentTarget.value);
- };
-
- /**
- * Changes whether to select matched or unmatched documents
- */
- @action
- changeMatch = (e: any) => {
- this._filterMatch = e.currentTarget.value;
- };
-
- @action
- changeSelected = () => {
- if (this._filterSelected) {
- this._filterSelected = false;
- SelectionManager.DeselectAll();
- } else {
- this._filterSelected = true;
- // helper method to select specified docs
- }
- };
-
- FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) {
- switch (property.split(':')[0]) {
- case StyleProp.Decorations:
- if (doc && !doc.treeViewHideHeaderFields) {
- return (
- <>
- <div style={{ marginRight: '5px', fontSize: '10px' }}>
- <select className="filterBox-selection">
- <option value="Is" key="Is">
- Is
- </option>
- <option value="Is Not" key="Is Not">
- Is Not
- </option>
- </select>
- </div>
- <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}>
- <FontAwesomeIcon icon={'times'} size="sm" />
- </div>
- </>
- );
- }
- }
- return DefaultStyleProvider(doc, props, property);
- }
-
- suppressChildClick = () => ScriptField.MakeScript('')!;
-
- /**
- * Adds a filterDoc to the list of saved filters
- */
- saveFilter = () => {
- Doc.AddDocToList(Doc.UserDoc(), 'savedFilters', this.props.Document);
- };
-
- /**
- * Changes the title of the filterDoc
- */
- onTitleValueChange = (val: string) => {
- Doc.GetProto(this.props.Document).title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`;
- return true;
- };
-
- /**
- * The flyout from which you can select a saved filter to apply
- */
- @computed get flyoutPanel() {
- return DocListCast(Doc.UserDoc().savedFilters).map(doc => {
- return (
- <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 20, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}>
- {StrCast(doc.title)}
- </div>
- );
- });
- }
- setTreeHeight = (hgt: number) => {
- this.layoutDoc._height = NumCast(this.layoutDoc._autoHeightMargins) + 150; // need to add all the border sizes together.
- };
-
- /**
- * add lock and hide button decorations for the "Dashboards" flyout TreeView
- */
- FilterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
- if (property.split(':')[0] === StyleProp.Decorations) {
- return !doc || doc.treeViewHideHeaderFields ? null : DashboardToggleButton(doc, 'hidden', 'trash', 'trash', () => this.removeFilter(StrCast(doc.title)));
- }
- return this.props.styleProvider?.(doc, props, property);
- };
-
- layoutHeight = () => this.layoutDoc[HeightSym]();
- render() {
- const facetCollection = this.props.Document;
-
- const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
- return this.props.dontRegisterView ? null : (
- <div className="filterBox-treeView" style={{ width: '100%' }}>
- <div className="filterBox-title">
- <EditableView key="editableView" contents={this.props.Document.title} height={24} fontSize={15} GetValue={() => StrCast(this.props.Document.title)} SetValue={this.onTitleValueChange} />
- </div>
-
- <div className="filterBox-select-bool">
- <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc?.currentFilter as Doc)?.filterBoolean)}>
- <option value="AND" key="AND">
- AND
- </option>
- <option value="OR" key="OR">
- OR
- </option>
- </select>
- <div className="filterBox-select-text">filters together</div>
- </div>
-
- <div className="filterBox-select">
- <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} />
- </div>
-
- <div className="filterBox-tree" key="tree">
- <CollectionTreeView
- Document={facetCollection}
- DataDoc={Doc.GetProto(facetCollection)}
- fieldKey={this.props.fieldKey}
- CollectionView={undefined}
- disableDocBrushing={true}
- setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in.
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- childDocumentsActive={returnTrue}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={this.layoutHeight}
- rootSelected={this.props.rootSelected}
- renderDepth={1}
- dropAction={this.props.dropAction}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- isAnyChildContentActive={returnFalse}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- isSelected={returnFalse}
- select={returnFalse}
- bringToFront={emptyFunction}
- isContentActive={returnTrue}
- whenChildContentsActiveChanged={returnFalse}
- treeViewHideTitle={true}
- focus={returnFalse}
- onCheckedClick={this.scriptField}
- treeViewHideHeaderFields={false}
- dontRegisterView={true}
- styleProvider={this.FilterStyleProvider}
- docViewPath={this.props.docViewPath}
- scriptContext={this.props.scriptContext}
- moveDocument={returnFalse}
- removeDocument={this.removeFilterDoc}
- addDocument={returnFalse}
- />
- </div>
- <div className="filterBox-bottom">
- {/* <div className="filterBox-select-matched">
- <input className="filterBox-select-box" type="checkbox"
- onChange={this.changeSelected} />
- <div className="filterBox-select-text">select</div>
- <select className="filterBox-selection" onChange={e => this.changeMatch(e)}>
- <option value="matched" key="matched">matched</option>
- <option value="unmatched" key="unmatched">unmatched</option>
- </select>
- <div className="filterBox-select-text">documents</div>
- </div> */}
-
- <div style={{ display: 'flex' }}>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark" onPointerDown={this.saveFilter}>
- <div>SAVE</div>
- </div>
- </div>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark">
- <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}>
- <div>FILTERS</div>
- </Flyout>
- </div>
- </div>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark" onPointerDown={this.props.createNewFilterDoc}>
- <div>NEW</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
- const docFilters = Cast(layoutDoc._docFilters, listSpec('string'), []);
- for (const filter of docFilters) {
- const fields = filter.split(':'); // split into key:value:modifiers
- if (fields[0] === facetHeader && fields[1] === facetValue) {
- return fields[2];
- }
- }
- return undefined;
-});
-ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, childKey: string, facetHeader: string) {
- const allCollectionDocs = new Set<Doc>();
- const activeTabs = DocListCast(layoutDoc[childKey]);
- SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>();
- if (facetHeader === 'tags')
- allCollectionDocs.forEach(child =>
- Field.toString(child[facetHeader] as Field)
- .split(':')
- .forEach(key => set.add(key))
- );
- else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
- const facetValues = Array.from(set).filter(v => v);
-
- let minVal = Number.MAX_VALUE,
- maxVal = -Number.MAX_VALUE;
- facetValues.map(val => {
- const num = val ? Number(val) : Number.NaN;
- if (!Number.isNaN(num)) {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
- }
- });
- const newFacetField = Doc.LayoutFieldKey(facetDoc);
- const ranged = Doc.readDocRangeFilter(layoutDoc, facetHeader);
- const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
- const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
- facetDoc[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0];
- facetDoc[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1];
- Doc.GetProto(facetDoc)[newFacetField + '-minThumb'] = extendedMinVal;
- Doc.GetProto(facetDoc)[newFacetField + '-maxThumb'] = extendedMaxVal;
-});
-ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) {
- const allCollectionDocs = new Set<Doc>();
- const activeTabs = DocListCast(layoutDoc[childKey]);
- SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>();
- if (facetHeader === 'tags')
- allCollectionDocs.forEach(child =>
- Field.toString(child[facetHeader] as Field)
- .split(':')
- .forEach(key => set.add(key))
- );
- else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
- const facetValues = Array.from(set).filter(v => v);
-
- let nonNumbers = 0;
-
- facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++);
- const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => {
- const doc = new Doc();
- doc.system = true;
- doc.title = facetValue.toString();
- doc.target = layoutDoc;
- doc.facetHeader = facetHeader;
- doc.facetValue = facetValue;
- doc.treeViewHideHeaderFields = true;
- doc.treeViewChecked = ComputedField.MakeFunction('determineCheckedState(self.target, self.facetHeader, self.facetValue)');
- return doc;
- });
- return new List<Doc>(facetValueDocSet);
-});
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 5c0005dae..1a78583f9 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -13,8 +13,9 @@ import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
-import { DocFocusOptions } from './DocumentView';
+import { DocFocusOptions, DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
const EquationSchema = createSchema({});
@@ -37,23 +38,21 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
componentDidMount() {
this.props.setContentView?.(this);
reaction(
- () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+ () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.rootDoc.xRange, this.rootDoc.yRange],
() => this.createGraph()
);
}
- getAnchor = (addAsAnnotation: boolean) => {
- const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc });
- anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
- anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.FunctionPlotConfigDocument({
+ //
+ annotationOn: this.rootDoc,
+ });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc);
+ anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
+ anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
if (addAsAnnotation) this.addDocument(anchor);
return anchor;
};
- @action
- scrollFocus = (doc: Doc, smooth: DocFocusOptions) => {
- this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]))));
- this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]))));
- return 0;
- };
createGraph = (ele?: HTMLDivElement) => {
this._plotEle = ele || this._plotEle;
const width = this.props.PanelWidth();
@@ -65,8 +64,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
target: '#' + this._plotEle.id,
width,
height,
- xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) },
- yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
+ xAxis: { domain: Cast(this.rootDoc.xRange, listSpec('number'), [-10, 10]) },
+ yAxis: { domain: Cast(this.rootDoc.yRange, listSpec('number'), [-1, 9]) },
grid: true,
data: fns.map(fn => ({
fn,
@@ -94,7 +93,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
}
- // if (this.autoHeight) this.tryUpdateScrollHeight();
+ // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
@computed get theGraph() {
return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
@@ -105,7 +104,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
<div
ref={this.createDropTarget}
style={{
- pointerEvents: !this.isContentActive() ? 'all' : undefined,
+ pointerEvents: !this.props.isContentActive() ? 'all' : undefined,
width: this.props.PanelWidth(),
height: this.props.PanelHeight(),
}}>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 6359a9491..29943e156 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -2,7 +2,9 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: relative;
+ position: absolute;
+ top: 0;
+ left: 0;
transform-origin: top left;
.imageBox-annotationLayer {
@@ -119,6 +121,14 @@
}
}
}
+.imageBox-alternateDropTarget {
+ position: absolute;
+ color: white;
+ background: black;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+}
.imageBox-fader img {
position: absolute;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index e2ecca0b6..068d39391 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,3 +1,5 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
@@ -8,16 +10,18 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { createSchema } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
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 { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../../views/ContextMenu';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -26,12 +30,13 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import './ImageBox.scss';
+import { PinProps, PresBox } from './trails';
import React = require('react');
-import { PresBox } from './trails';
-import { DocFocusOptions, DocumentViewProps } from './DocumentView';
+import Color = require('color');
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -50,9 +55,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
+ private _ignoreScroll = false;
+ private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
+ private _overlayIconRef = React.createRef<HTMLDivElement>();
@observable _curSuffix = '';
@observable _uploadIcon = uploadIcons.idle;
@@ -66,33 +74,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
};
- @action
- scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
- const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
- return PresBox.restoreTargetDocView(
- this.props.DocumentView?.(), //
- { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
- anchor,
- focusSpeed,
- !anchor.presPinData
- ? {}
- : {
- pannable: true,
- dataannos: anchor.presAnnotations !== undefined,
- dataview: true,
- }
- )
- ? focusSpeed
- : undefined;
- }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
-
- getAnchor = (addAsAnnotation: boolean) => {
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const anchor =
this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
- Docs.Create.ImageanchorDocument({ presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+ Docs.Create.ImageConfigDocument({
+ title: 'ImgAnchor:' + this.rootDoc.title,
+ presPanX: NumCast(this.layoutDoc._freeform_panX),
+ presPanY: NumCast(this.layoutDoc._freeform_panY),
+ presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null),
+ presTransition: 1000,
+ annotationOn: this.rootDoc,
+ });
if (anchor) {
- PresBox.pinDocView(anchor, { pinData: { pannable: true, dataview: true, dataannos: true } }, this.rootDoc);
- addAsAnnotation && this.addDocument(anchor);
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ /* addAsAnnotation &&*/ this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc);
return anchor;
}
return this.rootDoc;
@@ -102,7 +98,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._disposers.sizer = reaction(
() => ({
forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
- scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1),
+ scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._freeform_scale, 1),
selected: this.props.isSelected(),
}),
({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
@@ -118,6 +114,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.layoutDoc.layout_scrollTop,
+ s_top => {
+ this._forcedScroll = true;
+ !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
+ this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+ this._forcedScroll = false;
+ },
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -128,10 +134,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- if (de.metaKey) {
+ const targetIsBullseye = (ele: HTMLElement): boolean => {
+ if (!ele) return false;
+ if (ele === this._overlayIconRef.current) return true;
+ return targetIsBullseye(ele.parentElement as HTMLElement);
+ };
+ if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) {
de.complete.docDragData.droppedDocuments.forEach(
action((drop: Doc) => {
Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
+ this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
e.stopPropagation();
})
);
@@ -151,18 +163,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@undoBatch
resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
- @undoBatch
- setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']);
@undoBatch
+ setNativeSize = action(() => {
+ const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1);
+ const nscale = NumCast(this.props.PanelWidth()) / scaling;
+ const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
+ const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nh;
+ this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nh;
+ this.rootDoc._freeform_panX = nh * NumCast(this.rootDoc._freeform_panX);
+ this.rootDoc._freeform_panY = nh * NumCast(this.rootDoc._freeform_panY);
+ this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nh * NumCast(this.dataDoc._freeform_panXMax) : undefined;
+ this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nh * NumCast(this.dataDoc._freeform_panXMin) : undefined;
+ this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined;
+ this.dataDoc._freeform_panYMin = this.dataDoc._freeform_panYMin ? nw * NumCast(this.dataDoc._freeform_panYMin) : undefined;
+ });
+ @undoBatch
rotate = action(() => {
- const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
- const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
+ const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
const w = this.layoutDoc._width;
const h = this.layoutDoc._height;
this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360;
- this.dataDoc[this.fieldKey + '-nativeWidth'] = nh;
- this.dataDoc[this.fieldKey + '-nativeHeight'] = nw;
+ this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
+ this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
this.layoutDoc._width = h;
this.layoutDoc._height = w;
});
@@ -178,33 +203,38 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw;
cropping.title = 'crop: ' + this.rootDoc.title;
cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
+ croppingProto.backgroundColor = undefined;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.IMG;
croppingProto.layout = ImageBox.LayoutString('data');
croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
- croppingProto.viewScale = viewScale;
- croppingProto.viewScaleMin = viewScale;
- croppingProto.panX = anchx / viewScale;
- croppingProto.panY = anchy / viewScale;
- croppingProto.panXMin = anchx / viewScale;
- croppingProto.panXMax = anchw / viewScale;
- croppingProto.panYMin = anchy / viewScale;
- croppingProto.panYMax = anchh / viewScale;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
+ croppingProto.freeform_scale = viewScale;
+ croppingProto.freeform_scaleMin = viewScale;
+ croppingProto.freeform_panX = anchx / viewScale;
+ croppingProto.freeform_panY = anchy / viewScale;
+ croppingProto.freeform_panXMin = anchx / viewScale;
+ croppingProto.freeform_panXMax = anchw / viewScale;
+ croppingProto.freeform_panYMin = anchy / viewScale;
+ croppingProto.freeform_panYMax = anchh / viewScale;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
+ cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
+ cropping.y = NumCast(this.rootDoc.y);
+ this.props.addDocTab(cropping, OpenWhere.inParent);
}
+ DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
this.props.bringToFront(cropping);
return cropping;
};
@@ -213,10 +243,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' });
- funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' });
- funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, event: this.setUseAlt, icon: 'expand-arrows-alt' });
- funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
+ funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
+ funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
+ funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
+ funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -281,6 +311,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
};
+ getScrollHeight = () => (this.props.layout_fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._freeform_scale, 1) === NumCast(this.rootDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+
@computed
private get considerDownloadIcon() {
const data = this.dataDoc[this.fieldKey];
@@ -325,11 +357,46 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get nativeSize() {
TraceMobx();
- const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth'], 500));
- const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 1));
+ const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
+ const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
+ @computed get overlayImageIcon() {
+ const usePath = this.rootDoc[`_${this.fieldKey}_usePath`];
+ return (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">
+ toggle between
+ <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+ <em> primary, </em>
+ </span>
+ <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+ <em>alternate, </em>
+ </span>
+ and show
+ <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+ <em> alternate on hover</em>
+ </span>
+ </div>
+ }>
+ <div
+ className="imageBox-alternateDropTarget"
+ ref={this._overlayIconRef}
+ onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))}
+ style={{
+ display: (SnappingManager.GetIsDragging() && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none',
+ width: 'min(10%, 25px)',
+ height: 'min(10%, 25px)',
+ background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
+ color: usePath === undefined ? 'black' : 'white',
+ }}>
+ <FontAwesomeIcon icon="turn-up" size="lg" />
+ </div>
+ </Tooltip>
+ );
+ }
@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
@@ -339,14 +406,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
.filter(url => url)
.map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
- return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+ return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')];
}
+ @observable _isHovering = false; // flag to switch between primary and alternate images on hover
@computed get content() {
TraceMobx();
- const srcpath = this.paths[0];
- const fadepath = this.paths.lastElement();
+ const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
+ const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
+ const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
@@ -361,15 +430,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
transformOrigin = 'right top';
transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
}
+ const usePath = this.rootDoc[`_${this.fieldKey}_usePath`];
return (
- <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
- <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
+ <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
+ <div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
{fadepath === srcpath ? null : (
- <div
- className={`imageBox-fadeBlocker${(this.props.isHovering?.() && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`}
- style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
+ <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
<img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
</div>
)}
@@ -377,13 +445,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{!Doc.noviceMode && this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
<FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} />
+ {this.overlayImageIcon}
</div>
);
}
- screenToLocalTransform = this.props.ScreenToLocalTransform;
- contentFunc = () => [this.content];
-
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@observable _marqueeing: number[] | undefined;
@@ -392,8 +458,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
TraceMobx();
return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
}
+ screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.props.ScreenToLocalTransform().Scale);
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._freeform_scale, 1) <= NumCast(this.rootDoc.freeform_scaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -414,12 +481,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._marqueeing = undefined;
this.props.select(false);
};
- savedAnnotations = () => this._savedAnnotations;
- styleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
- if (property === StyleProp.BoxShadow) return undefined;
- return this.props.styleProvider?.(doc, props, property);
- };
+ focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options);
+ _ffref = React.createRef<CollectionFreeFormView>();
+ savedAnnotations = () => this._savedAnnotations;
render() {
TraceMobx();
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -429,30 +494,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
className="imageBox"
onContextMenu={this.specificContextMenu}
ref={this._mainCont}
+ onScroll={action(e => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop;
+ this._ignoreScroll = false;
+ }
+ }
+ })}
style={{
+ display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
width: this.props.PanelWidth() ? undefined : `100%`,
height: this.props.PanelWidth() ? undefined : `100%`,
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this.props.layout_fitWidth?.(this.rootDoc) ? 'auto' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ ref={this._ffref}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
- styleProvider={this.styleProvider}
- CollectionView={undefined}
+ styleProvider={this.props.styleProvider}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
ScreenToLocalTransform={this.screenToLocalTransform}
select={emptyFunction}
+ focus={this.focus}
+ getScrollHeight={this.getScrollHeight}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
{this.annotationLayer}
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 60417430f..88a82e8e6 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -3,35 +3,37 @@ import { observer } from 'mobx-react';
import { Doc, Field, FieldResult } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
-import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types';
+import { DocCast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
+import { returnAll, returnAlways, returnTrue } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
-import { FieldView, FieldViewProps } from './FieldView';
-import './KeyValueBox.scss';
-import { KeyValuePair } from './KeyValuePair';
-import React = require('react');
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import e = require('express');
+import { OpenWhere } from './DocumentView';
+import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { ImageBox } from './ImageBox';
-import { OpenWhere } from './DocumentView';
+import './KeyValueBox.scss';
+import { KeyValuePair } from './KeyValuePair';
+import React = require('react');
+import { DocumentManager } from '../../util/DocumentManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ScriptingRepl } from '../ScriptingRepl';
+import { DocumentIconContainer } from './DocumentIcon';
export type KVPScript = {
script: CompiledScript;
type: 'computed' | 'script' | false;
onDelegate: boolean;
};
-
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string) {
- return FieldView.LayoutString(KeyValueBox, fieldStr);
+ public static LayoutString() {
+ return FieldView.LayoutString(KeyValueBox, 'data');
}
private _mainCont = React.createRef<HTMLDivElement>();
@@ -39,13 +41,20 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
private _keyInput = React.createRef<HTMLInputElement>();
private _valInput = React.createRef<HTMLInputElement>();
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+ reverseNativeScaling = returnTrue;
+ able = returnAlways;
+ layout_fitWidth = returnTrue;
+ overridePointerEvents = returnAll;
+ onClickScriptDisable = returnAlways;
+
@observable private rows: KeyValuePair[] = [];
+ @observable _splitPercentage = 50;
- @computed get splitPercentage() {
- return NumCast(this.props.Document.schemaSplitPercentage, 50);
- }
get fieldDocToLayout() {
- return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document;
+ return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document;
}
@action
@@ -63,12 +72,12 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
};
public static CompileKVPScript(value: string): KVPScript | undefined {
const eq = value.startsWith('=');
- value = eq ? value.substr(1) : value;
- const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false;
- value = dubEq ? value.substr(2) : value;
- const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false };
+ 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);
+ const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
}
@@ -82,11 +91,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
} else if (type === 'script') {
field = new ScriptField(script);
} else {
- const res = script.run({ this: target }, console.log);
- if (!res.success) return false;
+ const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
+ if (!res.success) {
+ target[key] = script.originalScript;
+ return true;
+ }
field = res.result;
}
- if (Field.IsField(field, true)) {
+ if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
target[key] = field;
return true;
}
@@ -133,7 +145,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
const rows: JSX.Element[] = [];
let i = 0;
const self = this;
- for (const key of Object.keys(ids).slice().sort()) {
+ const keys = Object.keys(ids).slice();
+ //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) {
+ for (const key of keys.sort()) {
rows.push(
<KeyValuePair
doc={realDoc}
@@ -148,7 +162,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
if (el) self.rows.push(el);
};
})()}
- keyWidth={100 - this.splitPercentage}
+ keyWidth={100 - this._splitPercentage}
rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')}
key={key}
keyName={key}
@@ -166,7 +180,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
this._keyInput.current!.select();
e.stopPropagation();
}}
- style={{ width: `${100 - this.splitPercentage}%` }}>
+ style={{ width: `${100 - this._splitPercentage}%` }}>
<input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" />
</td>
<td
@@ -175,7 +189,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
this._valInput.current!.select();
e.stopPropagation();
}}
- style={{ width: `${this.splitPercentage}%` }}>
+ style={{ width: `${this._splitPercentage}%` }}>
<input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} />
</td>
</tr>
@@ -185,7 +199,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onDividerMove = (e: PointerEvent): void => {
const nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
+ this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
};
@action
onDividerUp = (e: PointerEvent): void => {
@@ -202,7 +216,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
getFieldView = async () => {
const rows = this.rows.filter(row => row.isChecked);
if (rows.length > 1) {
- const parent = Docs.Create.StackingDocument([], { _autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true });
+ const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true });
for (const row of rows) {
const field = this.createFieldView(DocCast(this.props.Document.data), row);
field && Doc.AddDocToList(parent, 'data', field);
@@ -215,9 +229,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
const metaKey = row.props.keyName;
- const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeAlias(templateDoc);
+ const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
fieldTemplate.title = metaKey;
- fieldTemplate.fitWidth = true;
+ fieldTemplate.layout_fitWidth = true;
fieldTemplate._xMargin = 10;
fieldTemplate._yMargin = 10;
fieldTemplate._width = 100;
@@ -270,8 +284,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
render() {
const dividerDragger =
- this.splitPercentage === 0 ? null : (
- <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
+ this._splitPercentage === 0 ? null : (
+ <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this._splitPercentage}% - 5px), 0px)` }}>
<div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
</div>
);
@@ -281,10 +295,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<table className="keyValueBox-table">
<tbody className="keyValueBox-tbody">
<tr className="keyValueBox-header">
- <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
+ <th className="keyValueBox-key" style={{ width: `${100 - this._splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
Key
</th>
- <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>
+ <th className="keyValueBox-fields" style={{ width: `${this._splitPercentage}%` }}>
Fields
</th>
</tr>
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 5b660e582..57d36932e 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -1,60 +1,52 @@
-@import "../global/globalCssVariables";
-
+@import '../global/globalCssVariables';
.keyValuePair-td-key {
- display:inline-block;
+ display: inline-block;
- .keyValuePair-td-key-container{
- width:100%;
- height:100%;
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- justify-content: space-between;
- align-items: center;
- .keyValuePair-td-key-delete{
+ .keyValuePair-td-key-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ .keyValuePair-td-key-delete {
position: relative;
background-color: transparent;
- color:red;
+ color: red;
}
.keyValuePair-td-key-check {
position: relative;
margin: 0;
}
.keyValuePair-keyField {
- width:100%;
+ width: 100%;
margin-left: 20px;
- margin-top: -1px;
font-family: monospace;
- // text-align: center;
- align-self: center;
position: relative;
overflow: auto;
+ display: inline;
}
}
}
.keyValuePair-td-value {
- display:inline-block;
+ display: inline-block;
overflow: scroll;
font-family: monospace;
height: 30px;
- .keyValuePair-td-value-container {
- display: flex;
- align-items: center;
- align-content: center;
- flex-direction: row;
- justify-content: space-between;
- flex-wrap: nowrap;
- width: 100%;
- height: 100%;
+ .keyValuePair-td-value-container {
+ display: inline;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
- img {
- max-height: 36px;
- width: auto;
- }
- .videoBox-cont{
- width: auto;
- max-height: 36px;
- }
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont {
+ width: auto;
+ max-height: 36px;
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index e74ef4a39..91df928c4 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,19 +1,18 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Field, Opt } from '../../../fields/Doc';
-import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Doc, Field } from '../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { EditableView } from '../EditableView';
+import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { KeyValueBox } from './KeyValueBox';
import './KeyValueBox.scss';
import './KeyValuePair.scss';
import React = require('react');
-import { DefaultStyleProvider } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
// Represents one row in a key value plane
@@ -48,7 +47,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
if (value instanceof Doc) {
e.stopPropagation();
e.preventDefault();
- ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
+ ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, ((OpenWhere.addRight as string) + 'KeyValue') as OpenWhere), icon: 'layer-group' });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
};
@@ -62,14 +61,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
searchFilterDocs: returnEmptyDoclist,
styleProvider: DefaultStyleProvider,
docViewPath: returnEmptyDoclist,
- ContainingCollectionView: undefined,
- ContainingCollectionDoc: undefined,
fieldKey: this.props.keyName,
rootSelected: returnFalse,
isSelected: returnFalse,
setHeight: returnFalse,
select: emptyFunction,
- dropAction: 'alias',
+ dropAction: 'embed',
bringToFront: emptyFunction,
renderDepth: 1,
isContentActive: returnFalse,
@@ -111,8 +108,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
})}>
X
</button>
- <input className={'keyValuePair-td-key-check'} type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
- <div className="keyValuePair-keyField" style={{ color: keyStyle }}>
+ <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
+ <div className='keyValuePair-keyField' style={{ marginLeft: 35 * (props.fieldKey.match(/_/g)?.length ||0), color: keyStyle }}>
{'('.repeat(parenCount)}
{props.fieldKey}
{')'.repeat(parenCount)}
@@ -121,13 +118,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
<div className="keyValuePair-td-value-container">
- <EditableView
- contents={contents}
- maxHeight={36}
- height={'auto'}
- GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)}
- SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)}
- />
+ <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} />
</div>
</td>
</tr>
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index a2143f629..32026ea9c 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -36,10 +36,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
this._timeout && clearTimeout(this._timeout);
}
- getAnchor = (addAsAnnotation: boolean) => this.rootDoc;
-
getTitle() {
- return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
+ return this.rootDoc.title_custom ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
}
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -81,10 +79,10 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
@observable _mouseOver = false;
@computed get hoverColor() {
- return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset';
+ return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
}
- fitTextToBox = (r: any): any => {
+ fitTextToBox = (r: any) => {
const singleLine = BoolCast(this.rootDoc._singleLine, true);
const params = {
rotateText: null,
@@ -133,6 +131,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
style={{
backgroundColor: this.hoverColor,
fontSize: StrCast(this.layoutDoc._fontSize),
+ color: StrCast(this.layoutDoc._color),
fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
@@ -142,9 +141,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
paddingBottom: NumCast(this.rootDoc._yPadding),
width: this.props.PanelWidth(),
height: this.props.PanelHeight(),
- whiteSpace: boxParams.singleLine ? 'pre' : 'pre-wrap',
+ whiteSpace: typeof boxParams !== 'number' && boxParams.singleLine ? 'pre' : 'pre-wrap',
}}>
- <span style={{ width: boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}>
+ <span style={{ width: typeof boxParams !== 'number' && boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}>
{label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')}
</span>
</div>
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index e89076c1f..f38ef634c 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,22 +1,19 @@
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
+import { SelectionManager } from '../../util/SelectionManager';
import { ViewBoxBaseComponent } from '../DocComponent';
import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkAnchorBox.scss';
import { LinkDocPreview } from './LinkDocPreview';
import React = require('react');
-import { LinkManager } from '../../util/LinkManager';
import globalCssVariables = require('../global/globalCssVariables.scss');
-import { SelectionManager } from '../../util/SelectionManager';
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -31,19 +28,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _x = 0;
@observable _y = 0;
+ @computed get linkSource() {
+ return this.props.docViewPath()[this.props.docViewPath().length - 2].rootDoc; // this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
+ }
+
onPointerDown = (e: React.PointerEvent) => {
- const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
- setupMoveUpEvents(
- this,
- e,
- this.onPointerMove,
- emptyFunction,
- (e, doubleTap) => {
- if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
- else this.props.select(false);
- },
- false
- );
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
+ if (doubleTap) LinkFollower.FollowLink(this.rootDoc, this.linkSource, false);
+ else this.props.select(false);
+ });
};
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
const cdiv = this._ref?.current?.parentElement;
@@ -53,13 +46,14 @@ 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.rootDoc]);
- dragData.dropAction = 'alias';
- dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'isLinkButton'];
+ dragData.dropAction = 'embed';
+ dragData.removeDropProperties = ['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;
} else {
this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+ this.rootDoc.layout_autoMoveAnchors = false;
}
}
return false;
@@ -72,12 +66,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView
const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100);
const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100);
- const linkSource = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
- const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1';
- const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
+ const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
+ const anchorScale = !this.dataDoc[this.fieldKey + '_useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
- const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : '';
+ const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_1')
+ ? 'link_anchor_1'
+ : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_2')
+ ? 'link_anchor_2'
+ : '';
return (
<div
ref={this._ref}
@@ -86,7 +83,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerEnter={e =>
LinkDocPreview.SetLinkInfo({
docProps: this.props,
- linkSrc: linkSource,
+ linkSrc: this.linkSource,
linkDoc: this.rootDoc,
showHeader: true,
location: [e.clientX, e.clientY + 20],
@@ -101,6 +98,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
transform: `scale(${anchorScale})`,
+ cursor: 'grab',
}}
/>
);
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 43f4b43fb..710d41471 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,29 +1,38 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import { emptyFunction, returnFalse } from "../../../Utils";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { StyleProp } from "../StyleProvider";
-import { ComparisonBox } from "./ComparisonBox";
+import React = require('react');
+import { observer } from 'mobx-react';
+import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkBox.scss";
+import './LinkBox.scss';
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
- isContentActiveFunc = () => this.isContentActive();
+ public static LayoutString(fieldKey: string = 'link') {
+ return FieldView.LayoutString(LinkBox, fieldKey);
+ }
+
+ onClickScriptDisable = returnAlways;
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
render() {
- if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
- return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
- style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} >
- <ComparisonBox {...this.props}
- fieldKey="anchor"
- setHeight={emptyFunction}
- dontRegisterView={true}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActiveFunc}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- moveDocument={returnFalse} />
- </div>;
+ if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
+ return (
+ <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
+ <ComparisonBox
+ {...this.props}
+ fieldKey="link_anchor"
+ setHeight={emptyFunction}
+ dontRegisterView={true}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.props.isContentActive}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 91bd505c5..c45045a8a 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -24,7 +24,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
onDismiss = (add: boolean) => {
LinkDescriptionPopup.descriptionPopup = false;
if (add) {
- LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description);
+ LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).link_description = this.description);
}
};
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 1eab06381..450176a49 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import wiki from 'wikijs';
-import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
import { DocServer } from '../../DocServer';
@@ -14,11 +14,10 @@ import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { SettingsManager } from '../../util/SettingsManager';
import { Transform } from '../../util/Transform';
+import { SearchBox } from '../search/SearchBox';
import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
import './LinkDocPreview.scss';
import React = require('react');
-import { SearchUtil } from '../../util/SearchUtil';
-import { SearchBox } from '../search/SearchBox';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -59,13 +58,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
var linkTarget = this.props.linkDoc;
this._linkSrc = this.props.linkSrc;
this._linkDoc = this.props.linkDoc;
- const anchor1 = this._linkDoc?.anchor1 as Doc;
- const anchor2 = this._linkDoc?.anchor2 as Doc;
- if (anchor1 && anchor2) {
- linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1;
+ const link_anchor_1 = this._linkDoc?.link_anchor_1 as Doc;
+ const link_anchor_2 = this._linkDoc?.link_anchor_2 as Doc;
+ if (link_anchor_1 && link_anchor_2) {
+ linkTarget = Doc.AreProtosEqual(link_anchor_1, this._linkSrc) || Doc.AreProtosEqual(link_anchor_1?.annotationOn as Doc, this._linkSrc) ? link_anchor_2 : link_anchor_1;
}
if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) {
- // want to show annotation context document if annotation is not text
+ // want to show annotation embedContainer document if annotation is not text
linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno)));
} else {
this._markerTargetDoc = this._targetDoc = linkTarget;
@@ -110,9 +109,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined;
anchorDoc?.then?.(
action(anchor => {
- if (anchor instanceof Doc && DocListCast(anchor.links).length) {
- this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0];
- const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords;
+ if (anchor instanceof Doc && LinkManager.Links(anchor).length) {
+ this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0];
+ const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords;
if (automaticLink) {
// automatic links specify the target in the link info, not the source
const linkTarget = anchor;
@@ -124,7 +123,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
this._markerTargetDoc = linkTarget;
this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
}
- this._toolTipText = 'link to ' + this._targetDoc?.title;
if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
}
})
@@ -172,11 +170,11 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
followLink = () => {
LinkDocPreview.Clear();
if (this._linkDoc && this._linkSrc) {
- LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
+ LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
} else if (this.props.hrefs?.length) {
const webDoc =
Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ??
- Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true });
+ Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox);
}
};
@@ -209,7 +207,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
</div>
<div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
{StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}
- <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
+ <p className="linkDocPreview-description"> {StrCast(this._linkDoc.link_description)}</p>
</div>
<div className="linkDocPreview-buttonBar" style={{ float: 'right' }}>
<Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top">
@@ -253,7 +251,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
<DocumentView
ref={r => {
const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor, {});
+ targetanchor && this._targetDoc !== targetanchor && r?.props.focus?.(targetanchor, {});
}}
Document={this._targetDoc!}
moveDocument={returnFalse}
@@ -264,7 +262,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
isDocumentActive={returnFalse}
isContentActive={returnFalse}
addDocument={returnFalse}
- showTitle={returnEmptyString}
+ layout_showTitle={returnEmptyString}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -272,16 +270,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
docFilters={returnEmptyFilter}
docRangeFilters={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={undefined}
- ContainingCollectionView={undefined}
renderDepth={0}
suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
pointerEvents={returnNone}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
- ignoreAutoHeight={true} // need to ignore autoHeight otherwise autoHeight text boxes will expand beyond the preview panel size.
+ ignoreAutoHeight={true} // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size.
bringToFront={returnFalse}
NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined}
NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined}
@@ -299,7 +295,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
className="linkDocPreview"
ref={this._linkDocRef}
onPointerDown={this.followLinkPointerDown}
- style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+ style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
{this.docPreview}
</div>
);
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 95cb49037..9b0fddce4 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,5 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
+import BingMapsReact from 'bingmaps-react';
import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -7,7 +8,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -20,6 +21,7 @@ import { AnchorMenu } from '../../pdf/AnchorMenu';
import { Annotation } from '../../pdf/Annotation';
import { SidebarAnnos } from '../../SidebarAnnos';
import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
import './MapBox.scss';
import { MapBoxInfoWindow } from './MapBoxInfoWindow';
@@ -43,20 +45,22 @@ const mapContainerStyle = {
};
const defaultCenter = {
- lat: 38.685,
- lng: -115.234,
+ lat: 42.360081,
+ lng: -71.058884,
};
const mapOptions = {
fullscreenControl: false,
};
+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 apiKey = process.env.GOOGLE_MAPS;
const script = document.createElement('script');
script.defer = true;
script.async = true;
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+console.log(script.src);
document.head.appendChild(script);
/**
@@ -84,6 +88,7 @@ const options = {
@observer
export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
+ static UseBing = true;
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -93,7 +98,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return FieldView.LayoutString(MapBox, fieldKey);
}
public get SidebarKey() {
- return this.fieldKey + '-sidebar';
+ return this.fieldKey + '_sidebar';
}
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
@computed get inlineTextAnnotations() {
@@ -121,7 +126,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _showSidebar = false;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
static _canAnnotate = true;
@@ -129,8 +134,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
- super(props);
+ componentDidMount() {
+ this.props.setContentView?.(this);
}
@action
@@ -262,7 +267,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
setTimeout(() => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
}, 250);
// listener to addmarker event
@@ -277,7 +282,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
centered = () => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
this.dataDoc.mapLat = this._map.getCenter()?.lat();
this.dataDoc.mapLng = this._map.getCenter()?.lng();
@@ -287,7 +292,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
zoomChanged = () => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
this.dataDoc.mapZoom = this._map.getZoom();
};
@@ -322,12 +327,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
*/
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
console.log('print all sidebar Docs');
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
docs.forEach(doc => {
if (doc.lat !== undefined && doc.lng !== undefined) {
const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
- doc.onClickBehavior = 'enterPortal';
if (existingMarker) {
Doc.AddDocToList(existingMarker, 'data', doc);
} else {
@@ -347,7 +351,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
* @returns
*/
sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
return this.removeDocument(doc, sidebarKey);
};
@@ -371,11 +375,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (this.sidebarWidth() + localDelta[0] > 0) {
this._showSidebar = true;
this.layoutDoc._width = fullWidth + localDelta[0];
- this.layoutDoc._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
} else {
this._showSidebar = false;
this.layoutDoc._width = mapWidth;
- this.layoutDoc._sidebarWidthPercent = '0%';
+ this.layoutDoc._layout_sidebarWidthPercent = '0%';
}
return false;
}),
@@ -384,12 +388,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
};
- sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
- @computed get sidebarWidthPercent() {
- return StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ @computed get layout_sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
/**
@@ -449,7 +453,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={this.sidebarBtnDown}>
@@ -463,8 +467,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
toggleSidebar = () => {
//1.2 * w * ? = .2 * w .2/1.2
const prevWidth = this.sidebarWidth();
- this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
- this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
+ this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
};
sidebarDown = (e: React.PointerEvent) => {
@@ -472,8 +476,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
const bounds = this._ref.current!.getBoundingClientRect();
- this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
- this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
+ this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
};
@@ -522,7 +526,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- getAnchor = (addAsAnnotation: boolean) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
/**
* render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
@@ -542,78 +546,109 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// }
};
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+ panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
infoWidth = () => this.props.PanelWidth() / 5;
infoHeight = () => this.props.PanelHeight() / 5;
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(),
+ });
+ }
+ });
+ };
+
+ bingViewOptions = {
+ center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng },
+ mapTypeId: 'grayscale',
+ };
+ bingMapOptions = {
+ navigationBarMode: 'square',
+ };
+ bingMapReady = (map: any) => (this._bingMap = map.map);
render() {
const renderAnnotations = (docFilters?: () => string[]) => null;
- // bcz: commmented this out. Otherwise, any documents that are rendered with an InfoWindow of a marker
- // will also be rendered as freeform annotations on the map. However, it doesn't seem that rendering
- // freeform documents on the map does anything anyway, so getting rid of it for now. Also, since documents
- // are rendered twice, adding a new note to the InfoWindow loses focus immediately on creation since it gets
- // shifted to the non-visible view of the document in this freeform view.
- // <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- // renderDepth={this.props.renderDepth + 1}
- // isAnnotationOverlay={true}
- // fieldKey={this.annotationKey}
- // CollectionView={undefined}
- // setPreviewCursor={this.setPreviewCursor}
- // PanelWidth={this.panelWidth}
- // PanelHeight={this.panelHeight}
- // ScreenToLocalTransform={this.scrollXf}
- // scaling={returnOne}
- // dropAction={"alias"}
- // docFilters={docFilters || this.props.docFilters}
- // dontRenderDocuments={docFilters ? false : true}
- // select={emptyFunction}
- // bringToFront={emptyFunction}
- // whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- // removeDocument={this.removeDocument}
- // moveDocument={this.moveDocument}
- // addDocument={this.sidebarAddDocument}
- // childPointerEvents={"all"}
- // pointerEvents={Doc.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
return (
<div className="mapBox" ref={this._ref}>
- {/*console.log(apiKey)*/}
- {/* <LoadScript
- googleMapsApiKey={apiKey!}
- libraries={['places', 'drawing']}
- > */}
- <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+ <div
+ className="mapBox-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={async e => {
+ e.button === 0 && !e.ctrlKey && e.stopPropagation();
+ // just a simple test of bing maps geocode api
+ // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA');
+ // this._bingMap.current.setView({
+ // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+ // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+ // zoom: 15,
+ // });
+ }}
+ style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
{this.annotationLayer}
- <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
- <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
- <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
- </Autocomplete>
-
- {this.renderMarkers()}
- {this.allMapMarkers
- .filter(marker => marker.infoWindowOpen)
- .map(marker => (
- <MapBoxInfoWindow
- key={marker[Id]}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- place={marker}
- markerMap={this.markerMap}
- PanelWidth={this.infoWidth}
- PanelHeight={this.infoHeight}
- moveDocument={this.moveDocument}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- />
- ))}
- {/* {this.handleMapCenter(this._map)} */}
- </GoogleMap>
+
+ {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />}
+ <div style={{ display: MapBox.UseBing ? 'none' : undefined }}>
+ <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
+ <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
+ <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
+ </Autocomplete>
+
+ {this.renderMarkers()}
+ {this.allMapMarkers
+ .filter(marker => marker.infoWindowOpen)
+ .map(marker => (
+ <MapBoxInfoWindow
+ key={marker[Id]}
+ {...this.props}
+ setContentView={emptyFunction}
+ place={marker}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ />
+ ))}
+ {/* {this.handleMapCenter(this._map)} */}
+ </GoogleMap>
+ </div>
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
<MarqueeAnnotator
rootDoc={this.rootDoc}
@@ -632,7 +667,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
)}
</div>
{/* </LoadScript > */}
- <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
@@ -640,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ usePanelWidth={true}
showSidebar={this.SidebarShown}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx
new file mode 100644
index 000000000..9354f9639
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox2.tsx
@@ -0,0 +1,641 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
+import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { InkTool } from '../../../../fields/InkField';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { UndoManager } from '../../../util/UndoManager';
+import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { Annotation } from '../../pdf/Annotation';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import './MapBox2.scss';
+import { MapBoxInfoWindow } from './MapBoxInfoWindow';
+
+/**
+ * MapBox2 architecture:
+ * Main component: MapBox2.tsx
+ * Supporting Components: SidebarAnnos, CollectionStackingView
+ *
+ * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
+ * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
+ * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
+ * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
+ * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
+ * 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 _global = (window /* browser */ || global /* node */) as any;
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 42.360081,
+ lng: -71.058884,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+};
+
+const apiKey = process.env.GOOGLE_MAPS;
+
+const script = document.createElement('script');
+script.defer = true;
+script.async = true;
+script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+console.log(script.src);
+document.head.appendChild(script);
+
+/**
+ * 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,
+// ],
+// },
+// });
+
+// options for searchbox in Google Maps Places Autocomplete API
+const options = {
+ fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields
+ strictBounds: false,
+ types: ['establishment'], // type pf places, subject of change according to user need
+} as google.maps.places.AutocompleteOptions;
+
+@observer
+export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ @observable private _overlayAnnoInfo: Opt<Doc>;
+ showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(MapBox2, fieldKey);
+ }
+ public get SidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ @computed get inlineTextAnnotations() {
+ return this.allMapMarkers.filter(a => a.textInlineAnnotations);
+ }
+
+ @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: Doc | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
+ @observable private _marqueeing: number[] | undefined;
+ @observable private _isAnnotating = false;
+ @observable private inputRef = React.createRef<HTMLInputElement>();
+ @observable private searchMarkers: google.maps.Marker[] = [];
+ @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
+ @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @computed get allSidebarDocs() {
+ return DocListCast(this.dataDoc[this.SidebarKey]);
+ }
+ @computed get allMapMarkers() {
+ return DocListCast(this.dataDoc[this.annotationKey]);
+ }
+ @observable private toggleAddMarker = false;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+ @observable _showSidebar = false;
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
+ }
+
+ static _canAnnotate = true;
+ static _hadSelection: boolean = false;
+ private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+
+ @action
+ private setSearchBox = (searchBox: any) => {
+ this.searchBox = searchBox;
+ };
+
+ // iterate allMarkers to size, center, and zoom map to contain all markers
+ private fitBounds = (map: google.maps.Map) => {
+ const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds();
+ const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean);
+ !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds()));
+ };
+
+ /**
+ * Custom control for add marker button
+ * @param controlDiv
+ * @param map
+ */
+ private CenterControl = () => {
+ const controlDiv = document.createElement('div');
+ controlDiv.className = 'MapBox2-addMarker';
+ // Set CSS for the control border.
+ const controlUI = document.createElement('div');
+ controlUI.style.backgroundColor = '#fff';
+ controlUI.style.borderRadius = '3px';
+ controlUI.style.cursor = 'pointer';
+ controlUI.style.marginTop = '10px';
+ controlUI.style.borderRadius = '4px';
+ controlUI.style.marginBottom = '22px';
+ controlUI.style.textAlign = 'center';
+ controlUI.style.position = 'absolute';
+ controlUI.style.width = '32px';
+ controlUI.style.height = '32px';
+ controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.';
+
+ const plIcon = document.createElement('img');
+ plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png';
+ plIcon.style.color = 'rgb(25,25,25)';
+ plIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
+ plIcon.style.fontSize = '16px';
+ plIcon.style.lineHeight = '32px';
+ plIcon.style.left = '18';
+ plIcon.style.top = '15';
+ plIcon.style.position = 'absolute';
+ plIcon.width = 14;
+ plIcon.height = 14;
+ plIcon.innerHTML = 'Add';
+ controlUI.appendChild(plIcon);
+
+ // Set CSS for the control interior.
+ const markerIcon = document.createElement('img');
+ markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png';
+ markerIcon.style.color = 'rgb(25,25,25)';
+ markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
+ markerIcon.style.fontSize = '16px';
+ markerIcon.style.lineHeight = '32px';
+ markerIcon.style.left = '-2';
+ markerIcon.style.top = '1';
+ markerIcon.width = 30;
+ markerIcon.height = 30;
+ markerIcon.style.position = 'absolute';
+ markerIcon.innerHTML = 'Add';
+ controlUI.appendChild(markerIcon);
+
+ // Setup the click event listeners
+ controlUI.addEventListener('click', () => {
+ if (this.toggleAddMarker === true) {
+ this.toggleAddMarker = false;
+ console.log('add marker button status:' + this.toggleAddMarker);
+ controlUI.style.backgroundColor = '#fff';
+ markerIcon.style.color = 'rgb(25,25,25)';
+ } else {
+ this.toggleAddMarker = true;
+ console.log('add marker button status:' + this.toggleAddMarker);
+ controlUI.style.backgroundColor = '#4476f7';
+ markerIcon.style.color = 'rgb(255,255,255)';
+ }
+ });
+ controlDiv.appendChild(controlUI);
+ return controlDiv;
+ };
+
+ /**
+ * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list
+ * @param position - the LatLng position where the marker is placed
+ * @param map
+ */
+ @action
+ private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
+ const marker = new google.maps.Marker({
+ position: position,
+ map: map,
+ });
+ map.panTo(position);
+ const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
+ this.addDocument(mapMarker, this.annotationKey);
+ };
+
+ _loadPending = true;
+ /**
+ * store a reference to google map instance
+ * setup the drawing manager on the top right corner of map
+ * fit map bounds to contain all markers
+ * @param map
+ */
+ @action
+ private loadHandler = (map: google.maps.Map) => {
+ this._map = map;
+ this._loadPending = true;
+ const centerControlDiv = this.CenterControl();
+ map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv);
+ //drawingManager.setMap(map);
+ // if (navigator.geolocation) {
+ // navigator.geolocation.getCurrentPosition(
+ // (position: Position) => {
+ // const pos = {
+ // lat: position.coords.latitude,
+ // lng: position.coords.longitude,
+ // };
+ // this._map.setCenter(pos);
+ // }
+ // );
+ // } else {
+ // alert("Your geolocation is not supported by browser.")
+ // };
+ map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5));
+ map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng)));
+ setTimeout(() => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ }, 250);
+ // listener to addmarker event
+ this._map.addListener('click', (e: MouseEvent) => {
+ if (this.toggleAddMarker === true) {
+ this.placeMarker((e as any).latLng, map);
+ }
+ });
+ };
+
+ @action
+ centered = () => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ this.dataDoc.mapLat = this._map.getCenter()?.lat();
+ this.dataDoc.mapLng = this._map.getCenter()?.lng();
+ };
+
+ @action
+ zoomChanged = () => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ this.dataDoc.mapZoom = this._map.getZoom();
+ };
+
+ /**
+ * Load and render all map markers
+ * @param marker
+ * @param place
+ */
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
+ place[Id] ? (this.markerMap[place[Id]] = marker) : null;
+ };
+
+ /**
+ * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true
+ * @param e
+ * @param place
+ */
+ @action
+ private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+ place.infoWindowOpen = true;
+ };
+
+ /**
+ * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ console.log('print all sidebar Docs');
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ if (doc.lat !== undefined && doc.lng !== undefined) {
+ const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
+ if (existingMarker) {
+ Doc.AddDocToList(existingMarker, 'data', doc);
+ } else {
+ const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
+ this.addDocument(marker, this.annotationKey);
+ }
+ }
+ }); //add to annotation list
+
+ return this.addDocument(doc, sidebarKey); // add to sidebar list
+ };
+
+ /**
+ * Removing documents from the sidebar
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ if (this.layoutDoc._layout_showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ return this.removeDocument(doc, sidebarKey);
+ };
+
+ /**
+ * Toggle sidebar onclick the tiny comment button on the top right corner
+ * @param e
+ */
+ sidebarBtnDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) =>
+ runInAction(() => {
+ const localDelta = this.props
+ .ScreenToLocalTransform()
+ .scale(this.props.NativeDimScaling?.() || 1)
+ .transformDirection(delta[0], delta[1]);
+ const fullWidth = this.layoutDoc[WidthSym]();
+ const mapWidth = fullWidth - this.sidebarWidth();
+ if (this.sidebarWidth() + localDelta[0] > 0) {
+ this._showSidebar = true;
+ this.layoutDoc._width = fullWidth + localDelta[0];
+ this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ } else {
+ this._showSidebar = false;
+ this.layoutDoc._width = mapWidth;
+ this.layoutDoc._layout_sidebarWidthPercent = '0%';
+ }
+ return false;
+ }),
+ emptyFunction,
+ () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
+ );
+ };
+
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ @computed get layout_sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
+ }
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ }
+
+ /**
+ * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted;
+ * add a customized temporary marker on the map
+ */
+ @action
+ private handlePlaceChanged = () => {
+ const place = this.searchBox.getPlace();
+
+ if (!place.geometry || !place.geometry.location) {
+ // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed
+ window.alert("No details available for input: '" + place.name + "'");
+ return;
+ }
+
+ // zoom in on the location of the search result
+ if (place.geometry.viewport) {
+ this._map.fitBounds(place.geometry.viewport);
+ } else {
+ this._map.setCenter(place.geometry.location);
+ this._map.setZoom(17);
+ }
+
+ // customize icon => customized icon for the nature of the location selected
+ const icon = {
+ url: place.icon as string,
+ size: new google.maps.Size(71, 71),
+ origin: new google.maps.Point(0, 0),
+ anchor: new google.maps.Point(17, 34),
+ scaledSize: new google.maps.Size(25, 25),
+ };
+
+ // put temporary cutomized marker on searched location
+ this.searchMarkers.forEach(marker => {
+ marker.setMap(null);
+ });
+ this.searchMarkers = [];
+ this.searchMarkers.push(
+ new window.google.maps.Marker({
+ map: this._map,
+ icon,
+ title: place.name,
+ position: place.geometry.location,
+ })
+ );
+ };
+
+ /**
+ * Handles toggle of sidebar on click the little comment button
+ */
+ @computed get sidebarHandle() {
+ return (
+ <div
+ className="MapBox2-overlayButton-sidebar"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}
+ onPointerDown={this.sidebarBtnDown}>
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+ </div>
+ );
+ }
+
+ // TODO: Adding highlight box layer to Maps
+ @action
+ toggleSidebar = () => {
+ //1.2 * w * ? = .2 * w .2/1.2
+ const prevWidth = this.sidebarWidth();
+ this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
+ this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ };
+
+ sidebarDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
+ };
+ sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const bounds = this._ref.current!.getBoundingClientRect();
+ this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
+ e.preventDefault();
+ return false;
+ };
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
+
+ @action
+ onMarqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ setupMoveUpEvents(
+ this,
+ e,
+ action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }),
+ returnFalse,
+ () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
+ false
+ );
+ }
+ };
+ @action finishMarquee = (x?: number, y?: number) => {
+ this._marqueeing = undefined;
+ this._isAnnotating = false;
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ };
+
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ return this.addDocument(doc, annotationKey);
+ };
+
+ pointerEvents = () => {
+ return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none';
+ };
+ @computed get annotationLayer() {
+ return (
+ <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ {this.inlineTextAnnotations
+ .sort((a, b) => NumCast(a.y) - NumCast(b.y))
+ .map(anno => (
+ <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />
+ ))}
+ </div>
+ );
+ }
+
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
+
+ /**
+ * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
+ * @returns
+ */
+ private renderMarkers = () => {
+ return this.allMapMarkers.map(place => (
+ <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} />
+ ));
+ };
+
+ // TODO: auto center on select a document in the sidebar
+ private handleMapCenter = (map: google.maps.Map) => {
+ // console.log("print the selected views in selectionManager:")
+ // if (SelectionManager.Views().lastElement()) {
+ // console.log(SelectionManager.Views().lastElement());
+ // }
+ };
+
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+ panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
+ transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ infoWidth = () => this.props.PanelWidth() / 5;
+ infoHeight = () => this.props.PanelHeight() / 5;
+ anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
+ savedAnnotations = () => this._savedAnnotations;
+
+ get MicrosoftMaps() {
+ return (window as any).Microsoft.Maps;
+ }
+ render() {
+ const renderAnnotations = (docFilters?: () => string[]) => null;
+ return (
+ <div className="MapBox2" ref={this._ref}>
+ <div
+ className="MapBox2-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={async e => {
+ e.button === 0 && !e.ctrlKey && e.stopPropagation();
+ }}
+ style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
+ <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
+ {renderAnnotations(this.opaqueFilter)}
+ {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
+ {this.annotationLayer}
+
+ <div>
+ <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
+ <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
+ <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
+ </Autocomplete>
+
+ {this.renderMarkers()}
+ {this.allMapMarkers
+ .filter(marker => marker.infoWindowOpen)
+ .map(marker => (
+ <MapBoxInfoWindow
+ key={marker[Id]}
+ {...this.props}
+ setContentView={emptyFunction}
+ place={marker}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ />
+ ))}
+ {/* {this.handleMapCenter(this._map)} */}
+ </GoogleMap>
+ </div>
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ anchorMenuClick={this.anchorMenuClick}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={returnOne}
+ addDocument={this.addDocumentWrapper}
+ docView={this.props.docViewPath().lastElement()}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ selectionText={returnEmptyString}
+ mainCont={this._mainCont.current}
+ />
+ )}
+ </div>
+ {/* </LoadScript > */}
+ <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ usePanelWidth={true}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ {this.sidebarHandle}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 00bedafbe..6b26f494b 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -32,7 +32,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
addNoteClick = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => {
- const newBox = Docs.Create.TextDocument('Note', { _autoHeight: true });
+ const newBox = Docs.Create.TextDocument('Note', { _layout_autoHeight: true });
FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
Doc.AddDocToList(this.props.place, 'data', newBox);
this._stack?.scrollToBottom();
@@ -42,7 +42,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
};
_stack: CollectionStackingView | CollectionNoteTakingView | null | undefined;
- childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
+ childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean);
removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean);
render() {
@@ -52,11 +52,11 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
<div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
<CollectionStackingView
ref={r => (this._stack = r)}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
Document={this.props.place}
DataDoc={undefined}
fieldKey="data"
- CollectionView={undefined}
NativeWidth={returnZero}
NativeHeight={returnZero}
docFilters={returnEmptyFilter}
@@ -69,12 +69,12 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
rootSelected={returnFalse}
childHideResizeHandles={returnTrue}
childHideDecorationTitle={returnTrue}
- childFitWidth={this.childFitWidth}
+ childLayoutFitWidth={this.childLayoutFitWidth}
// childDocumentsActive={returnFalse}
removeDocument={this.removeDoc}
addDocument={this.addDoc}
renderDepth={this.props.renderDepth + 1}
- viewType={CollectionViewType.Stacking}
+ type_collection={CollectionViewType.Stacking}
pointerEvents={returnAll}
/>
</div>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b88ac113e..95fbb274d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,15 +5,20 @@ import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { InkTool } from '../../../fields/InkField';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
import { KeyCodes } from '../../util/KeyCodes';
+import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -22,13 +27,12 @@ import { LightboxView } from '../LightboxView';
import { CreateImage } from '../nodes/WebBoxRenderer';
import { PDFViewer } from '../pdf/PDFViewer';
import { SidebarAnnos } from '../SidebarAnnos';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { ImageBox } from './ImageBox';
import './PDFBox.scss';
-import { VideoBox } from './VideoBox';
+import { PinProps, PresBox } from './trails';
import React = require('react');
-import { PresBox } from './trails';
-import { DocFocusOptions } from './DocumentView';
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -41,7 +45,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
private _searchRef = React.createRef<HTMLInputElement>();
- private _selectReactionDisposer: IReactionDisposer | undefined;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _sidebarRef = React.createRef<SidebarAnnos>();
@observable private _searching: boolean = false;
@@ -59,7 +63,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
super(props);
const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927;
const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
- !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
if (this.pdfUrl) {
if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)));
else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf)));
@@ -115,18 +119,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw;
cropping._height = anchh;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.IMG;
croppingProto.layout = ImageBox.LayoutString('data');
croppingProto.data = new ImageField(Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png'));
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
}
this.props.bringToFront(cropping);
@@ -136,12 +140,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
htmlString,
anchw,
anchh,
- (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']),
- (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']),
+ (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']),
+ (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']),
4
)
.then((data_url: any) => {
- VideoBox.convertDataUri(data_url, region[Id]).then(returnedfilename =>
+ Utils.convertDataUri(data_url, region[Id]).then(returnedfilename =>
setTimeout(
action(() => {
croppingProto.data = new ImageField(returnedfilename);
@@ -169,26 +173,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc[HeightSym](),
this.props.PanelWidth(),
this.props.PanelHeight(),
- NumCast(this.layoutDoc._scrollTop),
- NumCast(this.rootDoc[this.fieldKey + '-nativeHeight'], 1),
+ NumCast(this.layoutDoc._layout_scrollTop),
+ NumCast(this.rootDoc[this.fieldKey + '_nativeHeight'], 1),
true,
this.layoutDoc[Id] + '-icon',
(iconFile: string, nativeWidth: number, nativeHeight: number) => {
setTimeout(() => {
this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc['icon-nativeWidth'] = nativeWidth;
- this.dataDoc['icon-nativeHeight'] = nativeHeight;
+ this.dataDoc['icon_nativeWidth'] = nativeWidth;
+ this.dataDoc['icon_nativeHeight'] = nativeHeight;
}, 500);
}
);
};
componentWillUnmount() {
- this._selectReactionDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
}
componentDidMount() {
this.props.setContentView?.(this);
- this._selectReactionDisposer = reaction(
+ this._disposers.select = reaction(
() => this.props.isSelected(),
() => {
document.removeEventListener('keydown', this.onKeyDown);
@@ -196,38 +200,53 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.rootDoc.layout_scrollTop,
+ () => {
+ if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) {
+ this.props.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop');
+ }
+ this.props.Document[this.SidebarKey + '_freeform_scale'] = 1;
+ this.props.Document[this.SidebarKey + '_freeform_panX'] = 0;
+ }
+ );
}
- brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
- this._pdfViewer?.brushView(view);
- };
- scrollFocus = (doc: Doc, options: DocFocusOptions) => {
- let didToggle = false;
- if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
- this.toggleSidebar(!options.instant);
- didToggle = true;
+ brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
+
+ sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+ if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(false);
+ return true;
}
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
- this._initialScrollTarget = doc;
- PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
- return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
+ return this.props.addDocTab(doc, where);
+ };
+ focus = (anchor: Doc, options: DocFocusOptions) => {
+ this._initialScrollTarget = anchor;
+ return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.presViewScroll)), options);
};
- getAnchor = (addAsAnnotation: boolean) => {
+
+ getView = async (doc: Doc) => {
+ if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
let ele: Opt<HTMLDivElement> = undefined;
if (this._pdfViewer?.selectionContent()) {
ele = document.createElement('div');
ele.append(this._pdfViewer.selectionContent()!);
}
const docAnchor = () => {
- const anchor = Docs.Create.TextanchorDocument({
- title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
- unrendered: true,
+ const anchor = Docs.Create.PdfConfigDocument({
+ title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
+ annotationOn: this.rootDoc,
});
- PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
return anchor;
};
const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true);
const anchor = annoAnchor ?? docAnchor();
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc);
anchor.text = ele?.textContent ?? '';
anchor.textHtml = ele?.innerHTML;
if (addAsAnnotation || annoAnchor) {
@@ -238,11 +257,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc[this.props.fieldKey + '-numPages'] = np;
+ this.dataDoc[this.props.fieldKey + '_numPages'] = np;
Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72));
Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72);
this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1);
- !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
};
public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
@@ -259,14 +278,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
public backPage = () => {
- this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1);
+ this.Document._layout_curPage = Math.max(1, (NumCast(this.Document._layout_curPage) || 1) - 1);
return true;
};
public forwardPage = () => {
- this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '-numPages']), (NumCast(this.Document._curPage) || 1) + 1);
+ this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1);
return true;
};
- public gotoPage = (p: number) => (this.Document._curPage = p);
+ public gotoPage = (p: number) => (this.Document._layout_curPage = p);
@undoBatch
onKeyDown = action((e: KeyboardEvent) => {
@@ -287,17 +306,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
setPdfViewer = (pdfViewer: PDFViewer) => {
this._pdfViewer = pdfViewer;
- if (this._initialScrollTarget) {
- this.scrollFocus(this._initialScrollTarget, { instant: true });
+ const docView = this.props.DocumentView?.();
+ if (this._initialScrollTarget && docView) {
+ this.focus(this._initialScrollTarget, { instant: true });
this._initialScrollTarget = undefined;
}
};
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
// adding external documents; to sidebar key
- // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
- sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation")
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
+ if (!this.layoutDoc._show_sidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
};
sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => {
@@ -311,19 +331,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth;
if (ratio >= 1) {
this.layoutDoc.nativeWidth = nativeWidth * ratio;
onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
},
(e, movement, isClick) => !isClick && batch.end(),
() => {
- this.toggleSidebar();
+ onButton && this.toggleSidebar();
batch.end();
}
);
@@ -331,7 +351,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
@@ -342,7 +362,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
} else {
this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
});
settingsPanel() {
@@ -357,7 +377,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</>
);
const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`;
- const curPage = NumCast(this.Document._curPage) || 1;
+ const curPage = NumCast(this.Document._layout_curPage) || 1;
return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : (
<div
className="pdfBox-ui"
@@ -403,7 +423,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<input
value={curPage}
style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }}
- onChange={e => (this.Document._curPage = Number(e.currentTarget.value))}
+ onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))}
onKeyDown={e => e.stopPropagation()}
onClick={action(() => (this._pageControls = !this._pageControls))}
/>
@@ -419,12 +439,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
+ @undoBatch
+ toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
specificContextMenu = (e: React.MouseEvent): void => {
- const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
- funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
- //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
+ !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
+ //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+ !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
+ const help = cm.findByDescription('Help...');
+ const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+ helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+ !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
};
@computed get renderTitleBox() {
@@ -441,7 +469,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@observable _showSidebar = false;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._show_sidebar ? true : false;
}
@computed get sidebarHandle() {
return (
@@ -451,7 +479,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={e => this.sidebarBtnDown(e, true)}>
@@ -460,18 +488,90 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
+ public get SidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
+ @computed get pdfScale() {
+ const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth);
+ const pdfRatio = pdfNativeWidth / nativeWidth;
+ return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth;
+ }
+ @computed get sidebarNativeWidth() {
+ return this.sidebarWidth() / this.pdfScale;
+ }
+ @computed get sidebarNativeHeight() {
+ return this.props.PanelHeight() / this.pdfScale;
+ }
+ sidebarNativeWidthFunc = () => this.sidebarNativeWidth;
+ sidebarNativeHeightFunc = () => this.sidebarNativeHeight;
+ sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
+ sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
+ sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0);
+ @computed get sidebarCollection() {
+ const renderComponent = (tag: string) => {
+ const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView;
+ return ComponentTag === CollectionStackingView ? (
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ setHeight={emptyFunction}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ />
+ ) : (
+ <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+ <ComponentTag
+ {...this.props}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ NativeWidth={this.sidebarNativeWidthFunc}
+ NativeHeight={this.sidebarNativeHeightFunc}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.sidebarWidth}
+ xPadding={0}
+ yPadding={0}
+ viewField={this.SidebarKey}
+ isAnnotationOverlay={false}
+ originTopLeft={true}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ select={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.sidebarRemDocument}
+ moveDocument={this.sidebarMoveDocument}
+ addDocument={this.sidebarAddDocument}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ renderDepth={this.props.renderDepth + 1}
+ noSidebar={true}
+ fieldKey={this.SidebarKey}
+ />
+ </div>
+ );
+ };
+ return (
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
+ {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ </div>
+ );
+ }
isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
- return (
+ return !this._pdf ? null : (
<div
- className={'pdfBox'}
+ className="pdfBox"
onContextMenu={this.specificContextMenu}
style={{
display: this.props.thumbShown?.() ? 'none' : undefined,
- height: this.props.Document._scrollTop && !this.Document._fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined,
+ height: this.props.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined,
}}>
<div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
<div
@@ -485,10 +585,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}}>
<PDFViewer
{...this.props}
+ sidebarAddDoc={this.sidebarAddDocument}
rootDoc={this.rootDoc}
+ addDocTab={this.sidebarAddDocTab}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- pdf={this._pdf!}
+ pdf={this._pdf}
+ focus={this.focus}
url={this.pdfUrl!.url.pathname}
isContentActive={this.isPdfContentActive}
anchorMenuClick={this.anchorMenuClick}
@@ -501,22 +604,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
crop={this.crop}
/>
</div>
- <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
- <SidebarAnnos
- ref={this._sidebarRef}
- {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- setHeight={emptyFunction}
- nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
- showSidebar={this.SidebarShown}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- sidebarAddDocument={this.sidebarAddDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- />
- </div>
+ <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
{this.settingsPanel()}
</div>
);
@@ -526,21 +614,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
render() {
TraceMobx();
- if (this._pdf) {
- if (!this.props.thumbShown?.()) {
- return this.renderPdfView;
- }
- return null;
- }
+ if (this.props.thumbShown?.()) return null;
+ const pdfView = this.renderPdfView;
const href = this.pdfUrl?.url.href;
- if (href) {
+ if (!pdfView && href) {
if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
else {
if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
}
}
- return this.renderTitleBox;
+ return pdfView ?? this.renderTitleBox;
}
}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
index aaea988a2..ab93583df 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
@@ -1,2531 +1,2170 @@
-import "./PhysicsSimulationBox.scss";
+import './PhysicsSimulationBox.scss';
import { FieldView, FieldViewProps } from './../FieldView';
import React = require('react');
import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { observer } from 'mobx-react';
-import "./PhysicsSimulationBox.scss";
+import './PhysicsSimulationBox.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { CheckBox } from "../../search/CheckBox";
-import PauseIcon from "@mui/icons-material/Pause";
-import PlayArrowIcon from "@mui/icons-material/PlayArrow";
-import ReplayIcon from "@mui/icons-material/Replay";
-import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
-import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
-import ArrowRightIcon from "@mui/icons-material/ArrowRight";
-import EditIcon from "@mui/icons-material/Edit";
-import EditOffIcon from "@mui/icons-material/EditOff";
-import {
- Box,
- Button,
- Checkbox,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogContentText,
- DialogActions,
- FormControl,
- FormControlLabel,
- FormGroup,
- IconButton,
- LinearProgress,
- Stack,
-} from "@mui/material";
-import Typography from "@mui/material/Typography";
-import "./PhysicsSimulationBox.scss";
-import InputField from "./PhysicsSimulationInputField";
-import * as questions from "./PhysicsSimulationQuestions.json";
-import * as tutorials from "./PhysicsSimulationTutorial.json";
-import Wall from "./PhysicsSimulationWall";
-import Weight from "./PhysicsSimulationWeight";
+import { CheckBox } from '../../search/CheckBox';
+import PauseIcon from '@mui/icons-material/Pause';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import ReplayIcon from '@mui/icons-material/Replay';
+import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
+import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
+import ArrowRightIcon from '@mui/icons-material/ArrowRight';
+import EditIcon from '@mui/icons-material/Edit';
+import EditOffIcon from '@mui/icons-material/EditOff';
+import { Box, Button, Checkbox, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material';
+import Typography from '@mui/material/Typography';
+import './PhysicsSimulationBox.scss';
+import InputField from './PhysicsSimulationInputField';
+import * as questions from './PhysicsSimulationQuestions.json';
+import * as tutorials from './PhysicsSimulationTutorial.json';
+import Wall from './PhysicsSimulationWall';
+import Weight from './PhysicsSimulationWeight';
+import { NumCast } from '../../../../fields/Types';
+import { HeightSym, WidthSym } from '../../../../fields/Doc';
interface IWallProps {
- length: number;
- xPos: number;
- yPos: number;
- angleInDegrees: number;
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
}
interface IForce {
- description: string;
- magnitude: number;
- directionInDegrees: number;
- component: boolean;
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ component: boolean;
}
interface VectorTemplate {
- top: number;
- left: number;
- width: number;
- height: number;
- x1: number;
- y1: number;
- x2: number;
- y2: number;
- weightX: number;
- weightY: number;
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+ weightX: number;
+ weightY: number;
}
interface QuestionTemplate {
- questionSetup: string[];
- variablesForQuestionSetup: string[];
- question: string;
- answerParts: string[];
- answerSolutionDescriptions: string[];
- goal: string;
- hints: { description: string; content: string }[];
+ questionSetup: string[];
+ variablesForQuestionSetup: string[];
+ question: string;
+ answerParts: string[];
+ answerSolutionDescriptions: string[];
+ goal: string;
+ hints: { description: string; content: string }[];
}
interface TutorialTemplate {
- question: string;
- steps: {
- description: string;
- content: string;
- forces: {
- description: string;
- magnitude: number;
- directionInDegrees: number;
- component: boolean;
+ question: string;
+ steps: {
+ description: string;
+ content: string;
+ forces: {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ component: boolean;
+ }[];
+ showMagnitude: boolean;
}[];
- showMagnitude: boolean;
- }[];
}
@observer
-export default class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
-
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); }
-
- constructor(props: any) {
- super(props);
- }
-
- // Constants
- xMin = 0;
- yMin = 0;
- xMax = 100;
- yMax = 100;
- color = `rgba(0,0,0,0.5)`;
- radius = 50;
- wallPositions: IWallProps[] = [];
+export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PhysicsSimulationBox, fieldKey);
+ }
- componentDidMount() {
- // Used throughout sims
- this.layoutDoc._width = 1000;
- this.layoutDoc._height = 800;
- this.xMax = this.layoutDoc._width*0.6;
- this.yMax = this.layoutDoc._height*0.9;
- this.dataDoc.reviewCoefficient = this.dataDoc.reviewCoefficient ?? 0;
- this.dataDoc.questionVariables = this.dataDoc.questionVariables ?? [];
- this.dataDoc.accelerationXDisplay = this.dataDoc.accelerationXDisplay ?? 0;
- this.dataDoc.accelerationYDisplay = this.dataDoc.accelerationYDisplay ?? 0;
- this.dataDoc.componentForces = this.dataDoc.componentForces ?? [];
- this.dataDoc.displayChange = this.dataDoc.displayChange ?? { xDisplay: 0, yDisplay: 0 };
- this.dataDoc.elasticCollisions = this.dataDoc.elasticCollisions ?? false;
- this.dataDoc.gravity = this.dataDoc.gravity ?? -9.81;
- this.dataDoc.mass = this.dataDoc.mass ?? 1;
- this.dataDoc.mode = "Freeform";
- this.dataDoc.positionXDisplay = this.dataDoc.positionXDisplay ?? 0;
- this.dataDoc.positionYDisplay = this.dataDoc.positionYDisplay ?? 0;
- this.dataDoc.showAcceleration = this.dataDoc.showAcceleration ?? false;
- this.dataDoc.showComponentForces = this.dataDoc.showComponentForces ?? false;
- this.dataDoc.showForces = this.dataDoc.showForces ?? true;
- this.dataDoc.showForceMagnitudes = this.dataDoc.showForceMagnitudes ?? true;
- this.dataDoc.showVelocity = this.dataDoc.showVelocity ?? false;
- this.dataDoc.simulationPaused = this.dataDoc.simulationPaused ?? true;
- this.dataDoc.simulationReset = this.dataDoc.simulationReset ?? false;
- this.dataDoc.simulationSpeed = this.dataDoc.simulationSpeed ?? 2;
- this.dataDoc.simulationType = this.dataDoc.simulationType ?? "Inclined Plane";
- this.dataDoc.startForces = this.dataDoc.startForces ?? [];
- this.dataDoc.startPosX = this.dataDoc.startPosX ?? Math.round((this.xMax * 0.5 - 200) * 10) / 10;
- this.dataDoc.startPosY = this.dataDoc.startPosY ?? this.getDisplayYPos(
- (400 - (0.08*this.layoutDoc._height)) * Math.tan((26 * Math.PI) / 180) + Math.sqrt(26)
- );
- this.dataDoc.startVelX = this.dataDoc.startVelX ?? 0;
- this.dataDoc.startVelY = this.dataDoc.startVelY ?? 0;
- this.dataDoc.stepNumber = this.dataDoc.stepNumber ?? 0;
- this.dataDoc.updatedForces = this.dataDoc.updatedForces ?? [];
- this.dataDoc.velocityXDisplay = this.dataDoc.velocityXDisplay ?? 0;
- this.dataDoc.velocityYDisplay = this.dataDoc.velocityYDisplay ?? 0;
+ constructor(props: any) {
+ super(props);
+ }
- // Used for review mode
- // this.dataDoc.currentForceSketch = this.dataDoc.currentForceSketch ?? null;
- // this.dataDoc.deleteMode = this.dataDoc.deleteMode ?? false;
- // this.dataDoc.forceSketches = this.dataDoc.forceSketches ?? [];
- this.dataDoc.answers = [];
- this.dataDoc.showIcon = false;
- this.dataDoc.hintDialogueOpen = false;
- this.dataDoc.noMovement = false;
- this.dataDoc.questionNumber = 0;
- this.dataDoc.questionPartOne = "";
- this.dataDoc.questionPartTwo = "";
- this.dataDoc.reviewGravityAngle = 0;
- this.dataDoc.reviewGravityMagnitude = 0;
- this.dataDoc.reviewNormalAngle = 0;
- this.dataDoc.reviewNormalMagnitude = 0;
- this.dataDoc.reviewStaticAngle = 0;
- this.dataDoc.reviewStaticMagnitude = 0;
- this.dataDoc.selectedSolutions = [];
- this.dataDoc.selectedQuestion = this.dataDoc.selectedQuestion ?? questions.inclinePlane[0];
- // this.dataDoc.sketching = this.dataDoc.sketching ?? false;
+ // Constants
+ xMin = 0;
+ yMin = 0;
+ xMax = 100;
+ yMax = 100;
+ color = `rgba(0,0,0,0.5)`;
+ radius = 50;
+ wallPositions: IWallProps[] = [];
- // Used for tutorial mode
- this.dataDoc.selectedTutorial = this.dataDoc.selectedTutorial ?? tutorials.inclinePlane;
+ componentDidMount() {
+ // Used throughout sims
+ this.layoutDoc._width = 1000;
+ this.layoutDoc._height = 800;
+ this.xMax = this.layoutDoc._width * 0.6;
+ this.yMax = this.layoutDoc._height * 0.9;
+ this.dataDoc.reviewCoefficient = this.dataDoc.reviewCoefficient ?? 0;
+ this.dataDoc.questionVariables = this.dataDoc.questionVariables ?? [];
+ this.dataDoc.accelerationXDisplay = this.dataDoc.accelerationXDisplay ?? 0;
+ this.dataDoc.accelerationYDisplay = this.dataDoc.accelerationYDisplay ?? 0;
+ this.dataDoc.componentForces = this.dataDoc.componentForces ?? [];
+ this.dataDoc.displayChange = this.dataDoc.displayChange ?? { xDisplay: 0, yDisplay: 0 };
+ this.dataDoc.elasticCollisions = this.dataDoc.elasticCollisions ?? false;
+ this.dataDoc.gravity = this.dataDoc.gravity ?? -9.81;
+ this.dataDoc.mass = this.dataDoc.mass ?? 1;
+ this.dataDoc.mode = 'Freeform';
+ this.dataDoc.positionXDisplay = this.dataDoc.positionXDisplay ?? 0;
+ this.dataDoc.positionYDisplay = this.dataDoc.positionYDisplay ?? 0;
+ this.dataDoc.showAcceleration = this.dataDoc.showAcceleration ?? false;
+ this.dataDoc.showComponentForces = this.dataDoc.showComponentForces ?? false;
+ this.dataDoc.showForces = this.dataDoc.showForces ?? true;
+ this.dataDoc.showForceMagnitudes = this.dataDoc.showForceMagnitudes ?? true;
+ this.dataDoc.showVelocity = this.dataDoc.showVelocity ?? false;
+ this.dataDoc.simulationPaused = this.dataDoc.simulationPaused ?? true;
+ this.dataDoc.simulationReset = this.dataDoc.simulationReset ?? false;
+ this.dataDoc.simulationSpeed = this.dataDoc.simulationSpeed ?? 2;
+ this.dataDoc.simulationType = this.dataDoc.simulationType ?? 'Inclined Plane';
+ this.dataDoc.startForces = this.dataDoc.startForces ?? [];
+ this.dataDoc.startPosX = this.dataDoc.startPosX ?? Math.round((this.xMax * 0.5 - 200) * 10) / 10;
+ this.dataDoc.startPosY = this.dataDoc.startPosY ?? this.getDisplayYPos((400 - 0.08 * this.layoutDoc._height) * Math.tan((26 * Math.PI) / 180) + Math.sqrt(26));
+ this.dataDoc.startVelX = this.dataDoc.startVelX ?? 0;
+ this.dataDoc.startVelY = this.dataDoc.startVelY ?? 0;
+ this.dataDoc.stepNumber = this.dataDoc.stepNumber ?? 0;
+ this.dataDoc.updatedForces = this.dataDoc.updatedForces ?? [];
+ this.dataDoc.velocityXDisplay = this.dataDoc.velocityXDisplay ?? 0;
+ this.dataDoc.velocityYDisplay = this.dataDoc.velocityYDisplay ?? 0;
- // Used for uniform circular motion
- this.dataDoc.circularMotionRadius = this.dataDoc.circularMotionRadius ?? 150;
+ // Used for review mode
+ // this.dataDoc.currentForceSketch = this.dataDoc.currentForceSketch ?? null;
+ // this.dataDoc.deleteMode = this.dataDoc.deleteMode ?? false;
+ // this.dataDoc.forceSketches = this.dataDoc.forceSketches ?? [];
+ this.dataDoc.answers = [];
+ this.dataDoc.showIcon = false;
+ this.dataDoc.hintDialogueOpen = false;
+ this.dataDoc.noMovement = false;
+ this.dataDoc.questionNumber = 0;
+ this.dataDoc.questionPartOne = '';
+ this.dataDoc.questionPartTwo = '';
+ this.dataDoc.reviewGravityAngle = 0;
+ this.dataDoc.reviewGravityMagnitude = 0;
+ this.dataDoc.reviewNormalAngle = 0;
+ this.dataDoc.reviewNormalMagnitude = 0;
+ this.dataDoc.reviewStaticAngle = 0;
+ this.dataDoc.reviewStaticMagnitude = 0;
+ this.dataDoc.selectedSolutions = [];
+ this.dataDoc.selectedQuestion = this.dataDoc.selectedQuestion ?? questions.inclinePlane[0];
+ // this.dataDoc.sketching = this.dataDoc.sketching ?? false;
- // Used for spring simulation
- this.dataDoc.springConstant = this.dataDoc.springConstant ?? 0.5;
- this.dataDoc.springRestLength = this.dataDoc.springRestLength ?? 200;
- this.dataDoc.springStartLength = this.dataDoc.springStartLength ?? 200;
+ // Used for tutorial mode
+ this.dataDoc.selectedTutorial = this.dataDoc.selectedTutorial ?? tutorials.inclinePlane;
- // Used for pendulum simulation
- this.dataDoc.adjustPendulumAngle = this.dataDoc.adjustPendulumAngle ?? { angle: 0, length: 0 };
- this.dataDoc.pendulumAngle = this.dataDoc.pendulumAngle ?? 0;
- this.dataDoc.pendulumLength = this.dataDoc.pendulumLength ?? 300;
- this.dataDoc.startPendulumAngle = this.dataDoc.startPendulumAngle ?? 0;
+ // Used for uniform circular motion
+ this.dataDoc.circularMotionRadius = this.dataDoc.circularMotionRadius ?? 150;
- // Used for wedge simulation
- this.dataDoc.coefficientOfKineticFriction = this.dataDoc.coefficientOfKineticFriction ?? 0;
- this.dataDoc.coefficientOfStaticFriction = this.dataDoc.coefficientOfStaticFriction ?? 0;
- this.dataDoc.wedgeAngle = this.dataDoc.wedgeAngle ?? 26;
- this.dataDoc.wedgeHeight = this.dataDoc.wedgeHeight ?? Math.tan((26 * Math.PI) / 180) * this.xMax*0.5;
- this.dataDoc.wedgeWidth = this.dataDoc.wedgeWidth ?? this.xMax*0.5;
+ // Used for spring simulation
+ this.dataDoc.springConstant = this.dataDoc.springConstant ?? 0.5;
+ this.dataDoc.springRestLength = this.dataDoc.springRestLength ?? 200;
+ this.dataDoc.springStartLength = this.dataDoc.springStartLength ?? 200;
- // Used for pulley simulation
- this.dataDoc.positionXDisplay2 = this.dataDoc.positionXDisplay2 ?? 0;
- this.dataDoc.velocityXDisplay2 = this.dataDoc.velocityXDisplay2 ?? 0;
- this.dataDoc.accelerationXDisplay2 = this.dataDoc.accelerationXDisplay2 ?? 0;
- this.dataDoc.positionYDisplay2 = this.dataDoc.positionYDisplay2 ?? 0;
- this.dataDoc.velocityYDisplay2 = this.dataDoc.velocityYDisplay2 ?? 0;
- this.dataDoc.accelerationYDisplay2 = this.dataDoc.accelerationYDisplay2 ?? 0;
- this.dataDoc.startPosX2 = this.dataDoc.startPosX2 ?? 0;
- this.dataDoc.startPosY2 = this.dataDoc.startPosY2 ?? 0;
- this.dataDoc.displayChange2 = this.dataDoc.displayChange2 ?? { xDisplay: 0, yDisplay: 0 };
- this.dataDoc.startForces2 = this.dataDoc.startForces2 ?? [];
- this.dataDoc.updatedForces2 = this.dataDoc.updatedForces2 ?? [];
- this.dataDoc.mass2 = this.dataDoc.mass2 ?? 1;
+ // Used for pendulum simulation
+ this.dataDoc.adjustPendulumAngle = this.dataDoc.adjustPendulumAngle ?? { angle: 0, length: 0 };
+ this.dataDoc.pendulumAngle = this.dataDoc.pendulumAngle ?? 0;
+ this.dataDoc.pendulumLength = this.dataDoc.pendulumLength ?? 300;
+ this.dataDoc.startPendulumAngle = this.dataDoc.startPendulumAngle ?? 0;
- // Setup simulation
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
+ // Used for wedge simulation
+ this.dataDoc.coefficientOfKineticFriction = this.dataDoc.coefficientOfKineticFriction ?? 0;
+ this.dataDoc.coefficientOfStaticFriction = this.dataDoc.coefficientOfStaticFriction ?? 0;
+ this.dataDoc.wedgeAngle = this.dataDoc.wedgeAngle ?? 26;
+ this.dataDoc.wedgeHeight = this.dataDoc.wedgeHeight ?? Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5;
+ this.dataDoc.wedgeWidth = this.dataDoc.wedgeWidth ?? this.xMax * 0.5;
- // Create walls
- let walls = []
- walls.push({ length: this.xMax / this.layoutDoc._width * 100, xPos: 0, yPos: 0, angleInDegrees: 0 });
- walls.push({ length: this.xMax / this.layoutDoc._width * 100, xPos: 0, yPos: this.yMax / this.layoutDoc._height * 100, angleInDegrees: 0 });
- walls.push({ length: this.yMax / this.layoutDoc._height * 100, xPos: 0, yPos: 0, angleInDegrees: 90 });
- walls.push({ length: this.yMax / this.layoutDoc._height * 100, xPos: this.xMax / this.layoutDoc._width * 100, yPos: 0, angleInDegrees: 90 });
- this.wallPositions = walls
- }
+ // Used for pulley simulation
+ this.dataDoc.positionXDisplay2 = this.dataDoc.positionXDisplay2 ?? 0;
+ this.dataDoc.velocityXDisplay2 = this.dataDoc.velocityXDisplay2 ?? 0;
+ this.dataDoc.accelerationXDisplay2 = this.dataDoc.accelerationXDisplay2 ?? 0;
+ this.dataDoc.positionYDisplay2 = this.dataDoc.positionYDisplay2 ?? 0;
+ this.dataDoc.velocityYDisplay2 = this.dataDoc.velocityYDisplay2 ?? 0;
+ this.dataDoc.accelerationYDisplay2 = this.dataDoc.accelerationYDisplay2 ?? 0;
+ this.dataDoc.startPosX2 = this.dataDoc.startPosX2 ?? 0;
+ this.dataDoc.startPosY2 = this.dataDoc.startPosY2 ?? 0;
+ this.dataDoc.displayChange2 = this.dataDoc.displayChange2 ?? { xDisplay: 0, yDisplay: 0 };
+ this.dataDoc.startForces2 = this.dataDoc.startForces2 ?? [];
+ this.dataDoc.updatedForces2 = this.dataDoc.updatedForces2 ?? [];
+ this.dataDoc.mass2 = this.dataDoc.mass2 ?? 1;
- componentDidUpdate() {
- if (this.xMax != this.layoutDoc._width*0.6 || this.yMax != this.layoutDoc._height*0.9) {
- this.layoutDoc._width = Math.max(this.layoutDoc._width, 800)
- this.layoutDoc._height = Math.max(this.layoutDoc._height, 600)
- this.xMax = this.layoutDoc._width*0.6;
- this.yMax = this.layoutDoc._height*0.9;
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }
- }
+ // Setup simulation
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
- setupSimulation = (simulationType: string, mode: string) => {
- this.dataDoc.simulationPaused = true;
- if (simulationType != "Circular Motion") {
- this.dataDoc.startVelX = 0;
- this.dataDoc.setStartVelY = 0;
- this.dataDoc.velocityXDisplay = (0);
- this.dataDoc.velocityYDisplay = (0);
+ // Create walls
+ let walls = [];
+ walls.push({ length: (this.xMax / this.layoutDoc._width) * 100, xPos: 0, yPos: 0, angleInDegrees: 0 });
+ walls.push({ length: (this.xMax / this.layoutDoc._width) * 100, xPos: 0, yPos: (this.yMax / this.layoutDoc._height) * 100, angleInDegrees: 0 });
+ walls.push({ length: (this.yMax / this.layoutDoc._height) * 100, xPos: 0, yPos: 0, angleInDegrees: 90 });
+ walls.push({ length: (this.yMax / this.layoutDoc._height) * 100, xPos: (this.xMax / this.layoutDoc._width) * 100, yPos: 0, angleInDegrees: 90 });
+ this.wallPositions = walls;
}
- if (mode == "Freeform") {
- this.dataDoc.showForceMagnitudes = (true);
- if (simulationType == "One Weight") {
- this.dataDoc.showComponentForces = false;
- this.dataDoc.startPosY = (this.yMin + (0.08*this.layoutDoc._height));
- this.dataDoc.startPosX = ((this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height));
- this.dataDoc.positionYDisplay = (this.getDisplayYPos(this.yMin + (0.08*this.layoutDoc._height)));
- this.dataDoc.positionXDisplay = ((this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height));
- this.dataDoc.updatedForces = ([
- {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
- },
- ]);
- this.dataDoc.startForces = ([
- {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
- },
- ]);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- } else if (simulationType == "Inclined Plane") {
- this.changeWedgeBasedOnNewAngle(26);
- this.dataDoc.startForces = ([
- {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
- },
- ]);
- this.updateForcesWithFriction(Number(this.dataDoc.coefficientOfStaticFriction));
- } else if (simulationType == "Pendulum") {
- this.setupPendulum();
- } else if (simulationType == "Spring") {
- this.setupSpring();
- } else if (simulationType == "Circular Motion") {
- this.setupCircular(0);
- } else if (simulationType == "Pulley") {
- this.setupPulley();
- } else if (simulationType == "Suspension") {
- this.setupSuspension();
- }
- } else if (mode == "Review") {
- this.dataDoc.showComponentForces = (false);
- this.dataDoc.showForceMagnitudes = (true);
- this.dataDoc.showAcceleration = (false);
- this.dataDoc.showVelocity = (false);
- this.dataDoc.showForces= (true);
- this.generateNewQuestion();
- if (simulationType == "One Weight") {
- // TODO - one weight review problems
- } else if (simulationType == "Spring") {
- this.setupSpring();
- // TODO - spring review problems
- } else if (simulationType == "Inclined Plane") {
- this.dataDoc.updatedForces = ([]);
- this.dataDoc.startForces = ([]);
- } else if (simulationType == "Pendulum") {
- this.setupPendulum();
- // TODO - pendulum review problems
- } else if (simulationType == "Circular Motion") {
- this.setupCircular(0);
- // TODO - circular motion review problems
- } else if (simulationType == "Pulley") {
- this.setupPulley();
- // TODO - pulley tutorial review problems
- } else if (simulationType == "Suspension") {
- this.setupSuspension();
- // TODO - suspension tutorial review problems
- }
- } else if (mode == "Tutorial") {
- this.dataDoc.showComponentForces = (false);
- this.dataDoc.stepNumber = (0);
- this.dataDoc.showAcceleration = (false);
- if (this.dataDoc.simulationType != "Circular Motion") {
- this.dataDoc.velocityXDisplay = (0);
- this.dataDoc.velocityYDisplay = (0);
- this.dataDoc.showVelocity = (false);
- } else {
- this.dataDoc.velocityXDisplay = (20);
- this.dataDoc.velocityYDisplay = (0);
- this.dataDoc.showVelocity = (true);
- }
- if (this.dataDoc.simulationType == "One Weight") {
- this.dataDoc.showForces = (true);
- this.dataDoc.startPosY = (this.yMax - 100);
- this.dataDoc.startPosX = ((this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height));
- this.dataDoc.selectedTutorial = (tutorials.freeWeight);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.freeWeight.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.freeWeight.steps[0].showMagnitude);
- } else if (this.dataDoc.simulationType == "Spring") {
- this.dataDoc.showForces = (true);
- this.setupSpring();
- this.dataDoc.startPosY = (this.yMin + 200 + 19.62);
- this.dataDoc.startPosX = ((this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height));
- this.dataDoc.selectedTutorial = (tutorials.spring);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.spring.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.spring.steps[0].showMagnitude);
- } else if (this.dataDoc.simulationType == "Pendulum") {
- this.dataDoc.showForces = (true);
- const length = 300;
- const angle = 30;
- const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
- const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
- const xPos = this.xMax / 2 - x - (0.08*this.layoutDoc._height);
- const yPos = y - (0.08*this.layoutDoc._height) - 5;
- this.dataDoc.startPosX = (xPos);
- this.dataDoc.startPosY = (yPos);
- this.dataDoc.selectedTutorial = (tutorials.pendulum);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.pendulum.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.pendulum.steps[0].showMagnitude);
- this.dataDoc.pendulumAngle = (30);
- this.dataDoc.pendulumLength = (300);
- this.dataDoc.adjustPendulumAngle = ({ angle: 30, length: 300 });
- } else if (this.dataDoc.simulationType == "Inclined Plane") {
- this.dataDoc.showForces = (true);
- this.dataDoc.wedgeAngle = (26);
- this.changeWedgeBasedOnNewAngle(26);
- this.dataDoc.selectedTutorial = (tutorials.inclinePlane);
- this.dataDoc.startForces = (
- this.getForceFromJSON(tutorials.inclinePlane.steps[0].forces)
- );
- this.dataDoc.showForceMagnitudes = (tutorials.inclinePlane.steps[0].showMagnitude);
- } else if (this.dataDoc.simulationType == "Circular Motion") {
- this.dataDoc.showForces = (true);
- this.setupCircular(40);
- this.dataDoc.selectedTutorial = (tutorials.circular);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.circular.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.circular.steps[0].showMagnitude);
- } else if (this.dataDoc.simulationType == "Pulley") {
- this.dataDoc.showForces = (true);
- this.setupPulley();
- this.dataDoc.selectedTutorial = (tutorials.pulley);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.pulley.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.pulley.steps[0].showMagnitude);
- } else if (this.dataDoc.simulationType == "Suspension") {
- this.dataDoc.showForces = (true);
- this.setupSuspension();
- this.dataDoc.selectedTutorial = (tutorials.suspension);
- this.dataDoc.startForces = (this.getForceFromJSON(tutorials.suspension.steps[0].forces));
- this.dataDoc.showForceMagnitudes = (tutorials.suspension.steps[0].showMagnitude);
- }
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
+ componentDidUpdate() {
+ if (this.xMax !== this.layoutDoc[WidthSym]() * 0.6 || this.yMax != this.layoutDoc[HeightSym]() * 0.9) {
+ this.layoutDoc._width = Math.max(this.layoutDoc[WidthSym](), 800);
+ this.layoutDoc._height = Math.max(this.layoutDoc[HeightSym](), 600);
+ this.xMax = this.layoutDoc._width * 0.6;
+ this.yMax = this.layoutDoc._height * 0.9;
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }
}
- }
- // Helper function to go between display and real values
- getDisplayYPos = (yPos: number) => {
- return this.yMax - yPos - 2 * (0.08*this.layoutDoc._height) + 5;
- };
- getYPosFromDisplay = (yDisplay: number) => {
- return this.yMax - yDisplay - 2 * (0.08*this.layoutDoc._height) + 5;
- };
+ setupSimulation = (simulationType: string, mode: string) => {
+ this.dataDoc.simulationPaused = true;
+ if (simulationType != 'Circular Motion') {
+ this.dataDoc.startVelX = 0;
+ this.dataDoc.setStartVelY = 0;
+ this.dataDoc.velocityXDisplay = 0;
+ this.dataDoc.velocityYDisplay = 0;
+ }
+ if (mode == 'Freeform') {
+ this.dataDoc.showForceMagnitudes = true;
+ if (simulationType == 'One Weight') {
+ this.dataDoc.showComponentForces = false;
+ this.dataDoc.startPosY = this.yMin + 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.positionYDisplay = this.getDisplayYPos(this.yMin + 0.08 * this.layoutDoc[HeightSym]());
+ this.dataDoc.positionXDisplay = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.updatedForces = [
+ {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ },
+ ];
+ this.dataDoc.startForces = [
+ {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ },
+ ];
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ } else if (simulationType == 'Inclined Plane') {
+ this.changeWedgeBasedOnNewAngle(26);
+ this.dataDoc.startForces = [
+ {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ },
+ ];
+ this.updateForcesWithFriction(Number(this.dataDoc.coefficientOfStaticFriction));
+ } else if (simulationType == 'Pendulum') {
+ this.setupPendulum();
+ } else if (simulationType == 'Spring') {
+ this.setupSpring();
+ } else if (simulationType == 'Circular Motion') {
+ this.setupCircular(0);
+ } else if (simulationType == 'Pulley') {
+ this.setupPulley();
+ } else if (simulationType == 'Suspension') {
+ this.setupSuspension();
+ }
+ } else if (mode == 'Review') {
+ this.dataDoc.showComponentForces = false;
+ this.dataDoc.showForceMagnitudes = true;
+ this.dataDoc.showAcceleration = false;
+ this.dataDoc.showVelocity = false;
+ this.dataDoc.showForces = true;
+ this.generateNewQuestion();
+ if (simulationType == 'One Weight') {
+ // TODO - one weight review problems
+ } else if (simulationType == 'Spring') {
+ this.setupSpring();
+ // TODO - spring review problems
+ } else if (simulationType == 'Inclined Plane') {
+ this.dataDoc.updatedForces = [];
+ this.dataDoc.startForces = [];
+ } else if (simulationType == 'Pendulum') {
+ this.setupPendulum();
+ // TODO - pendulum review problems
+ } else if (simulationType == 'Circular Motion') {
+ this.setupCircular(0);
+ // TODO - circular motion review problems
+ } else if (simulationType == 'Pulley') {
+ this.setupPulley();
+ // TODO - pulley tutorial review problems
+ } else if (simulationType == 'Suspension') {
+ this.setupSuspension();
+ // TODO - suspension tutorial review problems
+ }
+ } else if (mode == 'Tutorial') {
+ this.dataDoc.showComponentForces = false;
+ this.dataDoc.stepNumber = 0;
+ this.dataDoc.showAcceleration = false;
+ if (this.dataDoc.simulationType != 'Circular Motion') {
+ this.dataDoc.velocityXDisplay = 0;
+ this.dataDoc.velocityYDisplay = 0;
+ this.dataDoc.showVelocity = false;
+ } else {
+ this.dataDoc.velocityXDisplay = 20;
+ this.dataDoc.velocityYDisplay = 0;
+ this.dataDoc.showVelocity = true;
+ }
- // Update forces when coefficient of static friction changes in freeform mode
- updateForcesWithFriction = (
- coefficient: number,
- width: number = this.dataDoc.wedgeWidth,
- height: number = this.dataDoc.wedgeHeight
- ) => {
- const normalForce: IForce = {
- description: "Normal Force",
- magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)) * this.dataDoc.mass,
- directionInDegrees:
- 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
- component: false,
- };
- let frictionForce: IForce = {
- description: "Static Friction Force",
- magnitude:
- coefficient *
- Math.abs(this.dataDoc.gravity) *
- Math.cos(Math.atan(height / width)) *
- this.dataDoc.mass,
- directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
- component: false,
- };
- // reduce magnitude or friction force if necessary such that block cannot slide up plane
- let yForce = -Math.abs(this.dataDoc.gravity) * this.dataDoc.mass;
- yForce +=
- normalForce.magnitude *
- Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
- yForce +=
- frictionForce.magnitude *
- Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
- if (yForce > 0) {
- frictionForce.magnitude =
- (-normalForce.magnitude *
- Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
- Math.abs(this.dataDoc.gravity) * this.dataDoc.mass) /
- Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
- }
- const frictionForceComponent: IForce = {
- description: "Static Friction Force",
- magnitude:
- coefficient * Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)),
- directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
- component: true,
- };
- const normalForceComponent: IForce = {
- description: "Normal Force",
- magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)),
- directionInDegrees:
- 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
- component: true,
+ if (this.dataDoc.simulationType == 'One Weight') {
+ this.dataDoc.showForces = true;
+ this.dataDoc.startPosY = this.yMax - 100;
+ this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.selectedTutorial = tutorials.freeWeight;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.freeWeight.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude;
+ } else if (this.dataDoc.simulationType == 'Spring') {
+ this.dataDoc.showForces = true;
+ this.setupSpring();
+ this.dataDoc.startPosY = this.yMin + 200 + 19.62;
+ this.dataDoc.startPosX = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.selectedTutorial = tutorials.spring;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.spring.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.spring.steps[0].showMagnitude;
+ } else if (this.dataDoc.simulationType == 'Pendulum') {
+ this.dataDoc.showForces = true;
+ const length = 300;
+ const angle = 30;
+ const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ const xPos = this.xMax / 2 - x - 0.08 * this.layoutDoc[HeightSym]();
+ const yPos = y - 0.08 * this.layoutDoc[HeightSym]() - 5;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = yPos;
+ this.dataDoc.selectedTutorial = tutorials.pendulum;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.pendulum.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude;
+ this.dataDoc.pendulumAngle = 30;
+ this.dataDoc.pendulumLength = 300;
+ this.dataDoc.adjustPendulumAngle = { angle: 30, length: 300 };
+ } else if (this.dataDoc.simulationType == 'Inclined Plane') {
+ this.dataDoc.showForces = true;
+ this.dataDoc.wedgeAngle = 26;
+ this.changeWedgeBasedOnNewAngle(26);
+ this.dataDoc.selectedTutorial = tutorials.inclinePlane;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.inclinePlane.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude;
+ } else if (this.dataDoc.simulationType == 'Circular Motion') {
+ this.dataDoc.showForces = true;
+ this.setupCircular(40);
+ this.dataDoc.selectedTutorial = tutorials.circular;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.circular.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.circular.steps[0].showMagnitude;
+ } else if (this.dataDoc.simulationType == 'Pulley') {
+ this.dataDoc.showForces = true;
+ this.setupPulley();
+ this.dataDoc.selectedTutorial = tutorials.pulley;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.pulley.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude;
+ } else if (this.dataDoc.simulationType == 'Suspension') {
+ this.dataDoc.showForces = true;
+ this.setupSuspension();
+ this.dataDoc.selectedTutorial = tutorials.suspension;
+ this.dataDoc.startForces = this.getForceFromJSON(tutorials.suspension.steps[0].forces);
+ this.dataDoc.showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude;
+ }
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }
};
- const gravityParallel: IForce = {
- description: "Gravity Parallel Component",
- magnitude:
- this.dataDoc.mass *
- Math.abs(this.dataDoc.gravity) *
- Math.sin(Math.PI / 2 - Math.atan(height / width)),
- directionInDegrees:
- 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180,
- component: true,
+
+ // Helper function to go between display and real values
+ getDisplayYPos = (yPos: number) => {
+ return this.yMax - yPos - 2 * (0.08 * this.layoutDoc[HeightSym]()) + 5;
};
- const gravityPerpendicular: IForce = {
- description: "Gravity Perpendicular Component",
- magnitude:
- this.dataDoc.mass *
- Math.abs(this.dataDoc.gravity) *
- Math.cos(Math.PI / 2 - Math.atan(height / width)),
- directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
- component: true,
+ getYPosFromDisplay = (yDisplay: number) => {
+ return this.yMax - yDisplay - 2 * (0.08 * this.layoutDoc[HeightSym]()) + 5;
};
- const gravityForce: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
+
+ // Update forces when coefficient of static friction changes in freeform mode
+ updateForcesWithFriction = (coefficient: number, width: number = this.dataDoc.wedgeWidth, height: number = this.dataDoc.wedgeHeight) => {
+ const normalForce: IForce = {
+ description: 'Normal Force',
+ magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)) * this.dataDoc.mass,
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ component: false,
+ };
+ let frictionForce: IForce = {
+ description: 'Static Friction Force',
+ magnitude: coefficient * Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)) * this.dataDoc.mass,
+ directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+ component: false,
+ };
+ // reduce magnitude or friction force if necessary such that block cannot slide up plane
+ let yForce = -Math.abs(this.dataDoc.gravity) * this.dataDoc.mass;
+ yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.dataDoc.gravity) * this.dataDoc.mass) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+ const frictionForceComponent: IForce = {
+ description: 'Static Friction Force',
+ magnitude: coefficient * Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)),
+ directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+ component: true,
+ };
+ const normalForceComponent: IForce = {
+ description: 'Normal Force',
+ magnitude: Math.abs(this.dataDoc.gravity) * Math.cos(Math.atan(height / width)),
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ component: true,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)),
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180,
+ component: true,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)),
+ directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
+ component: true,
+ };
+ const gravityForce: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ if (coefficient != 0) {
+ this.dataDoc.startForces = [gravityForce, normalForce, frictionForce];
+ this.dataDoc.updatedForces = [gravityForce, normalForce, frictionForce];
+ this.dataDoc.componentForces = [frictionForceComponent, normalForceComponent, gravityParallel, gravityPerpendicular];
+ } else {
+ this.dataDoc.startForces = [gravityForce, normalForce];
+ this.dataDoc.updatedForces = [gravityForce, normalForce];
+ this.dataDoc.componentForces = [normalForceComponent, gravityParallel, gravityPerpendicular];
+ }
};
- if (coefficient != 0) {
- this.dataDoc.startForces = [gravityForce, normalForce, frictionForce];
- this.dataDoc.updatedForces = [gravityForce, normalForce, frictionForce];
- this.dataDoc.componentForces = [
- frictionForceComponent,
- normalForceComponent,
- gravityParallel,
- gravityPerpendicular,
- ];
- } else {
- this.dataDoc.startForces = [gravityForce, normalForce];
- this.dataDoc.updatedForces = [gravityForce, normalForce];
- this.dataDoc.componentForces = [
- normalForceComponent,
- gravityParallel,
- gravityPerpendicular,
- ];
- }
- };
- // Change wedge height and width and weight position to match new wedge angle
- changeWedgeBasedOnNewAngle = (angle: number) => {
- this.dataDoc.wedgeWidth = this.xMax*0.5;
- this.dataDoc.wedgeHeight = Math.tan((angle * Math.PI) / 180) * this.xMax*0.5;
+ // Change wedge height and width and weight position to match new wedge angle
+ changeWedgeBasedOnNewAngle = (angle: number) => {
+ this.dataDoc.wedgeWidth = this.xMax * 0.5;
+ this.dataDoc.wedgeHeight = Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5;
- // update weight position based on updated wedge width/height
- let yPos = this.yMax - (0.08*this.layoutDoc._height*2) - Math.tan((angle * Math.PI) / 180) * this.xMax*0.5;
+ // update weight position based on updated wedge width/height
+ let yPos = this.yMax - 0.08 * this.layoutDoc[HeightSym]() * 2 - Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5;
- // adjust y position
- if (angle >= 5 && angle < 10) {
- yPos += 0.08*this.layoutDoc._height*0.1
- } else if (angle >= 10 && angle < 15) {
- yPos += 0.08*this.layoutDoc._height*0.23
- } else if (angle >= 15 && angle < 20) {
- yPos += 0.08*this.layoutDoc._height*0.26
- } else if (angle >= 20 && angle < 25) {
- yPos += 0.08*this.layoutDoc._height*0.33
- } else if (angle >= 25 && angle < 30) {
- yPos += 0.08*this.layoutDoc._height*0.35
- } else if (angle >= 30 && angle < 35) {
- yPos += 0.08*this.layoutDoc._height*0.40
- } else if (angle >= 35 && angle < 40) {
- yPos += 0.08*this.layoutDoc._height*0.45
- } else if (angle >= 40 && angle < 45) {
- yPos += 0.08*this.layoutDoc._height*0.47
- } else if (angle >= 45) {
- yPos += 0.08*this.layoutDoc._height*0.52
- }
-
-
- this.dataDoc.startPosX = this.xMax*0.25;
- this.dataDoc.startPosY = yPos;
- if (this.dataDoc.mode == "Freeform") {
- this.updateForcesWithFriction(
- Number(this.dataDoc.coefficientOfStaticFriction),
- this.xMax*0.5,
- Math.tan((angle * Math.PI) / 180) * this.xMax*0.5
- );
- }
- };
+ // adjust y position
+ if (angle >= 5 && angle < 10) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.1;
+ } else if (angle >= 10 && angle < 15) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.23;
+ } else if (angle >= 15 && angle < 20) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.26;
+ } else if (angle >= 20 && angle < 25) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.33;
+ } else if (angle >= 25 && angle < 30) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.35;
+ } else if (angle >= 30 && angle < 35) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.4;
+ } else if (angle >= 35 && angle < 40) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.45;
+ } else if (angle >= 40 && angle < 45) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.47;
+ } else if (angle >= 45) {
+ yPos += 0.08 * this.layoutDoc[HeightSym]() * 0.52;
+ }
- // In review mode, update forces when coefficient of static friction changed
- updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
- let theta: number = Number(this.dataDoc.wedgeAngle);
- let index =
- this.dataDoc.selectedQuestion.variablesForQuestionSetup.indexOf("theta - max 45");
- if (index >= 0) {
- theta = this.dataDoc.questionVariables[index];
- }
- if (isNaN(theta)) {
- return;
- }
- this.dataDoc.reviewGravityMagnitude = (Math.abs(this.dataDoc.gravity));
- this.dataDoc.reviewGravityAngle = (270);
- this.dataDoc.reviewNormalMagnitude = (
- Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180)
- );
- this.dataDoc.reviewNormalAngle = (90 - theta);
- let yForce = -Math.abs(this.dataDoc.gravity);
- yForce +=
- Math.abs(this.dataDoc.gravity) *
- Math.cos((theta * Math.PI) / 180) *
- Math.sin(((90 - theta) * Math.PI) / 180);
- yForce +=
- coefficient *
- Math.abs(this.dataDoc.gravity) *
- Math.cos((theta * Math.PI) / 180) *
- Math.sin(((180 - theta) * Math.PI) / 180);
- let friction =
- coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180);
- if (yForce > 0) {
- friction =
- (-(Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180)) *
- Math.sin(((90 - theta) * Math.PI) / 180) +
- Math.abs(this.dataDoc.gravity)) /
- Math.sin(((180 - theta) * Math.PI) / 180);
- }
- this.dataDoc.reviewStaticMagnitude = (friction);
- this.dataDoc.reviewStaticAngle = (180 - theta);
- };
+ this.dataDoc.startPosX = this.xMax * 0.25;
+ this.dataDoc.startPosY = yPos;
+ if (this.dataDoc.mode == 'Freeform') {
+ this.updateForcesWithFriction(Number(this.dataDoc.coefficientOfStaticFriction), this.xMax * 0.5, Math.tan((angle * Math.PI) / 180) * this.xMax * 0.5);
+ }
+ };
- // In review mode, update forces when wedge angle changed
- updateReviewForcesBasedOnAngle = (angle: number) => {
- this.dataDoc.reviewGravityMagnitude = (Math.abs(this.dataDoc.gravity));
- this.dataDoc.reviewGravityAngle = (270);
- this.dataDoc.reviewNormalMagnitude = (
- Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180)
- );
- this.dataDoc.reviewNormalAngle = (90 - angle);
- let yForce = -Math.abs(this.dataDoc.gravity);
- yForce +=
- Math.abs(this.dataDoc.gravity) *
- Math.cos((Number(angle) * Math.PI) / 180) *
- Math.sin(((90 - Number(angle)) * Math.PI) / 180);
- yForce +=
- this.dataDoc.reviewCoefficient *
- Math.abs(this.dataDoc.gravity) *
- Math.cos((Number(angle) * Math.PI) / 180) *
- Math.sin(((180 - Number(angle)) * Math.PI) / 180);
- let friction =
- this.dataDoc.reviewCoefficient *
- Math.abs(this.dataDoc.gravity) *
- Math.cos((Number(angle) * Math.PI) / 180);
- if (yForce > 0) {
- friction =
- (-(Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180)) *
- Math.sin(((90 - Number(angle)) * Math.PI) / 180) +
- Math.abs(this.dataDoc.gravity)) /
- Math.sin(((180 - Number(angle)) * Math.PI) / 180);
- }
- this.dataDoc.reviewStaticMagnitude = (friction);
- this.dataDoc.reviewStaticAngle = (180 - angle);
- };
+ // In review mode, update forces when coefficient of static friction changed
+ updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
+ let theta: number = Number(this.dataDoc.wedgeAngle);
+ let index = this.dataDoc.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
+ if (index >= 0) {
+ theta = this.dataDoc.questionVariables[index];
+ }
+ if (isNaN(theta)) {
+ return;
+ }
+ this.dataDoc.reviewGravityMagnitude = Math.abs(this.dataDoc.gravity);
+ this.dataDoc.reviewGravityAngle = 270;
+ this.dataDoc.reviewNormalMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180);
+ this.dataDoc.reviewNormalAngle = 90 - theta;
+ let yForce = -Math.abs(this.dataDoc.gravity);
+ yForce += Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180);
+ yForce += coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180);
+ let friction = coefficient * Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180);
+ if (yForce > 0) {
+ friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180);
+ }
+ this.dataDoc.reviewStaticMagnitude = friction;
+ this.dataDoc.reviewStaticAngle = 180 - theta;
+ };
- // Solve for the correct answers to the generated problem
- getAnswersToQuestion = (
- question: QuestionTemplate,
- questionVars: number[]
- ) => {
- const solutions: number[] = [];
+ // In review mode, update forces when wedge angle changed
+ updateReviewForcesBasedOnAngle = (angle: number) => {
+ this.dataDoc.reviewGravityMagnitude = Math.abs(this.dataDoc.gravity);
+ this.dataDoc.reviewGravityAngle = 270;
+ this.dataDoc.reviewNormalMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180);
+ this.dataDoc.reviewNormalAngle = 90 - angle;
+ let yForce = -Math.abs(this.dataDoc.gravity);
+ yForce += Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((90 - Number(angle)) * Math.PI) / 180);
+ yForce += this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180) * Math.sin(((180 - Number(angle)) * Math.PI) / 180);
+ let friction = this.dataDoc.reviewCoefficient * Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180);
+ if (yForce > 0) {
+ friction = (-(Math.abs(this.dataDoc.gravity) * Math.cos((Number(angle) * Math.PI) / 180)) * Math.sin(((90 - Number(angle)) * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin(((180 - Number(angle)) * Math.PI) / 180);
+ }
+ this.dataDoc.reviewStaticMagnitude = friction;
+ this.dataDoc.reviewStaticAngle = 180 - angle;
+ };
- let theta: number = Number(this.dataDoc.wedgeAngle);
- let index = question.variablesForQuestionSetup.indexOf("theta - max 45");
- if (index >= 0) {
- theta = questionVars[index];
- }
- let muS: number = Number(this.dataDoc.coefficientOfStaticFriction);
- index = question.variablesForQuestionSetup.indexOf(
- "coefficient of static friction"
- );
- if (index >= 0) {
- muS = questionVars[index];
- }
+ // Solve for the correct answers to the generated problem
+ getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => {
+ const solutions: number[] = [];
- for (let i = 0; i < question.answerSolutionDescriptions.length; i++) {
- const description = question.answerSolutionDescriptions[i];
- if (!isNaN(Number(description))) {
- solutions.push(Number(description));
- } else if (description == "solve normal force angle from wedge angle") {
- solutions.push(90 - theta);
- } else if (
- description == "solve normal force magnitude from wedge angle"
- ) {
- solutions.push(Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI));
- } else if (
- description ==
- "solve static force magnitude from wedge angle given equilibrium"
- ) {
- let normalForceMagnitude =
- Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI);
- let normalForceAngle = 90 - theta;
- let frictionForceAngle = 180 - theta;
- let frictionForceMagnitude =
- (-normalForceMagnitude *
- Math.sin((normalForceAngle * Math.PI) / 180) +
- Math.abs(this.dataDoc.gravity)) /
- Math.sin((frictionForceAngle * Math.PI) / 180);
- solutions.push(frictionForceMagnitude);
- } else if (
- description ==
- "solve static force angle from wedge angle given equilibrium"
- ) {
- solutions.push(180 - theta);
- } else if (
- description ==
- "solve minimum static coefficient from wedge angle given equilibrium"
- ) {
- let normalForceMagnitude =
- Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI);
- let normalForceAngle = 90 - theta;
- let frictionForceAngle = 180 - theta;
- let frictionForceMagnitude =
- (-normalForceMagnitude *
- Math.sin((normalForceAngle * Math.PI) / 180) +
- Math.abs(this.dataDoc.gravity)) /
- Math.sin((frictionForceAngle * Math.PI) / 180);
- let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
- solutions.push(frictionCoefficient);
- } else if (
- description ==
- "solve maximum wedge angle from coefficient of static friction given equilibrium"
- ) {
- solutions.push((Math.atan(muS) * 180) / Math.PI);
- }
- }
- this.dataDoc.selectedSolutions = (solutions);
- return solutions;
- };
+ let theta: number = Number(this.dataDoc.wedgeAngle);
+ let index = question.variablesForQuestionSetup.indexOf('theta - max 45');
+ if (index >= 0) {
+ theta = questionVars[index];
+ }
+ let muS: number = Number(this.dataDoc.coefficientOfStaticFriction);
+ index = question.variablesForQuestionSetup.indexOf('coefficient of static friction');
+ if (index >= 0) {
+ muS = questionVars[index];
+ }
- // In review mode, check if input answers match correct answers and optionally generate alert
- checkAnswers = (showAlert: boolean = true) => {
- let error: boolean = false;
- let epsilon: number = 0.01;
- if (this.dataDoc.selectedQuestion) {
- for (let i = 0; i < this.dataDoc.selectedQuestion.answerParts.length; i++) {
- if (this.dataDoc.selectedQuestion.answerParts[i] == "force of gravity") {
- if (
- Math.abs(this.dataDoc.reviewGravityMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon
- ) {
- error = true;
- }
- } else if (this.dataDoc.selectedQuestion.answerParts[i] == "angle of gravity") {
- if (Math.abs(this.dataDoc.reviewGravityAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
- error = true;
- }
- } else if (this.dataDoc.selectedQuestion.answerParts[i] == "normal force") {
- if (
- Math.abs(this.dataDoc.reviewNormalMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon
- ) {
- error = true;
- }
- } else if (this.dataDoc.selectedQuestion.answerParts[i] == "angle of normal force") {
- if (Math.abs(this.dataDoc.reviewNormalAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
- error = true;
- }
- } else if (
- this.dataDoc.selectedQuestion.answerParts[i] == "force of static friction"
- ) {
- if (
- Math.abs(this.dataDoc.reviewStaticMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon
- ) {
- error = true;
- }
- } else if (
- this.dataDoc.selectedQuestion.answerParts[i] == "angle of static friction"
- ) {
- if (Math.abs(this.dataDoc.reviewStaticAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
- error = true;
- }
- } else if (
- this.dataDoc.selectedQuestion.answerParts[i] == "coefficient of static friction"
- ) {
- if (
- Math.abs(
- Number(this.dataDoc.coefficientOfStaticFriction) - this.dataDoc.selectedSolutions[i]
- ) > epsilon
- ) {
- error = true;
- }
- } else if (this.dataDoc.selectedQuestion.answerParts[i] == "wedge angle") {
- if (Math.abs(Number(this.dataDoc.wedgeAngle) - this.dataDoc.selectedSolutions[i]) > epsilon) {
- error = true;
- }
+ for (let i = 0; i < question.answerSolutionDescriptions.length; i++) {
+ const description = question.answerSolutionDescriptions[i];
+ if (!isNaN(Number(description))) {
+ solutions.push(Number(description));
+ } else if (description == 'solve normal force angle from wedge angle') {
+ solutions.push(90 - theta);
+ } else if (description == 'solve normal force magnitude from wedge angle') {
+ solutions.push(Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI));
+ } else if (description == 'solve static force magnitude from wedge angle given equilibrium') {
+ let normalForceMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+ solutions.push(frictionForceMagnitude);
+ } else if (description == 'solve static force angle from wedge angle given equilibrium') {
+ solutions.push(180 - theta);
+ } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') {
+ let normalForceMagnitude = Math.abs(this.dataDoc.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.dataDoc.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+ let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
+ solutions.push(frictionCoefficient);
+ } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
+ solutions.push((Math.atan(muS) * 180) / Math.PI);
+ }
}
- }
- }
- if (showAlert) {
- if (!error) {
- this.dataDoc.simulationPaused = (false);
- setTimeout(() => {
- this.dataDoc.simulationPaused = (true);
- }, 3000);
- } else {
- this.dataDoc.simulationPaused = (false);
- setTimeout(() => {
- this.dataDoc.simulationPaused = (true);
- }, 3000);
- }
- }
- if (this.dataDoc.selectedQuestion.goal == "noMovement") {
- if (!error) {
- this.dataDoc.noMovement = (true);
- } else {
- this.dataDoc.roMovement = (false);
- }
- }
- };
+ this.dataDoc.selectedSolutions = solutions;
+ return solutions;
+ };
- // Reset all review values to default
- resetReviewValuesToDefault = () => {
- this.dataDoc.reviewGravityMagnitude = (0);
- this.dataDoc.reviewGravityAngle = (0);
- this.dataDoc.reviewNormalMagnitude = (0);
- this.dataDoc.reviewNormalAngle = (0);
- this.dataDoc.reviewStaticMagnitude = (0);
- this.dataDoc.reviewStaticAngle = (0);
- this.dataDoc.coefficientOfKineticFriction = (0);
- this.dataDoc.simulationPaused = (true);
- };
+ // In review mode, check if input answers match correct answers and optionally generate alert
+ checkAnswers = (showAlert: boolean = true) => {
+ let error: boolean = false;
+ let epsilon: number = 0.01;
+ if (this.dataDoc.selectedQuestion) {
+ for (let i = 0; i < this.dataDoc.selectedQuestion.answerParts.length; i++) {
+ if (this.dataDoc.selectedQuestion.answerParts[i] == 'force of gravity') {
+ if (Math.abs(this.dataDoc.reviewGravityMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of gravity') {
+ if (Math.abs(this.dataDoc.reviewGravityAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'normal force') {
+ if (Math.abs(this.dataDoc.reviewNormalMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of normal force') {
+ if (Math.abs(this.dataDoc.reviewNormalAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'force of static friction') {
+ if (Math.abs(this.dataDoc.reviewStaticMagnitude - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'angle of static friction') {
+ if (Math.abs(this.dataDoc.reviewStaticAngle - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'coefficient of static friction') {
+ if (Math.abs(Number(this.dataDoc.coefficientOfStaticFriction) - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.dataDoc.selectedQuestion.answerParts[i] == 'wedge angle') {
+ if (Math.abs(Number(this.dataDoc.wedgeAngle) - this.dataDoc.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ }
+ }
+ }
+ if (showAlert) {
+ if (!error) {
+ this.dataDoc.simulationPaused = false;
+ setTimeout(() => {
+ this.dataDoc.simulationPaused = true;
+ }, 3000);
+ } else {
+ this.dataDoc.simulationPaused = false;
+ setTimeout(() => {
+ this.dataDoc.simulationPaused = true;
+ }, 3000);
+ }
+ }
+ if (this.dataDoc.selectedQuestion.goal == 'noMovement') {
+ if (!error) {
+ this.dataDoc.noMovement = true;
+ } else {
+ this.dataDoc.roMovement = false;
+ }
+ }
+ };
- // In review mode, reset problem variables and generate a new question
- generateNewQuestion = () => {
- this.resetReviewValuesToDefault();
+ // Reset all review values to default
+ resetReviewValuesToDefault = () => {
+ this.dataDoc.reviewGravityMagnitude = 0;
+ this.dataDoc.reviewGravityAngle = 0;
+ this.dataDoc.reviewNormalMagnitude = 0;
+ this.dataDoc.reviewNormalAngle = 0;
+ this.dataDoc.reviewStaticMagnitude = 0;
+ this.dataDoc.reviewStaticAngle = 0;
+ this.dataDoc.coefficientOfKineticFriction = 0;
+ this.dataDoc.simulationPaused = true;
+ };
+
+ // In review mode, reset problem variables and generate a new question
+ generateNewQuestion = () => {
+ this.resetReviewValuesToDefault();
- const vars: number[] = [];
- let question: QuestionTemplate = questions.inclinePlane[0];
+ const vars: number[] = [];
+ let question: QuestionTemplate = questions.inclinePlane[0];
- if (this.dataDoc.simulationType == "Inclined Plane") {
- if (this.dataDoc.questionNumber == questions.inclinePlane.length - 1) {
- this.dataDoc.questionNumber = (0);
- } else {
- this.dataDoc.questionNumber =(this.dataDoc.questionNumber + 1);
- }
- question = questions.inclinePlane[this.dataDoc.questionNumber];
+ if (this.dataDoc.simulationType == 'Inclined Plane') {
+ if (this.dataDoc.questionNumber == questions.inclinePlane.length - 1) {
+ this.dataDoc.questionNumber = 0;
+ } else {
+ this.dataDoc.questionNumber = this.dataDoc.questionNumber + 1;
+ }
+ question = questions.inclinePlane[this.dataDoc.questionNumber];
- let coefficient = 0;
- let wedgeAngle = 0;
+ let coefficient = 0;
+ let wedgeAngle = 0;
- for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
- if (question.variablesForQuestionSetup[i] == "theta - max 45") {
- let randValue = Math.floor(Math.random() * 44 + 1);
- vars.push(randValue);
- wedgeAngle = randValue;
- } else if (
- question.variablesForQuestionSetup[i] ==
- "coefficient of static friction"
- ) {
- let randValue = Math.round(Math.random() * 1000) / 1000;
- vars.push(randValue);
- coefficient = randValue;
+ for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
+ if (question.variablesForQuestionSetup[i] == 'theta - max 45') {
+ let randValue = Math.floor(Math.random() * 44 + 1);
+ vars.push(randValue);
+ wedgeAngle = randValue;
+ } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') {
+ let randValue = Math.round(Math.random() * 1000) / 1000;
+ vars.push(randValue);
+ coefficient = randValue;
+ }
+ }
+ this.dataDoc.wedgeAngle = wedgeAngle;
+ this.changeWedgeBasedOnNewAngle(wedgeAngle);
+ this.dataDoc.coefficientOfStaticFriction = coefficient;
+ this.dataDoc.reviewCoefficient = coefficient;
}
- }
- this.dataDoc.wedgeAngle = (wedgeAngle);
- this.changeWedgeBasedOnNewAngle(wedgeAngle);
- this.dataDoc.coefficientOfStaticFriction = (coefficient);
- this.dataDoc.reviewCoefficient = coefficient;
- }
- let q = "";
- for (let i = 0; i < question.questionSetup.length; i++) {
- q += question.questionSetup[i];
- if (i != question.questionSetup.length - 1) {
- q += vars[i];
- if (question.variablesForQuestionSetup[i].includes("theta")) {
- q +=
- " degree (≈" +
- Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 +
- " rad)";
+ let q = '';
+ for (let i = 0; i < question.questionSetup.length; i++) {
+ q += question.questionSetup[i];
+ if (i != question.questionSetup.length - 1) {
+ q += vars[i];
+ if (question.variablesForQuestionSetup[i].includes('theta')) {
+ q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
+ }
+ }
}
- }
- }
- this.dataDoc.questionVariables = vars;
- this.dataDoc.selectedQuestion = (question);
- this.dataDoc.questionPartOne = (q);
- this.dataDoc.questionPartTwo = (question.question);
- this.dataDoc.answers = this.getAnswersToQuestion(question, vars);
- //this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- };
-
- // Default setup for uniform circular motion simulation
- setupCircular = (value: number) => {
- this.dataDoc.showComponentForces = (false);
- this.dataDoc.startVelY = (0);
- this.dataDoc.startVelX = (value);
- let xPos = (this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height);
- let yPos = (this.yMax + this.yMin) / 2 + this.dataDoc.circularMotionRadius - (0.08*this.layoutDoc._height);
- this.dataDoc.startPosY = (yPos);
- this.dataDoc.startPosX = (xPos);
- const tensionForce: IForce = {
- description: "Centripetal Force",
- magnitude: (this.dataDoc.startVelX ** 2 * this.dataDoc.mass) / this.dataDoc.circularMotionRadius,
- directionInDegrees: 90,
- component: false,
+ this.dataDoc.questionVariables = vars;
+ this.dataDoc.selectedQuestion = question;
+ this.dataDoc.questionPartOne = q;
+ this.dataDoc.questionPartTwo = question.question;
+ this.dataDoc.answers = this.getAnswersToQuestion(question, vars);
+ //this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
};
- this.dataDoc.updatedForces = ([tensionForce]);
- this.dataDoc.startForces = ([tensionForce]);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- };
- // Default setup for pendulum simulation
- setupPendulum = () => {
- const length = 300;
- const angle = 30;
- const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
- const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
- const xPos = this.xMax / 2 - x - (0.08*this.layoutDoc._height);
- const yPos = y - (0.08*this.layoutDoc._height) - 5;
- this.dataDoc.startPosX = (xPos);
- this.dataDoc.startPosY = (yPos);
- const mag = this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin((60 * Math.PI) / 180);
- const forceOfTension: IForce = {
- description: "Tension",
- magnitude: mag,
- directionInDegrees: 90 - angle,
- component: false,
+ // Default setup for uniform circular motion simulation
+ setupCircular = (value: number) => {
+ this.dataDoc.showComponentForces = false;
+ this.dataDoc.startVelY = 0;
+ this.dataDoc.startVelX = value;
+ let xPos = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ let yPos = (this.yMax + this.yMin) / 2 + this.dataDoc.circularMotionRadius - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.startPosY = yPos;
+ this.dataDoc.startPosX = xPos;
+ const tensionForce: IForce = {
+ description: 'Centripetal Force',
+ magnitude: (this.dataDoc.startVelX ** 2 * this.dataDoc.mass) / this.dataDoc.circularMotionRadius,
+ directionInDegrees: 90,
+ component: false,
+ };
+ this.dataDoc.updatedForces = [tensionForce];
+ this.dataDoc.startForces = [tensionForce];
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
};
- const tensionComponent: IForce = {
- description: "Tension",
- magnitude: mag,
- directionInDegrees: 90 - angle,
- component: true,
- };
- const gravityParallel: IForce = {
- description: "Gravity Parallel Component",
- magnitude:
- this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
- directionInDegrees: -angle - 90,
- component: true,
- };
- const gravityPerpendicular: IForce = {
- description: "Gravity Perpendicular Component",
- magnitude:
- this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
- directionInDegrees: -angle,
- component: true,
- };
+ // Default setup for pendulum simulation
+ setupPendulum = () => {
+ const length = 300;
+ const angle = 30;
+ const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ const xPos = this.xMax / 2 - x - 0.08 * this.layoutDoc[HeightSym]();
+ const yPos = y - 0.08 * this.layoutDoc[HeightSym]() - 5;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = yPos;
+ const mag = this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin((60 * Math.PI) / 180);
+ const forceOfTension: IForce = {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: 90 - angle,
+ component: false,
+ };
- this.dataDoc.componentForces = ([
- tensionComponent,
- gravityParallel,
- gravityPerpendicular,
- ]);
- this.dataDoc.updatedForces = ([
- {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- },
- forceOfTension,
- ]);
- this.dataDoc.startForces = ([
- {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- },
- forceOfTension,
- ]);
- this.dataDoc.startPendulumAngle = (30);
- this.dataDoc.pendulumAngle = (30);
- this.dataDoc.pendulumLength = (300);
- this.dataDoc.adjustPendulumAngle = ({ angle: 30, length: 300 });
- };
+ const tensionComponent: IForce = {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: 90 - angle,
+ component: true,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -angle - 90,
+ component: true,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -angle,
+ component: true,
+ };
- // Default setup for spring simulation
- setupSpring = () => {
- this.dataDoc.showComponentForces = (false);
- const gravityForce: IForce = {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
+ this.dataDoc.componentForces = [tensionComponent, gravityParallel, gravityPerpendicular];
+ this.dataDoc.updatedForces = [
+ {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ },
+ forceOfTension,
+ ];
+ this.dataDoc.startForces = [
+ {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ },
+ forceOfTension,
+ ];
+ this.dataDoc.startPendulumAngle = 30;
+ this.dataDoc.pendulumAngle = 30;
+ this.dataDoc.pendulumLength = 300;
+ this.dataDoc.adjustPendulumAngle = { angle: 30, length: 300 };
};
- this.dataDoc.updatedForces = ([gravityForce]);
- this.dataDoc.startForces = ([gravityForce]);
- this.dataDoc.startPosX = (this.xMax / 2 - (0.08*this.layoutDoc._height));
- this.dataDoc.startPosY = (200);
- this.dataDoc.springConstant = (0.5);
- this.dataDoc.springRestLength = (200);
- this.dataDoc.springStartLength = (200);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- };
- // Default setup for suspension simulation
- setupSuspension = () => {
- let xPos = (this.xMax + this.xMin) / 2 - (0.08*this.layoutDoc._height);
- let yPos = this.yMin + 200;
- this.dataDoc.startPosY = (yPos);
- this.dataDoc.startPosX = (xPos);
- this.dataDoc.positionYDisplay = (this.getDisplayYPos(yPos));
- this.dataDoc.positionXDisplay = (xPos);
- let tensionMag = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / (2 * Math.sin(Math.PI / 4));
- const tensionForce1: IForce = {
- description: "Tension",
- magnitude: tensionMag,
- directionInDegrees: 45,
- component: false,
- };
- const tensionForce2: IForce = {
- description: "Tension",
- magnitude: tensionMag,
- directionInDegrees: 135,
- component: false,
- };
- const grav: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
+ // Default setup for spring simulation
+ setupSpring = () => {
+ this.dataDoc.showComponentForces = false;
+ const gravityForce: IForce = {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ };
+ this.dataDoc.updatedForces = [gravityForce];
+ this.dataDoc.startForces = [gravityForce];
+ this.dataDoc.startPosX = this.xMax / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ this.dataDoc.startPosY = 200;
+ this.dataDoc.springConstant = 0.5;
+ this.dataDoc.springRestLength = 200;
+ this.dataDoc.springStartLength = 200;
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
};
- this.dataDoc.updatedForces = ([tensionForce1, tensionForce2, grav]);
- this.dataDoc.startForces = ([tensionForce1, tensionForce2, grav]);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- };
- // Default setup for pulley simulation
- setupPulley = () => {
- this.dataDoc.showComponentForces = (false);
- this.dataDoc.startPosY = ((this.yMax + this.yMin) / 2);
- this.dataDoc.startPosX = ((this.xMin + this.xMax) / 2 - 2*(0.08*this.layoutDoc._height)-5);
- this.dataDoc.positionYDisplay = (this.getDisplayYPos((this.yMax + this.yMin) / 2));
- this.dataDoc.positionXDisplay = ((this.xMin + this.xMax) / 2 - 2*(0.08*this.layoutDoc._height)-5);
- let a = (-1 * ((this.dataDoc.mass - this.dataDoc.mass2) * Math.abs(this.dataDoc.gravity))) / (this.dataDoc.mass + this.dataDoc.mass2);
- const gravityForce1: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- };
- const tensionForce1: IForce = {
- description: "Tension",
- magnitude: this.dataDoc.mass * a + this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 90,
- component: false,
- };
- a *= -1;
- const gravityForce2: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- };
- const tensionForce2: IForce = {
- description: "Tension",
- magnitude: this.dataDoc.mass2 * a + this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 90,
- component: false,
+ // Default setup for suspension simulation
+ setupSuspension = () => {
+ let xPos = (this.xMax + this.xMin) / 2 - 0.08 * this.layoutDoc[HeightSym]();
+ let yPos = this.yMin + 200;
+ this.dataDoc.startPosY = yPos;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.positionYDisplay = this.getDisplayYPos(yPos);
+ this.dataDoc.positionXDisplay = xPos;
+ let tensionMag = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / (2 * Math.sin(Math.PI / 4));
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag,
+ directionInDegrees: 45,
+ component: false,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag,
+ directionInDegrees: 135,
+ component: false,
+ };
+ const grav: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ this.dataDoc.updatedForces = [tensionForce1, tensionForce2, grav];
+ this.dataDoc.startForces = [tensionForce1, tensionForce2, grav];
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
};
- this.dataDoc.updatedForces = ([gravityForce1, tensionForce1]);
- this.dataDoc.startForces = ([gravityForce1, tensionForce1]);
- this.dataDoc.startPosY2 = ((this.yMax + this.yMin) / 2);
- this.dataDoc.startPosX2 = ((this.xMin + this.xMax) / 2 + 5 );
- this.dataDoc.positionYDisplay2 = (this.getDisplayYPos((this.yMax + this.yMin) / 2));
- this.dataDoc.positionXDisplay2 = ((this.xMin + this.xMax) / 2 + 5);
- this.dataDoc.updatedForces2 = ([gravityForce2, tensionForce2]);
- this.dataDoc.startForces2 = ([gravityForce2, tensionForce2]);
-
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- };
+ // Default setup for pulley simulation
+ setupPulley = () => {
+ this.dataDoc.showComponentForces = false;
+ this.dataDoc.startPosY = (this.yMax + this.yMin) / 2;
+ this.dataDoc.startPosX = (this.xMin + this.xMax) / 2 - 2 * (0.08 * this.layoutDoc[HeightSym]()) - 5;
+ this.dataDoc.positionYDisplay = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+ this.dataDoc.positionXDisplay = (this.xMin + this.xMax) / 2 - 2 * (0.08 * this.layoutDoc[HeightSym]()) - 5;
+ let a = (-1 * ((this.dataDoc.mass - this.dataDoc.mass2) * Math.abs(this.dataDoc.gravity))) / (this.dataDoc.mass + this.dataDoc.mass2);
+ const gravityForce1: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: this.dataDoc.mass * a + this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 90,
+ component: false,
+ };
+ a *= -1;
+ const gravityForce2: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: this.dataDoc.mass2 * a + this.dataDoc.mass2 * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 90,
+ component: false,
+ };
- // Helper function used for tutorial and review mode
- getForceFromJSON = (
- json: {
- description: string;
- magnitude: number;
- directionInDegrees: number;
- component: boolean;
- }[]
- ): IForce[] => {
- const forces: IForce[] = [];
- for (let i = 0; i < json.length; i++) {
- const force: IForce = {
- description: json[i].description,
- magnitude: json[i].magnitude,
- directionInDegrees: json[i].directionInDegrees,
- component: json[i].component,
- };
- forces.push(force);
- }
- return forces;
- };
+ this.dataDoc.updatedForces = [gravityForce1, tensionForce1];
+ this.dataDoc.startForces = [gravityForce1, tensionForce1];
+ this.dataDoc.startPosY2 = (this.yMax + this.yMin) / 2;
+ this.dataDoc.startPosX2 = (this.xMin + this.xMax) / 2 + 5;
+ this.dataDoc.positionYDisplay2 = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+ this.dataDoc.positionXDisplay2 = (this.xMin + this.xMax) / 2 + 5;
+ this.dataDoc.updatedForces2 = [gravityForce2, tensionForce2];
+ this.dataDoc.startForces2 = [gravityForce2, tensionForce2];
- // Handle force change in review mode
- updateReviewModeValues = () => {
- const forceOfGravityReview: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.reviewGravityMagnitude,
- directionInDegrees: this.dataDoc.reviewGravityAngle,
- component: false,
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
};
- const normalForceReview: IForce = {
- description: "Normal Force",
- magnitude: this.dataDoc.reviewNormalMagnitude,
- directionInDegrees: this.dataDoc.reviewNormalAngle,
- component: false,
+
+ // Helper function used for tutorial and review mode
+ getForceFromJSON = (
+ json: {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ component: boolean;
+ }[]
+ ): IForce[] => {
+ const forces: IForce[] = [];
+ for (let i = 0; i < json.length; i++) {
+ const force: IForce = {
+ description: json[i].description,
+ magnitude: json[i].magnitude,
+ directionInDegrees: json[i].directionInDegrees,
+ component: json[i].component,
+ };
+ forces.push(force);
+ }
+ return forces;
};
- const staticFrictionForceReview: IForce = {
- description: "Static Friction Force",
- magnitude: this.dataDoc.reviewStaticMagnitude,
- directionInDegrees: this.dataDoc.reviewStaticAngle,
- component: false,
+
+ // Handle force change in review mode
+ updateReviewModeValues = () => {
+ const forceOfGravityReview: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.reviewGravityMagnitude,
+ directionInDegrees: this.dataDoc.reviewGravityAngle,
+ component: false,
+ };
+ const normalForceReview: IForce = {
+ description: 'Normal Force',
+ magnitude: this.dataDoc.reviewNormalMagnitude,
+ directionInDegrees: this.dataDoc.reviewNormalAngle,
+ component: false,
+ };
+ const staticFrictionForceReview: IForce = {
+ description: 'Static Friction Force',
+ magnitude: this.dataDoc.reviewStaticMagnitude,
+ directionInDegrees: this.dataDoc.reviewStaticAngle,
+ component: false,
+ };
+ this.dataDoc.startForces = [forceOfGravityReview, normalForceReview, staticFrictionForceReview];
+ this.dataDoc.updatedForces = [forceOfGravityReview, normalForceReview, staticFrictionForceReview];
};
- this.dataDoc.startForces = ([
- forceOfGravityReview,
- normalForceReview,
- staticFrictionForceReview,
- ]);
- this.dataDoc.updatedForces = ([
- forceOfGravityReview,
- normalForceReview,
- staticFrictionForceReview,
- ]);
- }
- render () {
- return (
- <div className="physicsSimApp">
- <div className="mechanicsSimulationContainer">
- <div
- className="mechanicsSimulationContentContainer"
- >
- <div className="mechanicsSimulationButtonsAndElements">
- <div className="mechanicsSimulationButtons">
- {!this.dataDoc.simulationPaused && (
- <div
- style={{
- position: "fixed",
- left: 0.10*this.layoutDoc._width + "px",
- top: 0.95*this.layoutDoc._height + "px",
- width: 0.50*this.layoutDoc._width + "px",
- }}
- >
- <LinearProgress />
- </div>
- )}
- </div>
- <div className="mechanicsSimulationElements">
- <Weight
- dataDoc={this.dataDoc}
- layoutDoc={this.layoutDoc}
- wallPositions={this.wallPositions}
- adjustPendulumAngle={this.dataDoc.adjustPendulumAngle}
- gravity={this.dataDoc.gravity}
- circularMotionRadius={this.dataDoc.circularMotionRadius}
- componentForces={this.dataDoc.componentForces}
- showComponentForces={this.dataDoc.showComponentForces}
- color={"red"}
- coefficientOfKineticFriction={Number(
- this.dataDoc.coefficientOfKineticFriction
- )}
- displayXVelocity={this.dataDoc.velocityXDisplay}
- displayYVelocity={this.dataDoc.velocityYDisplay}
- elasticCollisions={this.dataDoc.elasticCollisions}
- mass={this.dataDoc.mass}
- mode={this.dataDoc.mode}
- noMovement={this.dataDoc.noMovement}
- paused={this.dataDoc.simulationPaused}
- pendulumAngle={this.dataDoc.pendulumAngle}
- pendulumLength={this.dataDoc.pendulumLength}
- radius={(0.08*this.layoutDoc._height)}
- reset={this.dataDoc.simulationReset}
- simulationSpeed={this.dataDoc.simulationSpeed}
- startPendulumAngle={this.dataDoc.startPendulumAngle}
- showAcceleration={this.dataDoc.showAcceleration}
- showForceMagnitudes={this.dataDoc.showForceMagnitudes}
- showForces={this.dataDoc.showForces}
- showVelocity={this.dataDoc.showVelocity}
- simulationType={this.dataDoc.simulationType}
- springConstant={this.dataDoc.springConstant}
- springStartLength={this.dataDoc.springStartLength}
- springRestLength={this.dataDoc.springRestLength}
- startForces={this.dataDoc.startForces}
- startPosX={this.dataDoc.startPosX}
- startPosY={this.dataDoc.startPosY ?? 0}
- startVelX={this.dataDoc.startVelX}
- startVelY={this.dataDoc.startVelY}
- timestepSize={0.05}
- updateDisplay={this.dataDoc.displayChange}
- updatedForces={this.dataDoc.updatedForces}
- wedgeHeight={this.dataDoc.wedgeHeight}
- wedgeWidth={this.dataDoc.wedgeWidth}
- xMax={this.xMax}
- xMin={this.xMin}
- yMax={this.yMax}
- yMin={this.yMin}
- />
- {this.dataDoc.simulationType == "Pulley" && (
- <Weight
- dataDoc={this.dataDoc}
- layoutDoc={this.layoutDoc}
- wallPositions={this.wallPositions}
- adjustPendulumAngle={this.dataDoc.adjustPendulumAngle}
- circularMotionRadius={this.dataDoc.circularMotionRadius}
- gravity={this.dataDoc.gravity}
- componentForces={this.dataDoc.componentForces}
- showComponentForces={this.dataDoc.showComponentForces}
- color={"blue"}
- coefficientOfKineticFriction={Number(
- this.dataDoc.coefficientOfKineticFriction
- )}
- displayXVelocity={this.dataDoc.velocityXDisplay2}
- displayYVelocity={this.dataDoc.velocityYDisplay2}
- elasticCollisions={this.dataDoc.elasticCollisions}
- mass={this.dataDoc.mass2}
- mode={this.dataDoc.mode}
- noMovement={this.dataDoc.noMovement}
- paused={this.dataDoc.simulationPaused}
- pendulumAngle={this.dataDoc.pendulumAngle}
- pendulumLength={this.dataDoc.pendulumLength}
- radius={(0.08*this.layoutDoc._height)}
- reset={this.dataDoc.simulationReset}
- simulationSpeed={this.dataDoc.simulationSpeed}
- startPendulumAngle={this.dataDoc.startPendulumAngle}
- showAcceleration={this.dataDoc.showAcceleration}
- showForceMagnitudes={this.dataDoc.showForceMagnitudes}
- showForces={this.dataDoc.showForces}
- showVelocity={this.dataDoc.showVelocity}
- simulationType={this.dataDoc.simulationType}
- springConstant={this.dataDoc.springConstant}
- springStartLength={this.dataDoc.springStartLength}
- springRestLength={this.dataDoc.springRestLength}
- startForces={this.dataDoc.startForces2}
- startPosX={this.dataDoc.startPosX2}
- startPosY={this.dataDoc.startPosY2}
- startVelX={this.dataDoc.startVelX}
- startVelY={this.dataDoc.startVelY}
- timestepSize={0.05}
- updateDisplay={this.dataDoc.displayChange2}
- updatedForces={this.dataDoc.updatedForces2}
- wedgeHeight={this.dataDoc.wedgeHeight}
- wedgeWidth={this.dataDoc.wedgeWidth}
- xMax={this.xMax}
- xMin={this.xMin}
- yMax={this.yMax}
- yMin={this.yMin}
- />
- )}
- </div>
- <div>
- {(this.dataDoc.simulationType == "One Weight" ||
- this.dataDoc.simulationType == "Inclined Plane") && this.wallPositions &&
- this.wallPositions.map((element, index) => {
- return (
- <Wall
- key={index}
- length={element.length}
- xPos={element.xPos}
- yPos={element.yPos}
- angleInDegrees={element.angleInDegrees}
- />
- );
- })}
- </div>
- </div>
- </div>
- <div className="mechanicsSimulationEquationContainer">
- <div className="mechanicsSimulationControls">
- <Stack direction="row" spacing={1}>
- {this.dataDoc.simulationPaused && this.dataDoc.mode != "Tutorial" && (
- <IconButton
- onClick={() => {
- this.dataDoc.simulationPaused = (false);
- }}
- >
- <PlayArrowIcon />
- </IconButton>
- )}
- {!this.dataDoc.simulationPaused && this.dataDoc.mode != "Tutorial" && (
- <IconButton
- onClick={() => {
- this.dataDoc.simulationPaused = (true);
- }}
- >
- <PauseIcon />
- </IconButton>
- )}
- {this.dataDoc.simulationPaused && this.dataDoc.mode != "Tutorial" && (
- <IconButton
- onClick={() => {
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- >
- <ReplayIcon />
- </IconButton>
- )}
- </Stack>
- <div className="dropdownMenu">
- <select
- value={this.dataDoc.simulationType}
- onChange={(event) => {
- this.dataDoc.simulationType = (event.target.value);
- this.setupSimulation(event.target.value, this.dataDoc.mode)
- }}
- style={{ height: "2em", width: "100%", fontSize: "16px" }}
- >
- <option value="One Weight">Projectile</option>
- <option value="Inclined Plane">Inclined Plane</option>
- <option value="Pendulum">Pendulum</option>
- <option value="Spring">Spring</option>
- <option value="Circular Motion">Circular Motion</option>
- <option value="Pulley">Pulley</option>
- <option value="Suspension">Suspension</option>
- </select>
- </div>
- <div className="dropdownMenu">
- <select
- value={this.dataDoc.mode}
- onChange={(event) => {
- this.dataDoc.mode = (event.target.value);
- this.setupSimulation(this.dataDoc.simulationType, event.target.value)
- }}
- style={{ height: "2em", width: "100%", fontSize: "16px" }}
- >
- <option value="Tutorial">Tutorial Mode</option>
- <option value="Freeform">Freeform Mode</option>
- <option value="Review">Review Mode</option>
- </select>
- </div>
- </div>
- {this.dataDoc.mode == "Review" && this.dataDoc.simulationType != "Inclined Plane" && (
- <div className="wordProblemBox">
- <p>{this.dataDoc.simulationType} review problems in progress!</p>
- <hr/>
- </div>
- )}
- {this.dataDoc.mode == "Review" && this.dataDoc.simulationType == "Inclined Plane" && (
- <div>
- {!this.dataDoc.hintDialogueOpen && (
- <IconButton
- onClick={() => {
- this.dataDoc.hintDialogueOpen = (true);
- }}
- sx={{
- position: "fixed",
- left: this.xMax - 50 + "px",
- top: this.yMin + 14 + "px",
- }}
- >
- <QuestionMarkIcon />
- </IconButton>
- )}
- <Dialog
- maxWidth={"sm"}
- fullWidth={true}
- open={this.dataDoc.hintDialogueOpen}
- onClose={() => this.dataDoc.hintDialogueOpen = (false)}
- >
- <DialogTitle>Hints</DialogTitle>
- <DialogContent>
- {this.dataDoc.selectedQuestion.hints && (this.dataDoc.selectedQuestion.hints.map((hint, index) => {
- return (
- <div key={index}>
- <DialogContentText>
- <details>
- <summary>
- <b>
- Hint {index + 1}: {hint.description}
- </b>
- </summary>
- {hint.content}
- </details>
- </DialogContentText>
- </div>
- );
- }))}
- </DialogContent>
- <DialogActions>
- <Button
- onClick={() => {
- this.dataDoc.hintDialogueOpen = (false);
- }}
- >
- Close
- </Button>
- </DialogActions>
- </Dialog>
- <div className="wordProblemBox">
- <div className="question">
- <p>{this.dataDoc.questionPartOne}</p>
- <p>{this.dataDoc.questionPartTwo}</p>
- </div>
- <div className="answers">
- {this.dataDoc.selectedQuestion.answerParts.includes("force of gravity") && (
- <InputField
- label={<p>Gravity magnitude</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewGravityMagnitude'}
- step={0.1}
- unit={"N"}
- upperBound={50}
- value={this.dataDoc.reviewGravityMagnitude}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("force of gravity")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("angle of gravity") && (
- <InputField
- label={<p>Gravity angle</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewGravityAngle'}
- step={1}
- unit={"°"}
- upperBound={360}
- value={this.dataDoc.reviewGravityAngle}
- radianEquivalent={true}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("angle of gravity")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("normal force") && (
- <InputField
- label={<p>Normal force magnitude</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewNormalMagnitude'}
- step={0.1}
- unit={"N"}
- upperBound={50}
- value={this.dataDoc.reviewNormalMagnitude}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("normal force")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("angle of normal force") && (
- <InputField
- label={<p>Normal force angle</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewNormalAngle'}
- step={1}
- unit={"°"}
- upperBound={360}
- value={this.dataDoc.reviewNormalAngle}
- radianEquivalent={true}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("angle of normal force")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("force of static friction") && (
- <InputField
- label={<p>Static friction magnitude</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewStaticMagnitude'}
- step={0.1}
- unit={"N"}
- upperBound={50}
- value={this.dataDoc.reviewStaticMagnitude}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("force of static friction")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("angle of static friction") && (
- <InputField
- label={<p>Static friction angle</p>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'reviewStaticAngle'}
- step={1}
- unit={"°"}
- upperBound={360}
- value={this.dataDoc.reviewStaticAngle}
- radianEquivalent={true}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("angle of static friction")]}
- labelWidth={"7em"}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("coefficient of static friction") && (
- <InputField
- label={
- <Box>
- &mu;<sub>s</sub>
- </Box>
- }
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'coefficientOfStaticFriction'}
- step={0.1}
- unit={""}
- upperBound={1}
- value={this.dataDoc.coefficientOfStaticFriction}
- effect={this.updateReviewForcesBasedOnCoefficient}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("coefficient of static friction")]}
- />
- )}
- {this.dataDoc.selectedQuestion.answerParts.includes("wedge angle") && (
- <InputField
- label={<Box>&theta;</Box>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'wedgeAngle'}
- step={1}
- unit={"°"}
- upperBound={49}
- value={this.dataDoc.wedgeAngle ?? 26}
- effect={(val: number) => {
- this.changeWedgeBasedOnNewAngle(val);
- this.updateReviewForcesBasedOnAngle(val);
- }}
- radianEquivalent={true}
- showIcon={this.dataDoc.showIcon}
- correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf("wedge angle")]}
- />
- )}
- </div>
- </div>
- </div>
- )}
- {this.dataDoc.mode == "Tutorial" && (
- <div className="wordProblemBox">
- <div className="question">
- <h2>Problem</h2>
- <p>{this.dataDoc.selectedTutorial.question}</p>
- </div>
- <div
- style={{
- display: "flex",
- justifyContent: "spaceBetween",
- width: "100%",
- }}
- >
- <IconButton
- onClick={() => {
- let step = this.dataDoc.stepNumber - 1;
- step = Math.max(step, 0);
- step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1);
- this.dataDoc.stepNumber = (step);
- this.dataDoc.startForces = (
- this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces)
- );
- this.dataDoc.updatedForces = (
- this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces)
- );
- this.dataDoc.showForceMagnitudes = (
- this.dataDoc.selectedTutorial.steps[step].showMagnitude
- );
- }}
- disabled={this.dataDoc.stepNumber == 0}
- >
- <ArrowLeftIcon />
- </IconButton>
- <div>
- <h3>
- Step {this.dataDoc.stepNumber + 1}:{" "}
- {this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].description}
- </h3>
- <p>{this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].content}</p>
- </div>
- <IconButton
- onClick={() => {
- let step = this.dataDoc.stepNumber + 1;
- step = Math.max(step, 0);
- step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1);
- this.dataDoc.stepNumber = (step);
- this.dataDoc.startForces = (
- this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces)
- );
- this.dataDoc.updatedForces = (
- this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces)
- );
- this.dataDoc.showForceMagnitudes = (
- this.dataDoc.selectedTutorial.steps[step].showMagnitude
- );
- }}
- disabled={this.dataDoc.stepNumber == this.dataDoc.selectedTutorial.steps.length - 1}
- >
- <ArrowRightIcon />
- </IconButton>
- </div>
- <div>
- {(this.dataDoc.simulationType == "One Weight" ||
- this.dataDoc.simulationType == "Inclined Plane" ||
- this.dataDoc.simulationType == "Pendulum") && <p>Resources</p>}
- {this.dataDoc.simulationType == "One Weight" && (
- <ul>
- <li>
- <a
- href="https://www.khanacademy.org/science/physics/one-dimensional-motion"
- target="_blank"
- rel="noreferrer"
- style={{
- color: "blue",
- textDecoration: "underline",
- }}
- >
- Khan Academy - One Dimensional Motion
- </a>
- </li>
- <li>
- <a
- href="https://www.khanacademy.org/science/physics/two-dimensional-motion"
- target="_blank"
- rel="noreferrer"
- style={{
- color: "blue",
- textDecoration: "underline",
- }}
- >
- Khan Academy - Two Dimensional Motion
- </a>
- </li>
- </ul>
- )}
- {this.dataDoc.simulationType == "Inclined Plane" && (
- <ul>
- <li>
- <a
- href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force"
- target="_blank"
- rel="noreferrer"
- style={{
- color: "blue",
- textDecoration: "underline",
- }}
- >
- Khan Academy - Normal Force
- </a>
- </li>
- <li>
- <a
- href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction"
- target="_blank"
- rel="noreferrer"
- style={{
- color: "blue",
- textDecoration: "underline",
- }}
- >
- Khan Academy - Inclined Planes
- </a>
- </li>
- </ul>
- )}
- {this.dataDoc.simulationType == "Pendulum" && (
- <ul>
- <li>
- <a
- href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial"
- target="_blank"
- rel="noreferrer"
- style={{
- color: "blue",
- textDecoration: "underline",
- }}
- >
- Khan Academy - Tension
- </a>
- </li>
- </ul>
- )}
- </div>
- </div>
- )}
- {this.dataDoc.mode == "Review" && this.dataDoc.simulationType == "Inclined Plane" && (
- <div
- style={{
- display: "flex",
- justifyContent: "space-between",
- marginTop: "10px",
- }}
- >
- <p
- style={{
- color: "blue",
- textDecoration: "underline",
- cursor: "pointer",
- }}
- onClick={() => this.dataDoc.mode = ("Tutorial")}
- >
- {" "}
- Go to walkthrough{" "}
- </p>
- <div style={{ display: "flex", flexDirection: "column" }}>
- <Button
- onClick={() => {
- this.dataDoc.simulationReset= (!this.dataDoc.simulationReset);
- this.checkAnswers();
- this.dataDoc.showIcon = true;
- }}
- variant="outlined"
- >
- <p>Submit</p>
- </Button>
- <Button
- onClick={() => {
- this.generateNewQuestion();
- this.dataDoc.showIcon = false;
- }}
- variant="outlined"
- >
- <p>New question</p>
- </Button>
- </div>
- </div>
- )}
- {this.dataDoc.mode == "Freeform" && (
- <div className="vars">
- <FormControl component="fieldset">
- <FormGroup>
- {this.dataDoc.simulationType == "One Weight" && (
- <FormControlLabel
- control={
- <Checkbox
- checked={this.dataDoc.elasticCollisions}
- onChange={() =>
- this.dataDoc.elasticCollisions= (!this.dataDoc.elasticCollisions)
- }
- />
- }
- label="Make collisions elastic"
- labelPlacement="start"
- />
- )}
- <FormControlLabel
- control={
- <Checkbox
- checked={this.dataDoc.showForces}
- onChange={() => this.dataDoc.showForces = (!this.dataDoc.showForces)}
- />
- }
- label="Show force vectors"
- labelPlacement="start"
- />
- {(this.dataDoc.simulationType == "Inclined Plane" ||
- this.dataDoc.simulationType == "Pendulum") && (
- <FormControlLabel
- control={
- <Checkbox
- checked={this.dataDoc.showForces}
- onChange={() =>
- this.dataDoc.showComponentForces = (!this.dataDoc.showComponentForces)
- }
- />
- }
- label="Show component force vectors"
- labelPlacement="start"
- />
- )}
- <FormControlLabel
- control={
- <Checkbox
- checked={this.dataDoc.showAcceleration}
- onChange={() => this.dataDoc.showAcceleration = (!this.dataDoc.showAcceleration)}
- />
- }
- label="Show acceleration vector"
- labelPlacement="start"
- />
- <FormControlLabel
- control={
- <Checkbox
- checked={this.dataDoc.showVelocity}
- onChange={() => this.dataDoc.showVelocity = (!this.dataDoc.showVelocity)}
- />
- }
- label="Show velocity vector"
- labelPlacement="start"
- />
- <InputField
- label={<Box>Speed</Box>}
- lowerBound={1}
- dataDoc={this.dataDoc}
- prop={'simulationSpeed'}
- step={1}
- unit={"x"}
- upperBound={10}
- value={this.dataDoc.simulationSpeed ?? 2}
- labelWidth={"5em"}
- />
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType != "Circular Motion" && (
- <InputField
- label={<Box>Gravity</Box>}
- lowerBound={-30}
- dataDoc={this.dataDoc}
- prop={'gravity'}
- step={0.01}
- unit={"m/s2"}
- upperBound={0}
- value={this.dataDoc.gravity ?? -9.81}
- effect={(val: number) => {
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }}
- labelWidth={"5em"}
- />
- )}
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType != "Pulley" && (
- <InputField
- label={<Box>Mass</Box>}
- lowerBound={1}
- dataDoc={this.dataDoc}
- prop={'mass'}
- step={0.1}
- unit={"kg"}
- upperBound={5}
- value={this.dataDoc.mass ?? 1}
- effect={(val: number) => {
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }}
- labelWidth={"5em"}
- />
- )}
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType == "Pulley" && (
- <InputField
- label={<Box>Red mass</Box>}
- lowerBound={1}
- dataDoc={this.dataDoc}
- prop={'mass'}
- step={0.1}
- unit={"kg"}
- upperBound={5}
- value={this.dataDoc.mass ?? 1}
- effect={(val: number) => {
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }}
- labelWidth={"5em"}
- />
- )}
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType == "Pulley" && (
- <InputField
- label={<Box>Blue mass</Box>}
- lowerBound={1}
- dataDoc={this.dataDoc}
- prop={'mass2'}
- step={0.1}
- unit={"kg"}
- upperBound={5}
- value={this.dataDoc.mass2 ?? 1}
- effect={(val: number) => {
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }}
- labelWidth={"5em"}
- />
- )}
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType == "Circular Motion" && (
- <InputField
- label={<Box>Rod length</Box>}
- lowerBound={100}
- dataDoc={this.dataDoc}
- prop={'circularMotionRadius'}
- step={5}
- unit={"m"}
- upperBound={250}
- value={this.dataDoc.circularMotionRadius ?? 100}
- effect={(val: number) => {
- this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode)
- }}
- labelWidth={"5em"}
- />
- )}
- </FormGroup>
- </FormControl>
- {this.dataDoc.simulationType == "Spring" && this.dataDoc.simulationPaused && (
- <div>
- <InputField
- label={
- <Typography color="inherit">Spring stiffness</Typography>
- }
- lowerBound={0.1}
- dataDoc={this.dataDoc}
- prop={'springConstant'}
- step={1}
- unit={"N/m"}
- upperBound={500}
- value={this.dataDoc.springConstant ?? 0.5}
- effect={(val: number) => {
- this.dataDoc.simulationReset(!this.dataDoc.simulationReset);
- }}
- radianEquivalent={false}
- mode={"Freeform"}
- labelWidth={"7em"}
- />
- <InputField
- label={<Typography color="inherit">Rest length</Typography>}
- lowerBound={10}
- dataDoc={this.dataDoc}
- prop={'springRestLength'}
- step={100}
- unit={""}
- upperBound={500}
- value={this.dataDoc.springRestLength ?? 200}
- effect={(val: number) => {
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- radianEquivalent={false}
- mode={"Freeform"}
- labelWidth={"7em"}
- />
- <InputField
- label={
- <Typography color="inherit">
- Starting displacement
- </Typography>
- }
- lowerBound={-(this.dataDoc.springRestLength - 10)}
- dataDoc={this.dataDoc}
- prop={""}
- step={10}
- unit={""}
- upperBound={this.dataDoc.springRestLength}
- value={this.dataDoc.springStartLength - this.dataDoc.springRestLength ?? 0}
- effect={(val: number) => {
- this.dataDoc.startPosY = (this.dataDoc.springRestLength + val);
- this.dataDoc.springStartLength = (this.dataDoc.springRestLength + val);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- radianEquivalent={false}
- mode={"Freeform"}
- labelWidth={"7em"}
- />
- </div>
- )}
- {this.dataDoc.simulationType == "Inclined Plane" && this.dataDoc.simulationPaused && (
- <div>
- <InputField
- label={<Box>&theta;</Box>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'wedgeAngle'}
- step={1}
- unit={"°"}
- upperBound={49}
- value={this.dataDoc.wedgeAngle ?? 26}
- effect={(val: number) => {
- this.changeWedgeBasedOnNewAngle(val);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- radianEquivalent={true}
- mode={"Freeform"}
- labelWidth={"2em"}
- />
- <InputField
- label={
- <Box>
- &mu;<sub>s</sub>
- </Box>
- }
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'coefficientOfStaticFriction'}
- step={0.1}
- unit={""}
- upperBound={1}
- value={this.dataDoc.coefficientOfStaticFriction ?? 0}
- effect={(val: number) => {
- this.updateForcesWithFriction(val);
- if (val < Number(this.dataDoc.coefficientOfKineticFriction)) {
- this.dataDoc.soefficientOfKineticFriction = (val);
- }
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- mode={"Freeform"}
- labelWidth={"2em"}
- />
- <InputField
- label={
- <Box>
- &mu;<sub>k</sub>
- </Box>
- }
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'coefficientOfKineticFriction'}
- step={0.1}
- unit={""}
- upperBound={Number(this.dataDoc.coefficientOfStaticFriction)}
- value={this.dataDoc.coefficientOfKineticFriction ?? 0}
- effect={(val: number) => {
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- mode={"Freeform"}
- labelWidth={"2em"}
- />
- </div>
- )}
- {this.dataDoc.simulationType == "Inclined Plane" && !this.dataDoc.simulationPaused && (
- <Typography>
- &theta;: {Math.round(Number(this.dataDoc.wedgeAngle) * 100) / 100}° ≈{" "}
- {Math.round(((Number(this.dataDoc.wedgeAngle) * Math.PI) / 180) * 100) /
- 100}{" "}
- rad
- <br />
- &mu; <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction}
- <br />
- &mu; <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction}
- </Typography>
- )}
- {this.dataDoc.simulationType == "Pendulum" && !this.dataDoc.simulationPaused && (
- <Typography>
- &theta;: {Math.round(this.dataDoc.pendulumAngle * 100) / 100}° ≈{" "}
- {Math.round(((this.dataDoc.pendulumAngle * Math.PI) / 180) * 100) / 100}{" "}
- rad
- </Typography>
- )}
- {this.dataDoc.simulationType == "Pendulum" && this.dataDoc.simulationPaused && (
- <div>
- <InputField
- label={<Box>Angle</Box>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'pendulumAngle'}
- step={1}
- unit={"°"}
- upperBound={59}
- value={this.dataDoc.pendulumAngle ?? 30}
- effect={(value) => {
- this.dataDoc.startPendulumAngle = (value);
- if (this.dataDoc.simulationType == "Pendulum") {
- const mag =
- this.dataDoc.mass *
- Math.abs(this.dataDoc.gravity) *
- Math.cos((value * Math.PI) / 180);
+ render() {
+ return (
+ <div className="physicsSimApp">
+ <div className="mechanicsSimulationContainer">
+ <div className="mechanicsSimulationContentContainer">
+ <div className="mechanicsSimulationButtonsAndElements">
+ <div className="mechanicsSimulationButtons">
+ {!this.dataDoc.simulationPaused && (
+ <div
+ style={{
+ position: 'fixed',
+ left: 0.1 * this.layoutDoc[WidthSym]() + 'px',
+ top: 0.95 * this.layoutDoc[HeightSym]() + 'px',
+ width: 0.5 * this.layoutDoc[WidthSym]() + 'px',
+ }}>
+ <LinearProgress />
+ </div>
+ )}
+ </div>
+ <div className="mechanicsSimulationElements">
+ <Weight
+ dataDoc={this.dataDoc}
+ layoutDoc={this.layoutDoc}
+ wallPositions={this.wallPositions}
+ adjustPendulumAngle={this.dataDoc.adjustPendulumAngle}
+ gravity={this.dataDoc.gravity}
+ circularMotionRadius={this.dataDoc.circularMotionRadius}
+ componentForces={this.dataDoc.componentForces}
+ showComponentForces={this.dataDoc.showComponentForces}
+ color={'red'}
+ coefficientOfKineticFriction={Number(this.dataDoc.coefficientOfKineticFriction)}
+ displayXVelocity={this.dataDoc.velocityXDisplay}
+ displayYVelocity={this.dataDoc.velocityYDisplay}
+ elasticCollisions={this.dataDoc.elasticCollisions}
+ mass={this.dataDoc.mass}
+ mode={this.dataDoc.mode}
+ noMovement={this.dataDoc.noMovement}
+ paused={this.dataDoc.simulationPaused}
+ pendulumAngle={this.dataDoc.pendulumAngle}
+ pendulumLength={this.dataDoc.pendulumLength}
+ radius={0.08 * this.layoutDoc[HeightSym]()}
+ reset={this.dataDoc.simulationReset}
+ simulationSpeed={this.dataDoc.simulationSpeed}
+ startPendulumAngle={this.dataDoc.startPendulumAngle}
+ showAcceleration={this.dataDoc.showAcceleration}
+ showForceMagnitudes={this.dataDoc.showForceMagnitudes}
+ showForces={this.dataDoc.showForces}
+ showVelocity={this.dataDoc.showVelocity}
+ simulationType={this.dataDoc.simulationType}
+ springConstant={this.dataDoc.springConstant}
+ springStartLength={this.dataDoc.springStartLength}
+ springRestLength={this.dataDoc.springRestLength}
+ startForces={this.dataDoc.startForces}
+ startPosX={this.dataDoc.startPosX}
+ startPosY={this.dataDoc.startPosY ?? 0}
+ startVelX={this.dataDoc.startVelX}
+ startVelY={this.dataDoc.startVelY}
+ timestepSize={0.05}
+ updateDisplay={this.dataDoc.displayChange}
+ updatedForces={this.dataDoc.updatedForces}
+ wedgeHeight={this.dataDoc.wedgeHeight}
+ wedgeWidth={this.dataDoc.wedgeWidth}
+ xMax={this.xMax}
+ xMin={this.xMin}
+ yMax={this.yMax}
+ yMin={this.yMin}
+ />
+ {this.dataDoc.simulationType == 'Pulley' && (
+ <Weight
+ dataDoc={this.dataDoc}
+ layoutDoc={this.layoutDoc}
+ wallPositions={this.wallPositions}
+ adjustPendulumAngle={this.dataDoc.adjustPendulumAngle}
+ circularMotionRadius={this.dataDoc.circularMotionRadius}
+ gravity={this.dataDoc.gravity}
+ componentForces={this.dataDoc.componentForces}
+ showComponentForces={this.dataDoc.showComponentForces}
+ color={'blue'}
+ coefficientOfKineticFriction={Number(this.dataDoc.coefficientOfKineticFriction)}
+ displayXVelocity={this.dataDoc.velocityXDisplay2}
+ displayYVelocity={this.dataDoc.velocityYDisplay2}
+ elasticCollisions={this.dataDoc.elasticCollisions}
+ mass={this.dataDoc.mass2}
+ mode={this.dataDoc.mode}
+ noMovement={this.dataDoc.noMovement}
+ paused={this.dataDoc.simulationPaused}
+ pendulumAngle={this.dataDoc.pendulumAngle}
+ pendulumLength={this.dataDoc.pendulumLength}
+ radius={0.08 * this.layoutDoc[HeightSym]()}
+ reset={this.dataDoc.simulationReset}
+ simulationSpeed={this.dataDoc.simulationSpeed}
+ startPendulumAngle={this.dataDoc.startPendulumAngle}
+ showAcceleration={this.dataDoc.showAcceleration}
+ showForceMagnitudes={this.dataDoc.showForceMagnitudes}
+ showForces={this.dataDoc.showForces}
+ showVelocity={this.dataDoc.showVelocity}
+ simulationType={this.dataDoc.simulationType}
+ springConstant={this.dataDoc.springConstant}
+ springStartLength={this.dataDoc.springStartLength}
+ springRestLength={this.dataDoc.springRestLength}
+ startForces={this.dataDoc.startForces2}
+ startPosX={this.dataDoc.startPosX2}
+ startPosY={this.dataDoc.startPosY2}
+ startVelX={this.dataDoc.startVelX}
+ startVelY={this.dataDoc.startVelY}
+ timestepSize={0.05}
+ updateDisplay={this.dataDoc.displayChange2}
+ updatedForces={this.dataDoc.updatedForces2}
+ wedgeHeight={this.dataDoc.wedgeHeight}
+ wedgeWidth={this.dataDoc.wedgeWidth}
+ xMax={this.xMax}
+ xMin={this.xMin}
+ yMax={this.yMax}
+ yMin={this.yMin}
+ />
+ )}
+ </div>
+ <div>
+ {(this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Inclined Plane') &&
+ this.wallPositions &&
+ this.wallPositions.map((element, index) => {
+ return <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />;
+ })}
+ </div>
+ </div>
+ </div>
+ <div className="mechanicsSimulationEquationContainer">
+ <div className="mechanicsSimulationControls">
+ <Stack direction="row" spacing={1}>
+ {this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && (
+ <IconButton
+ onClick={() => {
+ this.dataDoc.simulationPaused = false;
+ }}>
+ <PlayArrowIcon />
+ </IconButton>
+ )}
+ {!this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && (
+ <IconButton
+ onClick={() => {
+ this.dataDoc.simulationPaused = true;
+ }}>
+ <PauseIcon />
+ </IconButton>
+ )}
+ {this.dataDoc.simulationPaused && this.dataDoc.mode != 'Tutorial' && (
+ <IconButton
+ onClick={() => {
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}>
+ <ReplayIcon />
+ </IconButton>
+ )}
+ </Stack>
+ <div className="dropdownMenu">
+ <select
+ value={this.dataDoc.simulationType}
+ onChange={event => {
+ this.dataDoc.simulationType = event.target.value;
+ this.setupSimulation(event.target.value, this.dataDoc.mode);
+ }}
+ style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+ <option value="One Weight">Projectile</option>
+ <option value="Inclined Plane">Inclined Plane</option>
+ <option value="Pendulum">Pendulum</option>
+ <option value="Spring">Spring</option>
+ <option value="Circular Motion">Circular Motion</option>
+ <option value="Pulley">Pulley</option>
+ <option value="Suspension">Suspension</option>
+ </select>
+ </div>
+ <div className="dropdownMenu">
+ <select
+ value={this.dataDoc.mode}
+ onChange={event => {
+ this.dataDoc.mode = event.target.value;
+ this.setupSimulation(this.dataDoc.simulationType, event.target.value);
+ }}
+ style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+ <option value="Tutorial">Tutorial Mode</option>
+ <option value="Freeform">Freeform Mode</option>
+ <option value="Review">Review Mode</option>
+ </select>
+ </div>
+ </div>
+ {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType != 'Inclined Plane' && (
+ <div className="wordProblemBox">
+ <p>{this.dataDoc.simulationType} review problems in progress!</p>
+ <hr />
+ </div>
+ )}
+ {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType == 'Inclined Plane' && (
+ <div>
+ {!this.dataDoc.hintDialogueOpen && (
+ <IconButton
+ onClick={() => {
+ this.dataDoc.hintDialogueOpen = true;
+ }}
+ sx={{
+ position: 'fixed',
+ left: this.xMax - 50 + 'px',
+ top: this.yMin + 14 + 'px',
+ }}>
+ <QuestionMarkIcon />
+ </IconButton>
+ )}
+ <Dialog maxWidth={'sm'} fullWidth={true} open={this.dataDoc.hintDialogueOpen} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
+ <DialogTitle>Hints</DialogTitle>
+ <DialogContent>
+ {this.dataDoc.selectedQuestion.hints &&
+ this.dataDoc.selectedQuestion.hints.map((hint: any, index: number) => {
+ return (
+ <div key={index}>
+ <DialogContentText>
+ <details>
+ <summary>
+ <b>
+ Hint {index + 1}: {hint.description}
+ </b>
+ </summary>
+ {hint.content}
+ </details>
+ </DialogContentText>
+ </div>
+ );
+ })}
+ </DialogContent>
+ <DialogActions>
+ <Button
+ onClick={() => {
+ this.dataDoc.hintDialogueOpen = false;
+ }}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ <div className="wordProblemBox">
+ <div className="question">
+ <p>{this.dataDoc.questionPartOne}</p>
+ <p>{this.dataDoc.questionPartTwo}</p>
+ </div>
+ <div className="answers">
+ {this.dataDoc.selectedQuestion.answerParts.includes('force of gravity') && (
+ <InputField
+ label={<p>Gravity magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewGravityMagnitude'}
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={this.dataDoc.reviewGravityMagnitude}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('force of gravity')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('angle of gravity') && (
+ <InputField
+ label={<p>Gravity angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewGravityAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={this.dataDoc.reviewGravityAngle}
+ radianEquivalent={true}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of gravity')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('normal force') && (
+ <InputField
+ label={<p>Normal force magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewNormalMagnitude'}
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={this.dataDoc.reviewNormalMagnitude}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('normal force')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('angle of normal force') && (
+ <InputField
+ label={<p>Normal force angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewNormalAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={this.dataDoc.reviewNormalAngle}
+ radianEquivalent={true}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of normal force')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('force of static friction') && (
+ <InputField
+ label={<p>Static friction magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewStaticMagnitude'}
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={this.dataDoc.reviewStaticMagnitude}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('force of static friction')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('angle of static friction') && (
+ <InputField
+ label={<p>Static friction angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'reviewStaticAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={this.dataDoc.reviewStaticAngle}
+ radianEquivalent={true}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('angle of static friction')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('coefficient of static friction') && (
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'coefficientOfStaticFriction'}
+ step={0.1}
+ unit={''}
+ upperBound={1}
+ value={this.dataDoc.coefficientOfStaticFriction}
+ effect={this.updateReviewForcesBasedOnCoefficient}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('coefficient of static friction')]}
+ />
+ )}
+ {this.dataDoc.selectedQuestion.answerParts.includes('wedge angle') && (
+ <InputField
+ label={<Box>&theta;</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'wedgeAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={49}
+ value={this.dataDoc.wedgeAngle ?? 26}
+ effect={(val: number) => {
+ this.changeWedgeBasedOnNewAngle(val);
+ this.updateReviewForcesBasedOnAngle(val);
+ }}
+ radianEquivalent={true}
+ showIcon={this.dataDoc.showIcon}
+ correctValue={this.dataDoc.answers[this.dataDoc.selectedQuestion.answerParts.indexOf('wedge angle')]}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ )}
+ {this.dataDoc.mode == 'Tutorial' && (
+ <div className="wordProblemBox">
+ <div className="question">
+ <h2>Problem</h2>
+ <p>{this.dataDoc.selectedTutorial.question}</p>
+ </div>
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'spaceBetween',
+ width: '100%',
+ }}>
+ <IconButton
+ onClick={() => {
+ let step = this.dataDoc.stepNumber - 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1);
+ this.dataDoc.stepNumber = step;
+ this.dataDoc.startForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces);
+ this.dataDoc.updatedForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces);
+ this.dataDoc.showForceMagnitudes = this.dataDoc.selectedTutorial.steps[step].showMagnitude;
+ }}
+ disabled={this.dataDoc.stepNumber == 0}>
+ <ArrowLeftIcon />
+ </IconButton>
+ <div>
+ <h3>
+ Step {this.dataDoc.stepNumber + 1}: {this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].description}
+ </h3>
+ <p>{this.dataDoc.selectedTutorial.steps[this.dataDoc.stepNumber].content}</p>
+ </div>
+ <IconButton
+ onClick={() => {
+ let step = this.dataDoc.stepNumber + 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, this.dataDoc.selectedTutorial.steps.length - 1);
+ this.dataDoc.stepNumber = step;
+ this.dataDoc.startForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces);
+ this.dataDoc.updatedForces = this.getForceFromJSON(this.dataDoc.selectedTutorial.steps[step].forces);
+ this.dataDoc.showForceMagnitudes = this.dataDoc.selectedTutorial.steps[step].showMagnitude;
+ }}
+ disabled={this.dataDoc.stepNumber == this.dataDoc.selectedTutorial.steps.length - 1}>
+ <ArrowRightIcon />
+ </IconButton>
+ </div>
+ <div>
+ {(this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Pendulum') && <p>Resources</p>}
+ {this.dataDoc.simulationType == 'One Weight' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/one-dimensional-motion"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - One Dimensional Motion
+ </a>
+ </li>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Two Dimensional Motion
+ </a>
+ </li>
+ </ul>
+ )}
+ {this.dataDoc.simulationType == 'Inclined Plane' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Normal Force
+ </a>
+ </li>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Inclined Planes
+ </a>
+ </li>
+ </ul>
+ )}
+ {this.dataDoc.simulationType == 'Pendulum' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Tension
+ </a>
+ </li>
+ </ul>
+ )}
+ </div>
+ </div>
+ )}
+ {this.dataDoc.mode == 'Review' && this.dataDoc.simulationType == 'Inclined Plane' && (
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginTop: '10px',
+ }}>
+ <p
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ cursor: 'pointer',
+ }}
+ onClick={() => (this.dataDoc.mode = 'Tutorial')}>
+ {' '}
+ Go to walkthrough{' '}
+ </p>
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
+ <Button
+ onClick={() => {
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ this.checkAnswers();
+ this.dataDoc.showIcon = true;
+ }}
+ variant="outlined">
+ <p>Submit</p>
+ </Button>
+ <Button
+ onClick={() => {
+ this.generateNewQuestion();
+ this.dataDoc.showIcon = false;
+ }}
+ variant="outlined">
+ <p>New question</p>
+ </Button>
+ </div>
+ </div>
+ )}
+ {this.dataDoc.mode == 'Freeform' && (
+ <div className="vars">
+ <FormControl component="fieldset">
+ <FormGroup>
+ {this.dataDoc.simulationType == 'One Weight' && (
+ <FormControlLabel
+ control={<Checkbox checked={this.dataDoc.elasticCollisions} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />}
+ label="Make collisions elastic"
+ labelPlacement="start"
+ />
+ )}
+ <FormControlLabel control={<Checkbox checked={this.dataDoc.showForces} onChange={() => (this.dataDoc.showForces = !this.dataDoc.showForces)} />} label="Show force vectors" labelPlacement="start" />
+ {(this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Pendulum') && (
+ <FormControlLabel
+ control={<Checkbox checked={this.dataDoc.showForces} onChange={() => (this.dataDoc.showComponentForces = !this.dataDoc.showComponentForces)} />}
+ label="Show component force vectors"
+ labelPlacement="start"
+ />
+ )}
+ <FormControlLabel
+ control={<Checkbox checked={this.dataDoc.showAcceleration} onChange={() => (this.dataDoc.showAcceleration = !this.dataDoc.showAcceleration)} />}
+ label="Show acceleration vector"
+ labelPlacement="start"
+ />
+ <FormControlLabel control={<Checkbox checked={this.dataDoc.showVelocity} onChange={() => (this.dataDoc.showVelocity = !this.dataDoc.showVelocity)} />} label="Show velocity vector" labelPlacement="start" />
+ <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop={'simulationSpeed'} step={1} unit={'x'} upperBound={10} value={this.dataDoc.simulationSpeed ?? 2} labelWidth={'5em'} />
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Circular Motion' && (
+ <InputField
+ label={<Box>Gravity</Box>}
+ lowerBound={-30}
+ dataDoc={this.dataDoc}
+ prop={'gravity'}
+ step={0.01}
+ unit={'m/s2'}
+ upperBound={0}
+ value={this.dataDoc.gravity ?? -9.81}
+ effect={(val: number) => {
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Pulley' && (
+ <InputField
+ label={<Box>Mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop={'mass'}
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.dataDoc.mass ?? 1}
+ effect={(val: number) => {
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Pulley' && (
+ <InputField
+ label={<Box>Red mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop={'mass'}
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.dataDoc.mass ?? 1}
+ effect={(val: number) => {
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Pulley' && (
+ <InputField
+ label={<Box>Blue mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop={'mass2'}
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.dataDoc.mass2 ?? 1}
+ effect={(val: number) => {
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'Circular Motion' && (
+ <InputField
+ label={<Box>Rod length</Box>}
+ lowerBound={100}
+ dataDoc={this.dataDoc}
+ prop={'circularMotionRadius'}
+ step={5}
+ unit={'m'}
+ upperBound={250}
+ value={this.dataDoc.circularMotionRadius ?? 100}
+ effect={(val: number) => {
+ this.setupSimulation(this.dataDoc.simulationType, this.dataDoc.mode);
+ }}
+ labelWidth={'5em'}
+ />
+ )}
+ </FormGroup>
+ </FormControl>
+ {this.dataDoc.simulationType == 'Spring' && this.dataDoc.simulationPaused && (
+ <div>
+ <InputField
+ label={<Typography color="inherit">Spring stiffness</Typography>}
+ lowerBound={0.1}
+ dataDoc={this.dataDoc}
+ prop={'springConstant'}
+ step={1}
+ unit={'N/m'}
+ upperBound={500}
+ value={this.dataDoc.springConstant ?? 0.5}
+ effect={(val: number) => {
+ this.dataDoc.simulationReset(!this.dataDoc.simulationReset);
+ }}
+ radianEquivalent={false}
+ mode={'Freeform'}
+ labelWidth={'7em'}
+ />
+ <InputField
+ label={<Typography color="inherit">Rest length</Typography>}
+ lowerBound={10}
+ dataDoc={this.dataDoc}
+ prop={'springRestLength'}
+ step={100}
+ unit={''}
+ upperBound={500}
+ value={this.dataDoc.springRestLength ?? 200}
+ effect={(val: number) => {
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ radianEquivalent={false}
+ mode={'Freeform'}
+ labelWidth={'7em'}
+ />
+ <InputField
+ label={<Typography color="inherit">Starting displacement</Typography>}
+ lowerBound={-(this.dataDoc.springRestLength - 10)}
+ dataDoc={this.dataDoc}
+ prop={''}
+ step={10}
+ unit={''}
+ upperBound={this.dataDoc.springRestLength}
+ value={this.dataDoc.springStartLength - this.dataDoc.springRestLength ?? 0}
+ effect={(val: number) => {
+ this.dataDoc.startPosY = this.dataDoc.springRestLength + val;
+ this.dataDoc.springStartLength = this.dataDoc.springRestLength + val;
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ radianEquivalent={false}
+ mode={'Freeform'}
+ labelWidth={'7em'}
+ />
+ </div>
+ )}
+ {this.dataDoc.simulationType == 'Inclined Plane' && this.dataDoc.simulationPaused && (
+ <div>
+ <InputField
+ label={<Box>&theta;</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'wedgeAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={49}
+ value={this.dataDoc.wedgeAngle ?? 26}
+ effect={(val: number) => {
+ this.changeWedgeBasedOnNewAngle(val);
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ radianEquivalent={true}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'coefficientOfStaticFriction'}
+ step={0.1}
+ unit={''}
+ upperBound={1}
+ value={this.dataDoc.coefficientOfStaticFriction ?? 0}
+ effect={(val: number) => {
+ this.updateForcesWithFriction(val);
+ if (val < Number(this.dataDoc.coefficientOfKineticFriction)) {
+ this.dataDoc.soefficientOfKineticFriction = val;
+ }
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>k</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'coefficientOfKineticFriction'}
+ step={0.1}
+ unit={''}
+ upperBound={Number(this.dataDoc.coefficientOfStaticFriction)}
+ value={this.dataDoc.coefficientOfKineticFriction ?? 0}
+ effect={(val: number) => {
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ </div>
+ )}
+ {this.dataDoc.simulationType == 'Inclined Plane' && !this.dataDoc.simulationPaused && (
+ <Typography>
+ &theta;: {Math.round(Number(this.dataDoc.wedgeAngle) * 100) / 100}° ≈ {Math.round(((Number(this.dataDoc.wedgeAngle) * Math.PI) / 180) * 100) / 100} rad
+ <br />
+ &mu; <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction}
+ <br />
+ &mu; <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction}
+ </Typography>
+ )}
+ {this.dataDoc.simulationType == 'Pendulum' && !this.dataDoc.simulationPaused && (
+ <Typography>
+ &theta;: {Math.round(this.dataDoc.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.dataDoc.pendulumAngle * Math.PI) / 180) * 100) / 100} rad
+ </Typography>
+ )}
+ {this.dataDoc.simulationType == 'Pendulum' && this.dataDoc.simulationPaused && (
+ <div>
+ <InputField
+ label={<Box>Angle</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'pendulumAngle'}
+ step={1}
+ unit={'°'}
+ upperBound={59}
+ value={this.dataDoc.pendulumAngle ?? 30}
+ effect={value => {
+ this.dataDoc.startPendulumAngle = value;
+ if (this.dataDoc.simulationType == 'Pendulum') {
+ const mag = this.dataDoc.mass * Math.abs(this.dataDoc.gravity) * Math.cos((value * Math.PI) / 180);
- const forceOfTension: IForce = {
- description: "Tension",
- magnitude: mag,
- directionInDegrees: 90 - value,
- component: false,
- };
+ const forceOfTension: IForce = {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: 90 - value,
+ component: false,
+ };
- const tensionComponent: IForce = {
- description: "Tension",
- magnitude: mag,
- directionInDegrees: 90 - value,
- component: true,
- };
- const gravityParallel: IForce = {
- description: "Gravity Parallel Component",
- magnitude:
- Math.abs(this.dataDoc.gravity) *
- Math.cos((value * Math.PI) / 180),
- directionInDegrees: 270 - value,
- component: true,
- };
- const gravityPerpendicular: IForce = {
- description: "Gravity Perpendicular Component",
- magnitude:
- Math.abs(this.dataDoc.gravity) *
- Math.sin((value * Math.PI) / 180),
- directionInDegrees: -value,
- component: true,
- };
+ const tensionComponent: IForce = {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: 90 - value,
+ component: true,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: Math.abs(this.dataDoc.gravity) * Math.cos((value * Math.PI) / 180),
+ directionInDegrees: 270 - value,
+ component: true,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: Math.abs(this.dataDoc.gravity) * Math.sin((value * Math.PI) / 180),
+ directionInDegrees: -value,
+ component: true,
+ };
- const length = this.dataDoc.pendulumLength;
- const x =
- length * Math.cos(((90 - value) * Math.PI) / 180);
- const y =
- length * Math.sin(((90 - value) * Math.PI) / 180);
- const xPos = this.xMax / 2 - x - this.dataDoc.radius;
- const yPos = y - this.dataDoc.radius - 5;
- this.dataDoc.startPosX = (xPos);
- this.dataDoc.startPosY= (yPos);
+ const length = this.dataDoc.pendulumLength;
+ const x = length * Math.cos(((90 - value) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - value) * Math.PI) / 180);
+ const xPos = this.xMax / 2 - x - this.dataDoc.radius;
+ const yPos = y - this.dataDoc.radius - 5;
+ this.dataDoc.startPosX = xPos;
+ this.dataDoc.startPosY = yPos;
- this.dataDoc.startForces = ([
- {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
- },
- forceOfTension,
- ]);
- this.dataDoc.updatedForces = ([
- {
- description: "Gravity",
- magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
- directionInDegrees: 270,
- component: false,
- },
- forceOfTension,
- ]);
- this.dataDoc.componentForces = ([
- tensionComponent,
- gravityParallel,
- gravityPerpendicular,
- ]);
- this.dataDoc.adjustPendulumAngle = ({
- angle: value,
- length: this.dataDoc.pendulumLength,
- });
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }
- }}
- radianEquivalent={true}
- mode={"Freeform"}
- labelWidth={"5em"}
- />
- <InputField
- label={<Box>Rod length</Box>}
- lowerBound={0}
- dataDoc={this.dataDoc}
- prop={'pendulumLength'}
- step={1}
- unit={"m"}
- upperBound={400}
- value={Math.round(this.dataDoc.pendulumLength)}
- effect={(value) => {
- if (this.dataDoc.simulationType == "Pendulum") {
- this.dataDoc.adjustPendulumAngle = ({
- angle: this.dataDoc.pendulumAngle,
- length: value,
- });
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }
- }}
- radianEquivalent={false}
- mode={"Freeform"}
- labelWidth={"5em"}
- />
+ this.dataDoc.startForces = [
+ {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ },
+ forceOfTension,
+ ];
+ this.dataDoc.updatedForces = [
+ {
+ description: 'Gravity',
+ magnitude: Math.abs(this.dataDoc.gravity) * this.dataDoc.mass,
+ directionInDegrees: 270,
+ component: false,
+ },
+ forceOfTension,
+ ];
+ this.dataDoc.componentForces = [tensionComponent, gravityParallel, gravityPerpendicular];
+ this.dataDoc.adjustPendulumAngle = {
+ angle: value,
+ length: this.dataDoc.pendulumLength,
+ };
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }
+ }}
+ radianEquivalent={true}
+ mode={'Freeform'}
+ labelWidth={'5em'}
+ />
+ <InputField
+ label={<Box>Rod length</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop={'pendulumLength'}
+ step={1}
+ unit={'m'}
+ upperBound={400}
+ value={Math.round(this.dataDoc.pendulumLength)}
+ effect={value => {
+ if (this.dataDoc.simulationType == 'Pendulum') {
+ this.dataDoc.adjustPendulumAngle = {
+ angle: this.dataDoc.pendulumAngle,
+ length: value,
+ };
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }
+ }}
+ radianEquivalent={false}
+ mode={'Freeform'}
+ labelWidth={'5em'}
+ />
+ </div>
+ )}
+ </div>
+ )}
+ <div className="mechanicsSimulationEquation">
+ {this.dataDoc.mode == 'Freeform' && (
+ <table>
+ <tbody>
+ <tr>
+ <td>{this.dataDoc.simulationType == 'Pulley' ? 'Red Weight' : ''}</td>
+ <td>X</td>
+ <td>Y</td>
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Position</Box>
+ </td>
+ {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Circular Motion' || this.dataDoc.simulationType == 'Pulley') && (
+ <td style={{ cursor: 'default' }}>{this.dataDoc.positionXDisplay} m</td>
+ )}{' '}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Inclined Plane' && this.dataDoc.simulationType != 'Circular Motion' && this.dataDoc.simulationType != 'Pulley' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={this.dataDoc.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
+ dataDoc={this.dataDoc}
+ prop={'positionXDisplay'}
+ step={1}
+ unit={'m'}
+ upperBound={this.dataDoc.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
+ value={this.dataDoc.positionXDisplay}
+ effect={value => {
+ this.dataDoc.displayChange = {
+ xDisplay: value,
+ yDisplay: this.dataDoc.positionYDisplay,
+ };
+ if (this.dataDoc['simulationType'] == 'Suspension') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = value + this.radius - x1rod;
+ let deltaX2 = x2rod - (value + this.radius);
+ let deltaY = this.getYPosFromDisplay(this.dataDoc.positionYDisplay) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+ dir1T = (dir1T * 180) / Math.PI;
+ dir2T = (dir2T * 180) / Math.PI;
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag1,
+ directionInDegrees: dir1T,
+ component: false,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag2,
+ directionInDegrees: dir2T,
+ component: false,
+ };
+ const grav: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ this.dataDoc['updatedForces'] = [tensionForce1, tensionForce2, grav];
+ }
+ }}
+ small={true}
+ mode={'Freeform'}
+ />
+ </td>
+ )}{' '}
+ {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType == 'Inclined Plane' || this.dataDoc.simulationType == 'Circular Motion' || this.dataDoc.simulationType == 'Pulley') && (
+ <td style={{ cursor: 'default' }}>{this.dataDoc.positionYDisplay} m</td>
+ )}{' '}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType != 'Inclined Plane' && this.dataDoc.simulationType != 'Circular Motion' && this.dataDoc.simulationType != 'Pulley' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop={'positionYDisplay'}
+ step={1}
+ unit={'m'}
+ upperBound={this.yMax - 110}
+ value={this.dataDoc.positionYDisplay}
+ effect={value => {
+ this.dataDoc.displayChange = {
+ xDisplay: this.dataDoc.positionXDisplay,
+ yDisplay: value,
+ };
+ if (this.dataDoc['simulationType'] == 'Suspension') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = this.dataDoc.positionXDisplay + this.radius - x1rod;
+ let deltaX2 = x2rod - (this.dataDoc.positionXDisplay + this.radius);
+ let deltaY = this.getYPosFromDisplay(value) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+ dir1T = (dir1T * 180) / Math.PI;
+ dir2T = (dir2T * 180) / Math.PI;
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag1,
+ directionInDegrees: dir1T,
+ component: false,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag2,
+ directionInDegrees: dir2T,
+ component: false,
+ };
+ const grav: IForce = {
+ description: 'Gravity',
+ magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
+ directionInDegrees: 270,
+ component: false,
+ };
+ this.dataDoc['updatedForces'] = [tensionForce1, tensionForce2, grav];
+ }
+ }}
+ small={true}
+ mode={'Freeform'}
+ />
+ </td>
+ )}{' '}
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Velocity</Box>
+ </td>
+ {(!this.dataDoc.simulationPaused || (this.dataDoc.simulationType != 'One Weight' && this.dataDoc.simulationType != 'Circular Motion')) && (
+ <td style={{ cursor: 'default' }}>{this.dataDoc.velocityXDisplay} m/s</td>
+ )}{' '}
+ {this.dataDoc.simulationPaused && (this.dataDoc.simulationType == 'One Weight' || this.dataDoc.simulationType == 'Circular Motion') && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={-50}
+ dataDoc={this.dataDoc}
+ prop={'velocityXDisplay'}
+ step={1}
+ unit={'m/s'}
+ upperBound={50}
+ value={this.dataDoc.velocityXDisplay}
+ effect={value => {
+ this.dataDoc.startVelX = value;
+ this.dataDoc.simulationReset = !this.dataDoc.simulationReset;
+ }}
+ small={true}
+ mode={'Freeform'}
+ />
+ </td>
+ )}{' '}
+ {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType != 'One Weight') && <td style={{ cursor: 'default' }}>{this.dataDoc.velocityYDisplay} m/s</td>}{' '}
+ {this.dataDoc.simulationPaused && this.dataDoc.simulationType == 'One Weight' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={-50}
+ dataDoc={this.dataDoc}
+ prop={'velocityYDisplay'}
+ step={1}
+ unit={'m/s'}
+ upperBound={50}
+ value={this.dataDoc.velocityYDisplay}
+ effect={value => {
+ this.dataDoc.startVelY = -value;
+ this.dataDoc.displayChange = {
+ xDisplay: this.dataDoc.positionXDisplay,
+ yDisplay: this.dataDoc.positionYDisplay,
+ };
+ }}
+ small={true}
+ mode={'Freeform'}
+ />
+ </td>
+ )}{' '}
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Acceleration</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ {this.dataDoc.accelerationXDisplay} m/s<sup>2</sup>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ {this.dataDoc.accelerationYDisplay} m/s<sup>2</sup>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Momentum</Box>
+ </td>
+ <td>{Math.round(this.dataDoc.velocityXDisplay * this.dataDoc.mass * 10) / 10} kg*m/s</td>
+ <td>{Math.round(this.dataDoc.velocityYDisplay * this.dataDoc.mass * 10) / 10} kg*m/s</td>
+ </tr>
+ </tbody>
+ </table>
+ )}
+ {this.dataDoc.mode == 'Freeform' && this.dataDoc.simulationType == 'Pulley' && (
+ <table>
+ <tbody>
+ <tr>
+ <td>Blue Weight</td>
+ <td>X</td>
+ <td>Y</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Position</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>{this.dataDoc.positionXDisplay2} m</td>
+ <td style={{ cursor: 'default' }}>{this.dataDoc.positionYDisplay2} m</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Velocity</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>{this.dataDoc.velocityXDisplay2} m/s</td>
+
+ <td style={{ cursor: 'default' }}>{this.dataDoc.velocityYDisplay2} m/s</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Acceleration</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ {this.dataDoc.accelerationXDisplay2} m/s<sup>2</sup>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ {this.dataDoc.accelerationYDisplay2} m/s<sup>2</sup>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Momentum</Box>
+ </td>
+ <td>{Math.round(this.dataDoc.velocityXDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s</td>
+ <td>{Math.round(this.dataDoc.velocityYDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s</td>
+ </tr>
+ </tbody>
+ </table>
+ )}
+ </div>
+ {this.dataDoc.simulationType != 'Pendulum' && this.dataDoc.simulationType != 'Spring' && (
+ <div>
+ <p>Kinematic Equations</p>
+ <ul>
+ <li>
+ Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+
+ <sup>1</sup>&frasl;
+ <sub>2</sub>at
+ <sup>2</sup>
+ </li>
+ <li>
+ Velocity: v<sub>1</sub>=v<sub>0</sub>+at
+ </li>
+ <li>Acceleration: a = F/m</li>
+ </ul>
+ </div>
+ )}
+ {this.dataDoc.simulationType == 'Spring' && (
+ <div>
+ <p>Harmonic Motion Equations: Spring</p>
+ <ul>
+ <li>
+ Spring force: F<sub>s</sub>=kd
+ </li>
+ <li>
+ Spring period: T<sub>s</sub>=2&pi;&#8730;<sup>m</sup>&frasl;
+ <sub>k</sub>
+ </li>
+ <li>Equilibrium displacement for vertical spring: d = mg/k</li>
+ <li>
+ Elastic potential energy: U<sub>s</sub>=<sup>1</sup>&frasl;
+ <sub>2</sub>kd<sup>2</sup>
+ </li>
+ <ul>
+ <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li>
+ </ul>
+ <li>
+ Translational kinetic energy: K=<sup>1</sup>&frasl;
+ <sub>2</sub>mv<sup>2</sup>
+ </li>
+ <ul>
+ <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li>
+ </ul>
+ </ul>
+ </div>
+ )}
+ {this.dataDoc.simulationType == 'Pendulum' && (
+ <div>
+ <p>Harmonic Motion Equations: Pendulum</p>
+ <ul>
+ <li>
+ Pendulum period: T<sub>p</sub>=2&pi;&#8730;<sup>l</sup>&frasl;
+ <sub>g</sub>
+ </li>
+ </ul>
+ </div>
+ )}
+ </div>
</div>
- )}
- </div>
- )}
- <div className="mechanicsSimulationEquation">
- {this.dataDoc.mode == "Freeform" && (
- <table>
- <tbody>
- <tr>
- <td>{this.dataDoc.simulationType == "Pulley" ? "Red Weight" : ""}</td>
- <td>X</td>
- <td>Y</td>
- </tr>
- <tr>
- <td
- style={{ cursor: "help" }}
- // onClick={() => {
- // window.open(
- // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
- // );
- // }}
- >
- <Box>Position</Box>
- </td>
- {(!this.dataDoc.simulationPaused ||
- this.dataDoc.simulationType == "Inclined Plane" ||
- this.dataDoc.simulationType == "Circular Motion" ||
- this.dataDoc.simulationType == "Pulley") && (
- <td style={{ cursor: "default" }}>
- {this.dataDoc.positionXDisplay} m
- </td>
- )}{" "}
- {this.dataDoc.simulationPaused &&
- this.dataDoc.simulationType != "Inclined Plane" &&
- this.dataDoc.simulationType != "Circular Motion" &&
- this.dataDoc.simulationType != "Pulley" && (
- <td
- style={{
- cursor: "default",
- }}
- >
- <InputField
- lowerBound={this.dataDoc.simulationType == "Projectile" ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
- dataDoc={this.dataDoc}
- prop={'positionXDisplay'}
- step={1}
- unit={"m"}
- upperBound={this.dataDoc.simulationType == "Projectile" ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
- value={this.dataDoc.positionXDisplay}
- effect={(value) => {
- this.dataDoc.displayChange = ({
- xDisplay: value,
- yDisplay: this.dataDoc.positionYDisplay,
- });
- if (this.dataDoc['simulationType'] == "Suspension") {
- let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
- let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
- let deltaX1 = value + this.radius - x1rod;
- let deltaX2 = x2rod - (value + this.radius);
- let deltaY = this.getYPosFromDisplay(this.dataDoc.positionYDisplay) + this.radius;
- let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
- let dir2T = Math.atan(deltaY / deltaX2);
- let tensionMag2 =
- (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) /
- ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) +
- Math.sin(dir2T));
- let tensionMag1 =
- (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
- dir1T = (dir1T * 180) / Math.PI;
- dir2T = (dir2T * 180) / Math.PI;
- const tensionForce1: IForce = {
- description: "Tension",
- magnitude: tensionMag1,
- directionInDegrees: dir1T,
- component: false,
- };
- const tensionForce2: IForce = {
- description: "Tension",
- magnitude: tensionMag2,
- directionInDegrees: dir2T,
- component: false,
- };
- const grav: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- };
- this.dataDoc['updatedForces'] = ([tensionForce1, tensionForce2, grav]);
- }
- }}
- small={true}
- mode={"Freeform"}
- />
- </td>
- )}{" "}
- {(!this.dataDoc.simulationPaused ||
- this.dataDoc.simulationType == "Inclined Plane" ||
- this.dataDoc.simulationType == "Circular Motion" ||
- this.dataDoc.simulationType == "Pulley") && (
- <td style={{ cursor: "default" }}>
- {this.dataDoc.positionYDisplay} m
- </td>
- )}{" "}
- {this.dataDoc.simulationPaused &&
- this.dataDoc.simulationType != "Inclined Plane" &&
- this.dataDoc.simulationType != "Circular Motion" &&
- this.dataDoc.simulationType != "Pulley" && (
- <td
- style={{
- cursor: "default",
- }}
- >
- <InputField
- lowerBound={1}
- dataDoc={this.dataDoc}
- prop={'positionYDisplay'}
- step={1}
- unit={"m"}
- upperBound={this.yMax - 110}
- value={this.dataDoc.positionYDisplay}
- effect={(value) => {
- this.dataDoc.displayChange = ({
- xDisplay: this.dataDoc.positionXDisplay,
- yDisplay: value,
- });
- if (this.dataDoc['simulationType'] == "Suspension") {
- let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
- let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
- let deltaX1 = this.dataDoc.positionXDisplay + this.radius - x1rod;
- let deltaX2 = x2rod - (this.dataDoc.positionXDisplay + this.radius);
- let deltaY = this.getYPosFromDisplay(value) + this.radius;
- let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
- let dir2T = Math.atan(deltaY / deltaX2);
- let tensionMag2 =
- (this.dataDoc.mass * Math.abs(this.dataDoc.gravity)) /
- ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) +
- Math.sin(dir2T));
- let tensionMag1 =
- (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
- dir1T = (dir1T * 180) / Math.PI;
- dir2T = (dir2T * 180) / Math.PI;
- const tensionForce1: IForce = {
- description: "Tension",
- magnitude: tensionMag1,
- directionInDegrees: dir1T,
- component: false,
- };
- const tensionForce2: IForce = {
- description: "Tension",
- magnitude: tensionMag2,
- directionInDegrees: dir2T,
- component: false,
- };
- const grav: IForce = {
- description: "Gravity",
- magnitude: this.dataDoc.mass * Math.abs(this.dataDoc.gravity),
- directionInDegrees: 270,
- component: false,
- };
- this.dataDoc['updatedForces'] = ([tensionForce1, tensionForce2, grav]);
- }
- }}
- small={true}
- mode={"Freeform"}
- />
- </td>
- )}{" "}
- </tr>
- <tr>
- <td
- style={{ cursor: "help" }}
- // onClick={() => {
- // window.open(
- // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
- // );
- // }}
- >
- <Box>Velocity</Box>
- </td>
- {(!this.dataDoc.simulationPaused ||
- (this.dataDoc.simulationType != "One Weight" &&
- this.dataDoc.simulationType != "Circular Motion")) && (
- <td style={{ cursor: "default" }}>
- {this.dataDoc.velocityXDisplay} m/s
- </td>
- )}{" "}
- {this.dataDoc.simulationPaused &&
- (this.dataDoc.simulationType == "One Weight" ||
- this.dataDoc.simulationType == "Circular Motion") && (
- <td
- style={{
- cursor: "default",
- }}
- >
- <InputField
- lowerBound={-50}
- dataDoc={this.dataDoc}
- prop={'velocityXDisplay'}
- step={1}
- unit={"m/s"}
- upperBound={50}
- value={this.dataDoc.velocityXDisplay}
- effect={(value) => {
- this.dataDoc.startVelX = (value);
- this.dataDoc.simulationReset = (!this.dataDoc.simulationReset);
- }}
- small={true}
- mode={"Freeform"}
- />
- </td>
- )}{" "}
- {(!this.dataDoc.simulationPaused || this.dataDoc.simulationType != "One Weight") && (
- <td style={{ cursor: "default" }}>
- {this.dataDoc.velocityYDisplay} m/s
- </td>
- )}{" "}
- {this.dataDoc.simulationPaused && this.dataDoc.simulationType == "One Weight" && (
- <td
+ <div
+ style={{
+ position: 'fixed',
+ top: this.yMax - 120 + 20 + 'px',
+ left: this.xMin + 90 - 80 + 'px',
+ zIndex: -10000,
+ }}>
+ <svg width={100 + 'px'} height={100 + 'px'}>
+ <defs>
+ <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
+ <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} />
+ </marker>
+ </defs>
+ <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
+ <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
+ </svg>
+ <p
style={{
- cursor: "default",
- }}
- >
- <InputField
- lowerBound={-50}
- dataDoc={this.dataDoc}
- prop={'velocityYDisplay'}
- step={1}
- unit={"m/s"}
- upperBound={50}
- value={this.dataDoc.velocityYDisplay}
- effect={(value) => {
- this.dataDoc.startVelY = (-value);
- this.dataDoc.displayChange = ({
- xDisplay: this.dataDoc.positionXDisplay,
- yDisplay: this.dataDoc.positionYDisplay,
- });
- }}
- small={true}
- mode={"Freeform"}
- />
- </td>
- )}{" "}
- </tr>
- <tr>
- <td
- style={{ cursor: "help" }}
- // onClick={() => {
- // window.open(
- // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
- // );
- // }}
- >
- <Box>Acceleration</Box>
- </td>
- <td style={{ cursor: "default" }}>
- {this.dataDoc.accelerationXDisplay} m/s<sup>2</sup>
- </td>
- <td style={{ cursor: "default" }}>
- {this.dataDoc.accelerationYDisplay} m/s<sup>2</sup>
- </td>
- </tr>
- <tr>
- <td>
- <Box>Momentum</Box>
- </td>
- <td>
- {Math.round(this.dataDoc.velocityXDisplay * this.dataDoc.mass * 10) / 10} kg*m/s
- </td>
- <td>
- {Math.round(this.dataDoc.velocityYDisplay * this.dataDoc.mass * 10) / 10} kg*m/s
- </td>
- </tr>
- </tbody>
- </table>
- )}
- {this.dataDoc.mode == "Freeform" && this.dataDoc.simulationType == "Pulley" && (
- <table>
- <tbody>
- <tr>
- <td>Blue Weight</td>
- <td>X</td>
- <td>Y</td>
- </tr>
- <tr>
- <td>
- <Box>Position</Box>
- </td>
- <td style={{ cursor: "default" }}>{this.dataDoc.positionXDisplay2} m</td>
- <td style={{ cursor: "default" }}>{this.dataDoc.positionYDisplay2} m</td>
- </tr>
- <tr>
- <td>
- <Box>Velocity</Box>
- </td>
- <td style={{ cursor: "default" }}>
- {this.dataDoc.velocityXDisplay2} m/s
- </td>
-
- <td style={{ cursor: "default" }}>
- {this.dataDoc.velocityYDisplay2} m/s
- </td>
- </tr>
- <tr>
- <td>
- <Box>Acceleration</Box>
- </td>
- <td style={{ cursor: "default" }}>
- {this.dataDoc.accelerationXDisplay2} m/s<sup>2</sup>
- </td>
- <td style={{ cursor: "default" }}>
- {this.dataDoc.accelerationYDisplay2} m/s<sup>2</sup>
- </td>
- </tr>
- <tr>
- <td>
- <Box>Momentum</Box>
- </td>
- <td>
- {Math.round(this.dataDoc.velocityXDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s
- </td>
- <td>
- {Math.round(this.dataDoc.velocityYDisplay2 * this.dataDoc.mass * 10) / 10} kg*m/s
- </td>
- </tr>
- </tbody>
- </table>
- )}
- </div>
- {this.dataDoc.simulationType != "Pendulum" && this.dataDoc.simulationType != "Spring" && (
- <div>
- <p>Kinematic Equations</p>
- <ul>
- <li>
- Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+
- <sup>1</sup>&frasl;
- <sub>2</sub>at
- <sup>2</sup>
- </li>
- <li>
- Velocity: v<sub>1</sub>=v<sub>0</sub>+at
- </li>
- <li>Acceleration: a = F/m</li>
- </ul>
- </div>
- )}
- {this.dataDoc.simulationType == "Spring" && (
- <div>
- <p>Harmonic Motion Equations: Spring</p>
- <ul>
- <li>
- Spring force: F<sub>s</sub>=kd
- </li>
- <li>
- Spring period: T<sub>s</sub>=2&pi;&#8730;<sup>m</sup>&frasl;
- <sub>k</sub>
- </li>
- <li>Equilibrium displacement for vertical spring: d = mg/k</li>
- <li>
- Elastic potential energy: U<sub>s</sub>=<sup>1</sup>&frasl;
- <sub>2</sub>kd<sup>2</sup>
- </li>
- <ul>
- <li>
- Maximum when system is at maximum displacement, 0 when
- system is at 0 displacement
- </li>
- </ul>
- <li>
- Translational kinetic energy: K=<sup>1</sup>&frasl;
- <sub>2</sub>mv<sup>2</sup>
- </li>
- <ul>
- <li>
- Maximum when system is at maximum/minimum velocity (at 0
- displacement), 0 when velocity is 0 (at maximum
- displacement)
- </li>
- </ul>
- </ul>
- </div>
- )}
- {this.dataDoc.simulationType == "Pendulum" && (
- <div>
- <p>Harmonic Motion Equations: Pendulum</p>
- <ul>
- <li>
- Pendulum period: T<sub>p</sub>=2&pi;&#8730;<sup>l</sup>&frasl;
- <sub>g</sub>
- </li>
- </ul>
+ position: 'fixed',
+ top: this.yMax - 120 + 40 + 'px',
+ left: this.xMin + 90 - 80 + 'px',
+ }}>
+ {this.dataDoc.simulationType == 'Circular Motion' ? 'Z' : 'Y'}
+ </p>
+ <p
+ style={{
+ position: 'fixed',
+ top: this.yMax - 120 + 80 + 'px',
+ left: this.xMin + 90 - 40 + 'px',
+ }}>
+ X
+ </p>
+ </div>
</div>
- )}
- </div>
- </div>
- <div
- style={{
- position: "fixed",
- top: this.yMax - 120 + 20 + "px",
- left: this.xMin + 90 - 80 + "px",
- zIndex: -10000,
- }}
- >
- <svg width={100 + "px"} height={100 + "px"}>
- <defs>
- <marker
- id="miniArrow"
- markerWidth="20"
- markerHeight="20"
- refX="0"
- refY="3"
- orient="auto"
- markerUnits="strokeWidth"
- >
- <path d="M0,0 L0,6 L9,3 z" fill={"#000000"} />
- </marker>
- </defs>
- <line
- x1={20}
- y1={70}
- x2={70}
- y2={70}
- stroke={"#000000"}
- strokeWidth="2"
- markerEnd="url(#miniArrow)"
- />
- <line
- x1={20}
- y1={70}
- x2={20}
- y2={20}
- stroke={"#000000"}
- strokeWidth="2"
- markerEnd="url(#miniArrow)"
- />
- </svg>
- <p
- style={{
- position: "fixed",
- top: this.yMax - 120 + 40 + "px",
- left: this.xMin + 90 - 80 + "px",
- }}
- >
- {this.dataDoc.simulationType == "Circular Motion" ? "Z" : "Y"}
- </p>
- <p
- style={{
- position: "fixed",
- top: this.yMax - 120 + 80 + "px",
- left: this.xMin + 90 - 40 + "px",
- }}
- >
- X
- </p>
- </div>
- </div>
- )
- }
-} \ No newline at end of file
+ );
+ }
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx
index 5e5a60edd..d595a499e 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx
@@ -1,183 +1,179 @@
-import { TextField, InputAdornment } from "@mui/material";
+import { TextField, InputAdornment } from '@mui/material';
import { Doc } from '../../../../fields/Doc';
import React = require('react');
-import TaskAltIcon from "@mui/icons-material/TaskAlt";
-import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline";
+import TaskAltIcon from '@mui/icons-material/TaskAlt';
+import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
+import { isNumber } from 'lodash';
export interface IInputProps {
- label?: JSX.Element;
- lowerBound: number;
- dataDoc: Doc;
- prop: string;
- step: number;
- unit: string;
- upperBound: number;
- value: number | string | Array<number | string>;
- correctValue?: number;
- showIcon?: boolean;
- effect?: (val: number) => any;
- radianEquivalent?: boolean;
- small?: boolean;
- mode?: string;
- update?: boolean;
- labelWidth?: string;
+ label?: JSX.Element;
+ lowerBound: number;
+ dataDoc: Doc;
+ prop: string;
+ step: number;
+ unit: string;
+ upperBound: number;
+ value: number | string | Array<number | string>;
+ correctValue?: number;
+ showIcon?: boolean;
+ effect?: (val: number) => any;
+ radianEquivalent?: boolean;
+ small?: boolean;
+ mode?: string;
+ update?: boolean;
+ labelWidth?: string;
}
interface IState {
- tempValue: string | number | (string|number)[],
- tempRadianValue: number,
- width: string;
- margin: string;
+ tempValue: string | number | (string | number)[];
+ tempRadianValue: number;
+ width: string;
+ margin: string;
}
export default class InputField extends React.Component<IInputProps, IState> {
- constructor(props: any) {
- super(props)
- this.state = {
- tempValue: this.props.mode != "Freeform" && !this.props.showIcon ? 0 : this.props.value,
- tempRadianValue: this.props.mode != "Freeform" && !this.props.showIcon ? 0 : (Number(this.props.value) * Math.PI) / 180,
- width: this.props.small ? "6em" : "7em",
- margin: this.props.small ? "0px" : "10px"
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ tempValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : this.props.value,
+ tempRadianValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : (Number(this.props.value) * Math.PI) / 180,
+ width: this.props.small ? '6em' : '7em',
+ margin: this.props.small ? '0px' : '10px',
+ };
}
- }
- epsilon: number = 0.01;
+ epsilon: number = 0.01;
- componentDidMount(): void {
- this.setState({tempValue: Number(this.props.value)})
- }
+ componentDidMount(): void {
+ this.setState({ tempValue: Number(this.props.value) });
+ }
- componentDidUpdate(prevProps: Readonly<IInputProps>, prevState: Readonly<IState>, snapshot?: any): void {
- if (prevProps.value != this.props.value && !isNaN(this.props.value)) {
- if (this.props.mode == "Freeform") {
- if (Math.abs(this.state.tempValue - Number(this.props.value)) > 1) {
- this.setState({tempValue: Number(this.props.value)})
+ componentDidUpdate(prevProps: Readonly<IInputProps>, prevState: Readonly<IState>, snapshot?: any): void {
+ if (prevProps.value != this.props.value && isNumber(this.props.value) && !isNaN(this.props.value)) {
+ if (this.props.mode == 'Freeform') {
+ if (isNumber(this.state.tempValue) && Math.abs(this.state.tempValue - Number(this.props.value)) > 1) {
+ this.setState({ tempValue: Number(this.props.value) });
+ }
+ }
+ }
+ if (prevProps.update != this.props.update) {
+ this.externalUpdate();
}
- }
- }
- if (prevProps.update != this.props.update) {
- this.externalUpdate();
}
- }
- externalUpdate = () => {
- this.setState({tempValue: Number(this.props.value)})
- this.setState({tempRadianValue: Number(this.props.value) * Math.PI / 180})
- };
+ externalUpdate = () => {
+ this.setState({ tempValue: Number(this.props.value) });
+ this.setState({ tempRadianValue: (Number(this.props.value) * Math.PI) / 180 });
+ };
- onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- let value = event.target.value == "" ? 0 : Number(event.target.value);
- if (value > this.props.upperBound) {
- value = this.props.upperBound;
- } else if (value < this.props.lowerBound) {
- value = this.props.lowerBound;
- }
- if (this.props.prop != "") {
- this.props.dataDoc[this.props.prop] = value
- }
- this.setState({tempValue: event.target.value == "" ? event.target.value : value})
- this.setState({tempRadianValue: (value * Math.PI) / 180})
- if (this.props.effect) {
- this.props.effect(value);
- }
- };
+ onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ let value = event.target.value == '' ? 0 : Number(event.target.value);
+ if (value > this.props.upperBound) {
+ value = this.props.upperBound;
+ } else if (value < this.props.lowerBound) {
+ value = this.props.lowerBound;
+ }
+ if (this.props.prop != '') {
+ this.props.dataDoc[this.props.prop] = value;
+ }
+ this.setState({ tempValue: event.target.value == '' ? event.target.value : value });
+ this.setState({ tempRadianValue: (value * Math.PI) / 180 });
+ if (this.props.effect) {
+ this.props.effect(value);
+ }
+ };
- onChangeRadianValue = (event: React.ChangeEvent<HTMLInputElement>) => {
- let value = event.target.value === "" ? 0 : Number(event.target.value);
- if (value > 2 * Math.PI) {
- value = 2 * Math.PI;
- } else if (value < 0) {
- value = 0;
- }if (this.props.prop != "") {
- this.props.dataDoc[this.props.prop] = (value * 180) / Math.PI
- }
- this.setState({tempValue: (value * 180) / Math.PI})
- this.setState({tempRadianValue: value})
- if (this.props.effect) {
- this.props.effect((value * 180) / Math.PI);
- }
- };
+ onChangeRadianValue = (event: React.ChangeEvent<HTMLInputElement>) => {
+ let value = event.target.value === '' ? 0 : Number(event.target.value);
+ if (value > 2 * Math.PI) {
+ value = 2 * Math.PI;
+ } else if (value < 0) {
+ value = 0;
+ }
+ if (this.props.prop != '') {
+ this.props.dataDoc[this.props.prop] = (value * 180) / Math.PI;
+ }
+ this.setState({ tempValue: (value * 180) / Math.PI });
+ this.setState({ tempRadianValue: value });
+ if (this.props.effect) {
+ this.props.effect((value * 180) / Math.PI);
+ }
+ };
- render () {
- return (
- <div
- style={{
- display: "flex",
- lineHeight: "1.5",
- textAlign: "right",
- alignItems: "center",
- }}
- >
- {this.props.label && (
- <div
- style={{
- marginTop: "0.3em",
- marginBottom: "-0.5em",
- width: this.props.labelWidth ?? "2em",
- }}
- >
- {this.props.label}
- </div>
- )}
- <TextField
- type="number"
- variant="standard"
- value={this.state.tempValue}
- onChange={this.onChange}
- sx={{
- height: "1em",
- width: this.state.width,
- marginLeft: this.state.margin,
- zIndex: "modal",
- }}
- inputProps={{
- step: this.props.step,
- min: this.props.lowerBound,
- max: this.props.upperBound,
- type: "number",
- }}
- InputProps={{
- startAdornment: (
- <InputAdornment position="start">
- {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) < this.epsilon &&
- this.props.showIcon && <TaskAltIcon color={"success"} />}
- {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) >= this.epsilon &&
- this.props.showIcon && <ErrorOutlineIcon color={"error"} />}
- </InputAdornment>
- ),
- endAdornment: <InputAdornment position="end">{this.props.unit}</InputAdornment>,
- }}
- />
- {this.props.radianEquivalent && (
- <div
- style={{ marginTop: "0.3em", marginBottom: "-0.5em", width: "1em" }}
- >
- <p>≈</p>
- </div>
- )}
- {this.props.radianEquivalent && (
- <TextField
- type="number"
- variant="standard"
- value={this.state.tempRadianValue}
- onChange={this.onChangeRadianValue}
- sx={{
- height: "1em",
- width: this.state.width,
- marginLeft: this.state.margin,
- zIndex: "modal",
- }}
- inputProps={{
- step: Math.PI / 8,
- min: 0,
- max: 2 * Math.PI,
- type: "number",
- }}
- InputProps={{
- endAdornment: <InputAdornment position="end">rad</InputAdornment>,
- }}
- />
- )}
- </div>
- )
- }
-};
+ render() {
+ return (
+ <div
+ style={{
+ display: 'flex',
+ lineHeight: '1.5',
+ textAlign: 'right',
+ alignItems: 'center',
+ }}>
+ {this.props.label && (
+ <div
+ style={{
+ marginTop: '0.3em',
+ marginBottom: '-0.5em',
+ width: this.props.labelWidth ?? '2em',
+ }}>
+ {this.props.label}
+ </div>
+ )}
+ <TextField
+ type="number"
+ variant="standard"
+ value={this.state.tempValue}
+ onChange={this.onChange}
+ sx={{
+ height: '1em',
+ width: this.state.width,
+ marginLeft: this.state.margin,
+ zIndex: 'modal',
+ }}
+ inputProps={{
+ step: this.props.step,
+ min: this.props.lowerBound,
+ max: this.props.upperBound,
+ type: 'number',
+ }}
+ InputProps={{
+ startAdornment: (
+ <InputAdornment position="start">
+ {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) < this.epsilon && this.props.showIcon && <TaskAltIcon color={'success'} />}
+ {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) >= this.epsilon && this.props.showIcon && <ErrorOutlineIcon color={'error'} />}
+ </InputAdornment>
+ ),
+ endAdornment: <InputAdornment position="end">{this.props.unit}</InputAdornment>,
+ }}
+ />
+ {this.props.radianEquivalent && (
+ <div style={{ marginTop: '0.3em', marginBottom: '-0.5em', width: '1em' }}>
+ <p>≈</p>
+ </div>
+ )}
+ {this.props.radianEquivalent && (
+ <TextField
+ type="number"
+ variant="standard"
+ value={this.state.tempRadianValue}
+ onChange={this.onChangeRadianValue}
+ sx={{
+ height: '1em',
+ width: this.state.width,
+ marginLeft: this.state.margin,
+ zIndex: 'modal',
+ }}
+ inputProps={{
+ step: Math.PI / 8,
+ min: 0,
+ max: 2 * Math.PI,
+ type: 'number',
+ }}
+ InputProps={{
+ endAdornment: <InputAdornment position="end">rad</InputAdornment>,
+ }}
+ />
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 0ff7c4292..80b12b96e 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,58 +1,64 @@
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { VideoField } from "../../../../fields/URLField";
-import { Upload } from "../../../../server/SharedMediaTypes";
-import { ViewBoxBaseComponent } from "../../DocComponent";
-import { FieldView } from "../FieldView";
-import { VideoBox } from "../VideoBox";
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { VideoField } from '../../../../fields/URLField';
+import { Upload } from '../../../../server/SharedMediaTypes';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { FieldView } from '../FieldView';
+import { VideoBox } from '../VideoBox';
import { RecordingView } from './RecordingView';
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { Presentation } from "../../../util/TrackMovements";
-import { Doc } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { Presentation } from '../../../util/TrackMovements';
+import { Doc } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
@observer
export class RecordingBox extends ViewBoxBaseComponent() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(RecordingBox, fieldKey);
+ }
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
-
- private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
+ constructor(props: any) {
super(props);
- }
-
- componentDidMount() {
- Doc.SetNativeWidth(this.dataDoc, 1280);
- Doc.SetNativeHeight(this.dataDoc, 720);
- }
-
- @observable result: Upload.AccessPathInfo | undefined = undefined
- @observable videoDuration: number | undefined = undefined
-
- @action
- setVideoDuration = (duration: number) => {
- this.videoDuration = duration
- }
-
- @action
- setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
- this.result = info
- this.dataDoc.type = DocumentType.VID;
- this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
-
- this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
- this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
- this.dataDoc[this.fieldKey + "-recorded"] = true;
- // stringify the presentation and store it
- presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
- }
-
- render() {
- return <div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
- </div>;
- }
+ }
+
+ componentDidMount() {
+ Doc.SetNativeWidth(this.dataDoc, 1280);
+ Doc.SetNativeHeight(this.dataDoc, 720);
+ }
+
+ @observable result: Upload.AccessPathInfo | undefined = undefined;
+ @observable videoDuration: number | undefined = undefined;
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration;
+ };
+
+ @action
+ setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
+ this.result = info;
+ this.dataDoc.type = DocumentType.VID;
+ this.dataDoc[this.fieldKey + '_duration'] = this.videoDuration;
+
+ this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
+ this.dataDoc[this.fieldKey + '-recorded'] = true;
+ // stringify the presentation and store it
+ if (presentation?.movements) {
+ const presCopy = { ...presentation };
+ presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any;
+ this.dataDoc[this.fieldKey + '-presentation'] = JSON.stringify(presCopy);
+ }
+ };
+
+ render() {
+ return (
+ <div className="recordingBox" ref={this._ref}>
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
+ </div>
+ );
+ }
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index ec5917b9e..424ebc384 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,32 +1,31 @@
import * as React from 'react';
-import "./RecordingView.scss";
-import { useEffect, useRef, useState } from "react";
-import { ProgressBar } from "./ProgressBar"
-import { MdBackspace } from 'react-icons/md';
+import { useEffect, useRef, useState } from 'react';
+import { IconContext } from 'react-icons';
import { FaCheckCircle } from 'react-icons/fa';
-import { IconContext } from "react-icons";
-import { Networking } from '../../../Network';
+import { MdBackspace } from 'react-icons/md';
import { Upload } from '../../../../server/SharedMediaTypes';
import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Networking } from '../../../Network';
import { Presentation, TrackMovements } from '../../../util/TrackMovements';
+import { ProgressBar } from './ProgressBar';
+import './RecordingView.scss';
export interface MediaSegment {
- videoChunks: any[],
- endTime: number,
- startTime: number,
- presentation?: Presentation,
+ videoChunks: any[];
+ endTime: number;
+ startTime: number;
+ presentation?: Presentation;
}
interface IRecordingViewProps {
- setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
- setDuration: (seconds: number) => void
- id: string
+ setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void;
+ setDuration: (seconds: number) => void;
+ id: string;
}
const MAXTIME = 100000;
export function RecordingView(props: IRecordingViewProps) {
-
const [recording, setRecording] = useState(false);
const recordingTimerRef = useRef<number>(0);
const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
@@ -46,19 +45,16 @@ export function RecordingView(props: IRecordingViewProps) {
const [finished, setFinished] = useState<boolean>(false);
const [trackScreen, setTrackScreen] = useState<boolean>(false);
-
-
const DEFAULT_MEDIA_CONSTRAINTS = {
video: {
width: 1280,
height: 720,
-
},
audio: {
echoCancellation: true,
noiseSuppression: true,
- sampleRate: 44100
- }
+ sampleRate: 44100,
+ },
};
useEffect(() => {
@@ -71,12 +67,11 @@ export function RecordingView(props: IRecordingViewProps) {
const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
// upload the segments to the server and get their server access paths
- const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles))
- .map(res => (res.result instanceof Error) ? '' : res.result.accessPaths.agnostic.server)
+ const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server));
// concat the segments together using post call
const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
- !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+ !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed');
})();
}
}, [videos]);
@@ -87,7 +82,9 @@ export function RecordingView(props: IRecordingViewProps) {
}, [finished]);
// check if the browser supports media devices on first load
- useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+ useEffect(() => {
+ if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.');
+ }, []);
useEffect(() => {
let interval: any = null;
@@ -102,24 +99,24 @@ export function RecordingView(props: IRecordingViewProps) {
}, [recording]);
useEffect(() => {
- setVideoProgressHelper(recordingTimer)
+ setVideoProgressHelper(recordingTimer);
recordingTimerRef.current = recordingTimer;
}, [recordingTimer]);
const setVideoProgressHelper = (progress: number) => {
const newProgress = (progress / MAXTIME) * 100;
setProgress(newProgress);
- }
+ };
const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- videoElementRef.current!.src = "";
+ videoElementRef.current!.src = '';
videoElementRef.current!.srcObject = stream;
videoElementRef.current!.muted = true;
return stream;
- }
+ };
const record = async () => {
// don't need to start a new stream every time we start recording a new segment
@@ -145,29 +142,28 @@ export function RecordingView(props: IRecordingViewProps) {
const nextVideo = {
videoChunks,
endTime: recordingTimerRef.current,
- startTime: videos?.lastElement()?.endTime || 0
+ startTime: videos?.lastElement()?.endTime || 0,
};
// depending on if a presenation exists, add it to the video
const presentation = TrackMovements.Instance.yieldPresentation();
- setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+ setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]);
}
// reset the temporary chunks
videoChunks = [];
setRecording(false);
- }
+ };
videoRecorder.current.start(200);
- }
-
+ };
// if this is called, then we're done recording all the segments
const finish = (e: React.PointerEvent) => {
e.stopPropagation();
// call stop on the video recorder if active
- videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+ videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop();
// end the streams (audio/video) to remove recording icon
const stream = videoElementRef.current!.srcObject;
@@ -178,94 +174,91 @@ export function RecordingView(props: IRecordingViewProps) {
// this will call upon progessbar to update videos to be in the correct order
setFinished(true);
- }
+ };
const pause = (e: React.PointerEvent) => {
e.stopPropagation();
// if recording, then this is just a new segment
- videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
- }
+ videoRecorder.current?.state === 'recording' && videoRecorder.current.stop();
+ };
const start = (e: React.PointerEvent) => {
- setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
- // start recording if not already recording
- if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
-
- return true; // cancels propagation to documentView to avoid selecting it.
- }, false, false);
- }
+ setupMoveUpEvents(
+ {},
+ e,
+ returnTrue,
+ returnFalse,
+ e => {
+ // start recording if not already recording
+ if (!videoRecorder.current || videoRecorder.current.state === 'inactive') record();
+
+ return true; // cancels propagation to documentView to avoid selecting it.
+ },
+ false,
+ false
+ );
+ };
const undoPrevious = (e: React.PointerEvent) => {
e.stopPropagation();
setDoUndo(prev => !prev);
- }
+ };
- const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+ const handleOnTimeUpdate = () => {
+ playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
+ };
const millisecondToMinuteSecond = (milliseconds: number) => {
const toTwoDigit = (digit: number) => {
- return String(digit).length == 1 ? "0" + digit : digit
- }
+ return String(digit).length == 1 ? '0' + digit : digit;
+ };
const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
- return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
- }
+ return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds);
+ };
return (
<div className="recording-container">
<div className="video-wrapper">
- <video id={`video-${props.id}`}
- autoPlay
- muted
- onTimeUpdate={() => handleOnTimeUpdate()}
- ref={videoElementRef}
- />
+ <video id={`video-${props.id}`} autoPlay muted onTimeUpdate={() => handleOnTimeUpdate()} ref={videoElementRef} />
<div className="recording-sign">
<span className="dot" />
<p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
</div>
<div className="controls">
-
<div className="controls-inner-container">
- <div className="record-button-wrapper">
- {recording ?
- <button className="stop-button" onPointerDown={pause} /> :
- <button className="record-button" onPointerDown={start} />
- }
- </div>
-
- {!recording && (videos.length > 0 ?
-
- <div className="options-wrapper video-edit-wrapper">
- <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
- <MdBackspace onPointerDown={undoPrevious} />
- </IconContext.Provider>
- <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
- <FaCheckCircle onPointerDown={finish} />
- </IconContext.Provider>
- </div>
-
- : <div className="options-wrapper track-screen-wrapper">
- <label className="track-screen">
- <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
- <span className="checkmark"></span>
- Track Screen
- </label>
- </div>)}
-
+ <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div>
+
+ {!recording &&
+ (videos.length > 0 ? (
+ <div className="options-wrapper video-edit-wrapper">
+ <IconContext.Provider value={{ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }}>
+ <MdBackspace onPointerDown={undoPrevious} />
+ </IconContext.Provider>
+ <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}>
+ <FaCheckCircle onPointerDown={finish} />
+ </IconContext.Provider>
+ </div>
+ ) : (
+ <div className="options-wrapper track-screen-wrapper">
+ <label className="track-screen">
+ <input
+ type="checkbox"
+ checked={trackScreen}
+ onChange={e => {
+ setTrackScreen(e.target.checked);
+ }}
+ />
+ <span className="checkmark"></span>
+ Track Screen
+ </label>
+ </div>
+ ))}
</div>
-
</div>
- <ProgressBar
- videos={videos}
- setVideos={setVideos}
- orderVideos={orderVideos}
- progress={progress}
- recording={recording}
- doUndo={doUndo}
- setCanUndo={setCanUndo}
- />
+ <ProgressBar videos={videos} setVideos={setVideos} orderVideos={orderVideos} progress={progress} recording={recording} doUndo={doUndo} setCanUndo={setCanUndo} />
</div>
- </div>)
-} \ No newline at end of file
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index f94996c66..7bf765042 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast } from '../../../fields/Types';
import { AudioField, VideoField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
@@ -129,8 +129,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
}
}
getAnchor = (addAsAnnotation: boolean) => {
- const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
+ const startTime = Cast(this.layoutDoc._layout_currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
};
videoLoad = () => {
@@ -236,13 +236,13 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
console.log('screenshotbox: upload');
const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
const [{ result }] = await Networking.UploadFilesToServer(file);
- this.dataDoc[this.fieldKey + '-duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
+ this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
if (!(result instanceof Error)) {
// convert this screenshotBox into normal videoBox
this.dataDoc.type = DocumentType.VID;
this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
- this.layoutDoc._fitWidth = undefined;
+ this.layoutDoc._layout_fitWidth = undefined;
this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
} else alert('video conversion failed');
};
@@ -270,15 +270,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
setupDictation = () => {
if (this.dataDoc[this.fieldKey + '-dictation']) return;
const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
- dictationText._autoHeight = false;
+ dictationText._layout_autoHeight = false;
const dictationTextProto = Doc.GetProto(dictationText);
dictationTextProto.recordingSource = this.dataDoc;
dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
};
- contentFunc = () => [this.threed, this.content];
- videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
+ videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
TraceMobx();
@@ -287,7 +286,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
<div className="videoBox-viewer">
<div style={{ position: 'relative', height: this.videoPanelHeight() }}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
PanelHeight={this.videoPanelHeight}
PanelWidth={this.props.PanelWidth}
focus={this.props.focus}
@@ -296,25 +298,26 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
select={emptyFunction}
isContentActive={returnFalse}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={emptyFunction}
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
- CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- {this.contentFunc}
+ renderDepth={this.props.renderDepth + 1}>
+ <>
+ {this.threed}
+ {this.content}
+ </>
</CollectionFreeFormView>
</div>
- <div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
+ <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}>
{!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
<FormattedTextBox
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
Document={this.dataDoc[this.fieldKey + '-dictation']}
fieldKey={'text'}
PanelHeight={this.formattedPanelHeight}
- isAnnotationOverlay={true}
select={emptyFunction}
isContentActive={emptyFunction}
NativeDimScaling={returnOne}
@@ -324,9 +327,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
- CollectionView={undefined}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
+ />
)}
</div>
</div>
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 281967a21..37fda14fc 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyString } from '../../../Utils';
+import { returnAlways, returnEmptyString, returnTrue } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
import { CompileScript, ScriptParam } from '../../util/Scripting';
@@ -20,6 +20,7 @@ import { EditableView } from '../EditableView';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { OverlayView } from '../OverlayView';
import { DocumentIconContainer } from './DocumentIcon';
+import { DocFocusOptions, DocumentView } from './DocumentView';
import './ScriptingBox.scss';
const _global = (window /* browser */ || global) /* node */ as any;
@@ -113,6 +114,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}
}
+ onClickScriptDisable = returnAlways;
+
@action
componentDidMount() {
this.props.setContentView?.(this);
@@ -164,7 +167,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// only included in buttons, transforms scripting UI to a button
@action
onFinish = () => {
- this.rootDoc.layoutKey = 'layout';
+ this.rootDoc.layout_fieldKey = 'layout';
};
// displays error message
@@ -179,13 +182,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const params: ScriptParam = {};
this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim()));
- const result = CompileScript(this.rawText, {
- editable: true,
- transformer: DocumentIconContainer.getTransformer(),
- params,
- typecheck: false,
- });
- Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true);
+ const result = !this.rawText.trim()
+ ? ({ compiled: false, errors: undefined } as any)
+ : CompileScript(this.rawText, {
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer(),
+ params,
+ typecheck: false,
+ });
+ Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true);
this.onError(result.compiled ? undefined : result.errors);
return result.compiled;
};
@@ -481,10 +486,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
return value;
}
- scrollFocus = () => {
- return undefined;
- };
-
getSuggestedParams(pos: number) {
const firstScript = this.rawText.slice(0, pos);
const indexP = firstScript.lastIndexOf('.');
@@ -701,7 +702,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// toolbar (with compile and apply buttons) for scripting UI
renderScriptingTools() {
- const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : '');
return (
<div className="scriptingBox-toolbar">
<button
@@ -729,7 +730,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
Save
</button>
- {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout
+ {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout
<button
className={buttonStyle}
onPointerDown={e => {
@@ -775,7 +776,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// toolbar (with edit and run buttons and error message) for params UI
renderTools(toolSet: string, func: () => void) {
- const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : '');
return (
<div className="scriptingBox-toolbar">
<button
@@ -794,7 +795,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}}>
{toolSet}
</button>
- {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : (
+ {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : (
<button
className={buttonStyle}
onPointerDown={e => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1dfa55c64..9cf929679 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,17 +3,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
import { observer } from 'mobx-react';
import { basename } from 'path';
-import * as rp from 'request-promise';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, HeightSym, StrListCast, WidthSym } from '../../../fields/Doc';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, 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 { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
@@ -27,11 +28,13 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
+import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
-import { ObjectField } from '../../../fields/ObjectField';
-import { DocFocusOptions, OpenWhere } from './DocumentView';
+import { ScriptField } from '../../../fields/ScriptField';
+import { FollowLinkScript } from '../../util/LinkFollower';
const path = require('path');
/**
@@ -51,30 +54,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(VideoBox, fieldKey);
}
- /**
- * Uploads an image buffer to the server and stores with specified filename. by default the image
- * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
- * @param imageUri the bytes of the image
- * @param returnedFilename the base filename to store the image on the server
- * @param nosuffix optionally suppress creating multiple resolution images
- */
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
- try {
- const posting = Utils.prepend('/uploadURI');
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix,
- replaceRootFilename,
- },
- json: true,
- });
- return returnedUri;
- } catch (e) {
- console.log('VideoBox :' + e);
- }
- }
static _youtubeIframeCounter: number = 0;
static heightPercent = 80; // height of video relative to videoBox when timeline is open
@@ -109,12 +88,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _scrubbing: boolean = false;
@computed get links() {
- return DocListCast(this.dataDoc.links);
+ return LinkManager.Links(this.dataDoc);
}
@computed get heightPercent() {
- return NumCast(this.layoutDoc._timelineHeightPercent, 100);
+ return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100);
} // current percent of video relative to VideoBox height
- // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); }
@observable rawDuration: number = 0;
@computed get youtubeVideoId() {
@@ -242,10 +221,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
+ _keepCurrentlyPlaying = false; // flag to prevent document when paused from being removed from global 'currentlyPlaying' list
+ IsPlaying = () => this._playing;
+ TogglePause = () => {
+ if (!this._playing) this.Play();
+ else {
+ this._keepCurrentlyPlaying = true;
+ this.pause();
+ setTimeout(() => (this._keepCurrentlyPlaying = false));
+ }
+ };
+
// pauses video
- @action public Pause = (update: boolean = true) => {
+ @action public pause = (update: boolean = true) => {
this._playing = false;
- this.removeCurrentlyPlaying();
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
@@ -263,6 +252,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
this._playRegionTimer = undefined;
};
+ @action Pause = (update: boolean = true) => {
+ this.pause(update);
+ !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
+ };
// toggles video full screen
@action public FullScreen = () => {
@@ -332,19 +325,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
y: NumCast(this.layoutDoc.y, 1),
_width: 150,
_height: 50,
- title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true,
+ title: (this.layoutDoc._layout_currentTimecode || 0).toString(),
+ onClick: FollowLinkScript(),
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ DocUtils.MakeLink(b, this.rootDoc, { link_relationship: 'video snapshot' });
Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
- timecode: this.layoutDoc._currentTimecode,
+ timecode: this.layoutDoc._layout_currentTimecode,
}).then(response => {
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
this.props.removeDocument?.(b);
- this.createRealSummaryLink(resolved);
+ this.createSnapshotLink(resolved);
}
});
} else {
@@ -352,51 +345,51 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
- const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
+ const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
}
};
updateIcon = () => {
const makeIcon = (returnedfilename: string) => {
this.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc['icon-nativeWidth'] = this.layoutDoc[WidthSym]();
- this.dataDoc['icon-nativeHeight'] = this.layoutDoc[HeightSym]();
+ this.dataDoc.icon_nativeWidth = this.layoutDoc[WidthSym]();
+ this.dataDoc.icon_nativeHeight = this.layoutDoc[HeightSym]();
};
this.Snapshot(undefined, undefined, makeIcon);
};
// creates link for snapshot
- createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
const width = NumCast(this.layoutDoc._width) || 1;
const height = NumCast(this.layoutDoc._height);
- const imageSummary = Docs.Create.ImageDocument(url, {
+ const imageSnapshot = Docs.Create.ImageDocument(url, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc),
_nativeHeight: Doc.NativeHeight(this.layoutDoc),
x: NumCast(this.layoutDoc.x) + width,
y: NumCast(this.layoutDoc.y),
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
_width: 150,
_height: (height / width) * 150,
- title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
+ title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
});
- Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
- Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor(true) }, 'video snapshot');
- link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSnapshot);
+ const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
+ link && (Doc.GetProto(link.link_anchor_2 as Doc).timecodeToHide = NumCast((link.link_anchor_2 as Doc).timecodeToShow) + 3);
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
};
- getAnchor = (addAsAnnotation: boolean) => {
- const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
- return (
- CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) ||
- this.rootDoc
- );
+ if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
+ const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+ return anchor;
};
// sets video info on load
@@ -409,25 +402,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
if (Number.isFinite(this.player!.duration)) {
this.rawDuration = this.player!.duration;
- this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
- } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
+ this.dataDoc[this.fieldKey + '_duration'] = this.rawDuration;
+ } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '_duration']);
});
// updates video time
@action
updateTimecode = () => {
- this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+ this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
try {
- this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+ this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
console.log('Video Timecode Exception:', e);
}
};
+ // getView = async (doc: Doc) => {
+ // return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ // };
+
// extracts video thumbnails and saves them as field of doc
getVideoThumbnails = () => {
- if (this.dataDoc.thumbnails !== undefined) return;
- this.dataDoc.thumbnails = new List<string>();
+ if (this.dataDoc[this.fieldKey + '_thumbnails'] !== undefined) return;
+ this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>();
const thumbnailPromises: Promise<any>[] = [];
const video = document.createElement('video');
@@ -440,12 +437,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
- thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
+ thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
if (newTime < video.duration) {
video.currentTime = newTime;
} else {
- Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc.thumbnails = new List<string>(thumbnails)));
+ Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails)));
}
};
@@ -463,12 +460,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
this._disposers.reactionDisposer?.();
this._disposers.reactionDisposer = reaction(
- () => NumCast(this.layoutDoc._currentTimecode),
+ () => NumCast(this.layoutDoc._layout_currentTimecode),
time => !this._playing && (vref.currentTime = time),
{ fireImmediately: true }
);
- (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails();
+ (!this.dataDoc[this.fieldKey + '_thumbnails'] || this.dataDoc[this.fieldKey + '_thumbnails'].length != VideoBox.numThumbnails) && this.getVideoThumbnails();
}
};
@@ -534,7 +531,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
// delete assoicated video data
this.dataDoc[this.props.fieldKey] = '';
- this.dataDoc[this.fieldKey + '-duration'] = '';
+ this.dataDoc[this.fieldKey + '_duration'] = '';
// delete assoicated presentation data
this.dataDoc[this.fieldKey + '-presentation'] = '';
},
@@ -576,7 +573,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
key="video"
autoPlay={this._screenCapture}
ref={this.setVideoRef}
- style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
+ style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -623,8 +620,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._disposers.reactionDisposer?.();
this._disposers.youtubeReactionDisposer?.();
this._disposers.reactionDisposer = reaction(
- () => this.layoutDoc._currentTimecode,
- () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))
+ () => this.layoutDoc._layout_currentTimecode,
+ () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode))
);
this._disposers.youtubeReactionDisposer = reaction(
() => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
@@ -680,15 +677,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._clicking = false;
if (this.props.isContentActive()) {
// const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
- // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+ // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
- this.layoutDoc._timelineHeightPercent = 80;
+ this.layoutDoc._layout_timelineHeightPercent = 80;
}
return false;
}),
emptyFunction,
() => {
- this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+ this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
setTimeout(
action(() => (this._clicking = false)),
500
@@ -699,23 +696,24 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- // removes video from currently playing display
+ // removes from currently playing display
@action
removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ const docView = this.props.DocumentView?.();
+ if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
};
-
- // adds video to currently playing display
+ // adds doc to currently playing display
@action
addCurrentlyPlaying = () => {
+ const docView = this.props.DocumentView?.();
if (!CollectionStackedTimeline.CurrentlyPlaying) {
CollectionStackedTimeline.CurrentlyPlaying = [];
}
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(docView);
}
};
@@ -723,7 +721,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
- const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
+ const start = untracked(() => Math.round(NumCast(this.layoutDoc._layout_currentTimecode)));
return (
<iframe
key={this._youtubeIframeId}
@@ -742,7 +740,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action.bound
addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
- const curTime = NumCast(this.layoutDoc._currentTimecode);
+ const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
return this.addDocument(doc);
}
@@ -861,7 +859,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -872,7 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}),
returnFalse,
() => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
- false
+ false, false
);
}
};
@@ -892,18 +890,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
.scale(this.scaling())
.translate(0, (-this.heightPercent / 100) * this.props.PanelHeight());
- setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time);
+ setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time);
timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100;
playing = () => this._playing;
- contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
scaling = () => this.props.NativeDimScaling?.() || 1;
panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
- panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
+ panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
screenToLocalTransform = () => {
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
@@ -953,14 +949,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- scrollFocus = (doc: Doc, options: DocFocusOptions) => {
- if (doc !== this.rootDoc) {
- const showTime = Cast(doc._timecodeToShow, 'number', null);
- showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
- return 0.1;
- }
- };
-
// renders CollectionStackedTimeline
@computed get renderTimeline() {
return (
@@ -968,14 +956,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
{...this.props}
+ dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.audiopath}
+ thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])}
renderDepth={this.props.renderDepth + 1}
startTag={'_timecodeToShow' /* videoStart */}
endTag={'_timecodeToHide' /* videoEnd */}
bringToFront={emptyFunction}
- CollectionView={undefined}
playFrom={this.playFrom}
setTime={this.setPlayheadTime}
playing={this.playing}
@@ -1016,7 +1005,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw;
cropping.title = 'crop: ' + this.rootDoc.title;
cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
cropping.y = NumCast(this.rootDoc.y);
@@ -1024,28 +1013,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
cropping.timecodeToHide = undefined;
cropping.timecodeToShow = undefined;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.VID;
croppingProto.layout = VideoBox.LayoutString('data');
croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
croppingProto.videoCrop = true;
- croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
- croppingProto.viewScale = viewScale;
- croppingProto.viewScaleMin = viewScale;
- croppingProto.panX = anchx / viewScale;
- croppingProto.panY = anchy / viewScale;
- croppingProto.panXMin = anchx / viewScale;
- croppingProto.panXMax = anchw / viewScale;
- croppingProto.panYMin = anchy / viewScale;
- croppingProto.panYMax = anchh / viewScale;
+ croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
+ croppingProto.freeform_scale = viewScale;
+ croppingProto.freeform_scale_min = viewScale;
+ croppingProto.freeform_ = anchx / viewScale;
+ croppingProto.freeform_panY = anchy / viewScale;
+ croppingProto.freeform_panX_min = anchx / viewScale;
+ croppingProto.freeform_panX_max = anchw / viewScale;
+ croppingProto.freeform_panY_min = anchy / viewScale;
+ croppingProto.freeform_panY_max = anchh / viewScale;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
}
this.props.bringToFront(cropping);
return cropping;
@@ -1063,7 +1052,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
style={{
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
borderRadius,
- overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined,
+ overflow: this.props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined,
}}
onWheel={e => {
e.stopPropagation();
@@ -1080,23 +1069,27 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
left: (this.props.PanelWidth() - this.panelWidth()) / 2,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
- CollectionView={undefined}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
+ isAnyChildContentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocalTransform}
docFilters={this.timelineDocFilter}
select={emptyFunction}
+ focus={emptyFunction}
NativeDimScaling={returnOne}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
+ {this.youtubeVideoId ? this.youtubeContent : this.content}
</CollectionFreeFormView>
</div>
{this.annotationLayer}
@@ -1126,7 +1119,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get UIButtons() {
const bounds = this.props.docViewPath().lastElement().getBounds();
const width = (bounds?.right || 0) - (bounds?.left || 0);
- const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ const curTime = NumCast(this.layoutDoc._layout_currentTimecode) - (this.timeline?.clipStart || 0);
return (
<>
<div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 6f578a9fc..75847c100 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -183,6 +183,7 @@
height: 100%;
position: absolute;
top: 0;
+ left: 0;
body {
::selection {
color: white;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d0d638e98..a898398a1 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,18 +1,20 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as WebRequest from 'web-request';
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { HtmlField } from '../../../fields/HtmlField';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, removeStyleSheetRule, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
+import { DragManager } from '../../util/DragManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -21,21 +23,21 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { DocumentDecorations } from '../DocumentDecorations';
import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { Annotation } from '../pdf/Annotation';
+import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
import { SidebarAnnos } from '../SidebarAnnos';
import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentView, DocumentViewInternal, DocumentViewProps } from './DocumentView';
+import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { LinkDocPreview } from './LinkDocPreview';
-import { VideoBox } from './VideoBox';
+import { PinProps, PresBox } from './trails';
import './WebBox.scss';
import React = require('react');
-import { DragManager } from '../../util/DragManager';
+import { RefField } from '../../../fields/RefField';
const { CreateImage } = require('./WebBoxRenderer');
const _global = (window /* browser */ || global) /* node */ as any;
const htmlToText = require('html-to-text');
@@ -46,6 +48,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
+ static webStyleSheet = addStyleSheet();
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -53,19 +56,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _disposers: { [name: string]: IReactionDisposer } = {};
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _keyInput = React.createRef<HTMLInputElement>();
- private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop));
+ private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop));
private _sidebarRef = React.createRef<SidebarAnnos>();
private _searchRef = React.createRef<HTMLInputElement>();
private _searchString = '';
+ private _scrollTimer: any;
private get _getAnchor() {
return AnchorMenu.Instance?.GetAnchor;
}
- @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
+ @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render.
@observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled
@observable private _searching: boolean = false;
@observable private _showSidebar = false;
- @observable private _scrollTimer: any;
@observable private _webPageHasBeenRendered = false;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
@@ -90,7 +93,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this.allAnnotations.filter(a => a.textInlineAnnotations);
}
@computed get webField() {
- return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url;
+ return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url;
}
@computed get webThumb() {
return (
@@ -98,7 +101,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ImageCast(
this.layoutDoc['thumb-frozen'],
ImageCast(
- this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight)
+ this.layoutDoc.thumbScrollTop === this.layoutDoc._layout_scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight)
? this.layoutDoc.thumb
: undefined
)
@@ -140,81 +143,65 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
updateThumb = async () => {
- const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href;
- const scrollTop = NumCast(this.layoutDoc._scrollTop);
+ if (!this._iframe) return;
+ const scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth();
- if (
- !this.rootDoc.thumbLockout &&
- !this.props.dontRegisterView &&
- this._iframe &&
- !imageBitmap &&
- (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight)
- ) {
- var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
- if (!htmlString) {
- htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text();
- }
- this.layoutDoc.thumb = undefined;
- this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates.
- CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
- .then((data_url: any) => {
- if (data_url.includes('<!DOCTYPE')) {
- console.log('BAD DATA IN THUMB CREATION');
- return;
- }
- VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
- setTimeout(
- action(() => {
- this.rootDoc.thumbLockout = false;
- this.layoutDoc.thumb = new ImageField(returnedfilename);
- this.layoutDoc.thumbScrollTop = scrollTop;
- this.layoutDoc.thumbNativeWidth = nativeWidth;
- this.layoutDoc.thumbNativeHeight = nativeHeight;
- }),
- 500
- )
- );
- })
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
- });
+ var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
+ if (!htmlString) {
+ htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text();
}
+ this.layoutDoc.thumb = undefined;
+ this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates.
+ CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
+ .then((data_url: any) => {
+ if (data_url.includes('<!DOCTYPE')) {
+ console.log('BAD DATA IN THUMB CREATION');
+ return;
+ }
+ Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
+ setTimeout(
+ action(() => {
+ this.rootDoc.thumbLockout = false;
+ this.layoutDoc.thumb = new ImageField(returnedfilename);
+ this.layoutDoc.thumbScrollTop = scrollTop;
+ this.layoutDoc.thumbNativeWidth = nativeWidth;
+ this.layoutDoc.thumbNativeHeight = nativeHeight;
+ }),
+ 500
+ )
+ );
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
};
+
async componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons)
runInAction(() => {
- this._annotationKeySuffix = () => this._urlHash + '-annotations';
+ this._annotationKeySuffix = () => this._urlHash + '_annotations';
const reqdFuncs: { [key: string]: string } = {};
- // 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)
- reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`;
- reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`;
+ // 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)
+ 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);
});
- reaction(
- () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document),
- async selected => {
- if (selected) {
- this._webPageHasBeenRendered = true;
- } else if (
- (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
- LightboxView.LightboxDoc !== this.rootDoc
- ) {
- // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
- this.updateThumb();
- }
- },
- { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) }
+ this._disposers.urlchange = reaction(
+ () => WebCast(this.rootDoc.data),
+ url => {
+ this.submitURL(url.url.href, false, false);
+ }
);
- this._disposers.autoHeight = reaction(
- () => this.layoutDoc._autoHeight,
- autoHeight => {
- if (autoHeight) {
- this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']);
- this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
+ this._disposers.layout_autoHeight = reaction(
+ () => this.layoutDoc._layout_autoHeight,
+ layout_autoHeight => {
+ if (layout_autoHeight) {
+ this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']);
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
}
}
);
@@ -238,7 +225,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
this._disposers.scrollReaction = reaction(
- () => NumCast(this.layoutDoc._scrollTop),
+ () => NumCast(this.layoutDoc._layout_scrollTop),
scrollTop => {
const viewTrans = StrCast(this.Document._viewTransition);
const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
@@ -257,6 +244,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
}
+ private _selectionText: string = '';
+ private _selectionContent: DocumentFragment | undefined;
+ selectionText = () => this._selectionText;
+ selectionContent = () => this._selectionContent;
@action
createTextAnnotation = (sel: Selection, selRange: Range | undefined) => {
if (this._mainCont.current && selRange) {
@@ -276,6 +267,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
//this._selectionText = selRange.cloneContents().textContent || "";
+ this._selectionContent = selRange?.cloneContents();
+ this._selectionText = this._selectionContent?.textContent || '';
// clear selection
if (sel.empty) sel.empty(); // Chrome
@@ -283,31 +276,42 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this._savedAnnotations;
};
- menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
-
setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
- scrollFocus = (doc: Doc, options: DocFocusOptions) => {
- if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant);
- if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
- this.toggleSidebar(options.instant);
- }
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
- if (doc !== this.rootDoc && this._outerRef.current) {
+ focus = (anchor: Doc, options: DocFocusOptions) => {
+ if (anchor !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
- if (scrollTo !== undefined && this._initialScroll === undefined) {
- const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
- this.goTo(scrollTo, focusSpeed, options.easeFunc);
- return focusSpeed;
- } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
- this._initialScroll = scrollTo;
+ const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
+ if (scrollTo !== undefined) {
+ if (this._initialScroll === undefined) {
+ const focusTime = options.zoomTime ?? 500;
+ this.goTo(scrollTo, focusTime, options.easeFunc);
+ return focusTime;
+ } else {
+ this._initialScroll = scrollTo;
+ }
}
}
- return undefined;
};
- getAnchor = (addAsAnnotation: boolean) => {
+ @action
+ getView = (doc: Doc) => {
+ if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.()));
+ if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify();
+ const webUrl = WebCast(doc.presData)?.url;
+ if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
+ if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+
+ sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+ if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(false);
+ return true;
+ }
+ return this.props.addDocTab(doc, where);
+ };
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
let ele: Opt<HTMLDivElement> = undefined;
try {
const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
@@ -318,14 +322,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
} catch (e) {}
const anchor =
this._getAnchor(this._savedAnnotations, false) ??
- Docs.Create.WebanchorDocument(this._url, {
- title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop),
- y: NumCast(this.layoutDoc._scrollTop),
- unrendered: true,
+ Docs.Create.WebConfigDocument({
+ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop),
+ y: NumCast(this.layoutDoc._layout_scrollTop),
+ annotationOn: this.rootDoc,
});
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc);
anchor.text = ele?.textContent ?? '';
anchor.textHtml = ele?.innerHTML;
- addAsAnnotation && this.addDocumentWrapper(anchor);
+ //addAsAnnotation &&
+ this.addDocumentWrapper(anchor);
return anchor;
};
@@ -341,8 +347,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const sel = this._iframe.contentWindow.getSelection();
if (sel) {
+ this._selectionText = sel.toString();
+ AnchorMenu.Instance.setSelectedText(sel.toString());
this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined);
- AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale);
+ AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale);
+ // Changing which document to add the annotation to (the currently selected WebBox)
+ GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`);
+ GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
}
};
@@ -354,7 +365,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]);
+ e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]);
if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) {
setTimeout(
action(() => (this._marqueeing = undefined)),
@@ -376,16 +387,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ContextMenu.Instance.setIgnoreEvents(true);
}
};
-
- getScrollHeight = () => this._scrollHeight;
-
isFirefox = () => {
return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
};
iframeClick = () => this._iframeClick;
iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale;
- addStyleSheet(document: any, styleType: string = 'text/css') {
+ addWebStyleSheet(document: any, styleType: string = 'text/css') {
if (document) {
const style = document.createElement('style');
style.type = styleType;
@@ -393,7 +401,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return (sheets as any).sheet;
}
}
- addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
+ addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
const propText =
typeof css === 'string'
? css
@@ -410,8 +418,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (this._initialScroll !== undefined) {
this.setScrollPos(this._initialScroll);
}
-
- this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
+ this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0;
+ this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
let href: Opt<string>;
try {
@@ -435,12 +443,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.replace('search&', 'search?')
.replace('?gbv=1', '');
}
- this.submitURL(requrlraw, undefined, true);
+ this.setData(requrlraw);
}
const iframeContent = iframe?.contentDocument;
if (iframeContent) {
iframeContent.addEventListener('pointerup', this.iframeUp);
iframeContent.addEventListener('pointerdown', this.iframeDown);
+ // iframeContent.addEventListener(
+ // 'wheel',
+ // e => {
+ // e.ctrlKey && e.preventDefault();
+ // },
+ // { passive: false }
+ // );
const initHeights = () => {
this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0);
if (this._scrollHeight) {
@@ -454,7 +469,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
action(() => initHeights),
5000
);
- iframe.setAttribute('enable-annotation', 'true');
iframeContent.addEventListener(
'click',
undoBatch(
@@ -465,49 +479,63 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
const origin = this.webField?.origin;
if (href && origin) {
+ const batch = UndoManager.StartBatch('webclick');
e.stopPropagation();
- setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin)));
+ setTimeout(() => {
+ this.setData(href.replace(Utils.prepend(''), origin));
+ batch.end();
+ });
if (this._outerRef.current) {
- this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop);
+ this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
this._outerRef.current.scrollLeft = 0;
}
}
})
)
);
- iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false);
- //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false));
+ iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false });
}
};
@action
- iframeWheel = (e: any) => {
+ iframeWheel = (e: WheelEvent) => {
if (!this._scrollTimer) {
- this._scrollTimer = setTimeout(
- action(() => (this._scrollTimer = undefined)),
- 250
- ); // this turns events off on the iframe which allows scrolling to change direction smoothly
+ addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' });
+ this._scrollTimer = setTimeout(() => {
+ this._scrollTimer = undefined;
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly
+ }
+ if (e.ctrlKey) {
+ if (this._innerCollectionView) {
+ this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY);
+ const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop);
+ this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale);
+ }
+ e.preventDefault();
}
};
@action
setDashScrollTop = (scrollTop: number, timeout: number = 250) => {
const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
- this._scrollTimer && clearTimeout(this._scrollTimer);
- this._scrollTimer = setTimeout(
- action(() => {
- this._scrollTimer = undefined;
- const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
- if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
- this.layoutDoc.thumb = undefined;
- this.layoutDoc.thumbScrollTop = undefined;
- this.layoutDoc.thumbNativeWidth = undefined;
- this.layoutDoc.thumbNativeHeight = undefined;
- this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop;
- } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop;
- }),
- timeout
- );
+ if (this._scrollTimer) {
+ clearTimeout(this._scrollTimer);
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ }
+ addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' });
+ this._scrollTimer = setTimeout(() => {
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ this._scrollTimer = undefined;
+ const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
+ if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
+ this.layoutDoc.thumb = undefined;
+ this.layoutDoc.thumbScrollTop = undefined;
+ this.layoutDoc.thumbNativeWidth = undefined;
+ this.layoutDoc.thumbNativeHeight = undefined;
+ this.layoutDoc.layout_scrollTop = this._outerRef.current.scrollTop = newScrollTop;
+ } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop;
+ }, timeout);
};
goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => {
@@ -523,14 +551,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
forward = (checkAvailable?: boolean) => {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'), []);
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+ const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'), []);
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
if (checkAvailable) return future.length;
runInAction(() => {
if (future.length) {
const curUrl = this._url;
- this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]);
+ this.dataDoc[this.fieldKey + '_history'] = new List<string>([...history, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
+ this._scrollHeight = 0;
if (this._webUrl === this._url) {
this._webUrl = curUrl;
setTimeout(action(() => (this._webUrl = this._url)));
@@ -544,15 +573,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
back = (checkAvailable?: boolean) => {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+ const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'));
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
if (checkAvailable) return history.length;
runInAction(() => {
if (history.length) {
const curUrl = this._url;
- if (future === undefined) this.dataDoc[this.fieldKey + '-future'] = new List<string>([this._url]);
- else this.dataDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]);
+ if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]);
+ else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
+ this._scrollHeight = 0;
if (this._webUrl === this._url) {
this._webUrl = curUrl;
setTimeout(action(() => (this._webUrl = this._url)));
@@ -579,23 +609,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (!newUrl) return;
if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl;
try {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'));
- const url = this.webField?.toString();
- if (url && !preview) {
- this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]);
- this.layoutDoc._scrollTop = 0;
+ if (!preview) {
if (this._webPageHasBeenRendered) {
this.layoutDoc.thumb = undefined;
this.layoutDoc.thumbScrollTop = undefined;
this.layoutDoc.thumbNativeWidth = undefined;
this.layoutDoc.thumbNativeHeight = undefined;
}
- future && (future.length = 0);
}
if (!preview) {
- this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
- !dontUpdateIframe && (this._webUrl = this._url);
+ if (!dontUpdateIframe) {
+ this._webUrl = this._url;
+ }
}
} catch (e) {
console.log('WebBox URL error:' + this._url);
@@ -609,53 +634,34 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const uri = dataTransfer.getData('text/uri-list');
const url = uri || html || this._url || '';
const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url;
- this.submitURL(newurl);
+ this.setData(newurl);
e.stopPropagation();
};
+
+ @action
+ setData = (data: Field | Promise<RefField | undefined>) => {
+ if (!(typeof data === 'string') && !(data instanceof WebField)) return false;
+ if (Field.toString(data) === this._url) return false;
+ this._scrollHeight = 0;
+ const oldUrl = this._url;
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
+ const weburl = new WebField(Field.toString(data));
+ this.dataDoc[this.fieldKey + '_future'] = new List<string>([]);
+ this.dataDoc[this.fieldKey + '_history'] = new List<string>([...(history || []), oldUrl]);
+ this.dataDoc[this.fieldKey] = weburl;
+ return true;
+ };
onWebUrlValueKeyDown = (e: React.KeyboardEvent) => {
- e.key === 'Enter' && this.submitURL(this._keyInput.current!.value);
+ if (e.key === 'Enter') this.setData(this._keyInput.current!.value);
e.stopPropagation();
};
- @computed get urlEditor() {
- return (
- <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}>
- <input
- className="collectionMenu-urlInput"
- key={this._url}
- placeholder="ENTER URL"
- defaultValue={this._url}
- onDrop={this.onWebUrlDrop}
- onDragOver={e => e.preventDefault()}
- onKeyDown={this.onWebUrlValueKeyDown}
- onClick={e => {
- this._keyInput.current!.select();
- e.stopPropagation();
- }}
- ref={this._keyInput}
- />
- <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}>
- <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}>
- GO
- </button>
- <button className="submitUrl" onClick={() => this.back}>
- {' '}
- <FontAwesomeIcon icon="caret-left" size="lg" />{' '}
- </button>
- <button className="submitUrl" onClick={() => this.forward}>
- {' '}
- <FontAwesomeIcon icon="caret-right" size="lg" />{' '}
- </button>
- </div>
- </div>
- );
- }
-
specificContextMenu = (e: React.MouseEvent | PointerEvent): void => {
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
if (!cm.findByDescription('Options...')) {
- !Doc.noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc.useCors = !this.layoutDoc.useCors), icon: 'snowflake' });
+ !Doc.noviceMode &&
+ funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' });
funcs.push({
description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts',
event: () => {
@@ -668,16 +674,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
icon: 'snowflake',
});
funcs.push({
- description: (!this.layoutDoc.forceReflow ? 'Force' : 'Prevent') + ' Reflow',
+ description: (!this.layoutDoc.layout_forceReflow ? 'Force' : 'Prevent') + ' Reflow',
event: () => {
- const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1);
- this.layoutDoc.forceReflow = !nw;
+ const nw = !this.layoutDoc.layout_forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1);
+ this.layoutDoc.layout_forceReflow = !nw;
if (nw) {
- Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true);
+ Doc.SetInPlace(this.layoutDoc, this.fieldKey + '_nativeWidth', nw, true);
}
},
icon: 'snowflake',
});
+ funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' });
cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
@@ -718,57 +725,43 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
@computed get urlContent() {
- if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null;
- this.props.thumbShown?.();
- const field = this.dataDoc[this.props.fieldKey];
- let view;
+ setTimeout(
+ action(() => {
+ if (this._initialScroll === undefined && !this._webPageHasBeenRendered) {
+ this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop)));
+ }
+ this._webPageHasBeenRendered = true;
+ })
+ );
+ const field = this.rootDoc[this.props.fieldKey];
if (field instanceof HtmlField) {
- view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />;
- } else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl;
- view = (
+ return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />;
+ }
+ if (field instanceof WebField) {
+ const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl;
+ return (
<iframe
className="webBox-iframe"
- enable-annotation={'true'}
- style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }}
ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
src={url}
onLoad={this.iframeLoaded}
+ scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document.
// the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
// sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`}
/>
);
- } else {
- view = (
- <iframe
- className="webBox-iframe"
- enable-annotation={'true'}
- style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly
- ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
- src={'https://crossorigin.me/https://cs.brown.edu'}
- />
- );
}
- setTimeout(
- action(() => {
- this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0);
- if (this._initialScroll === undefined && !this._webPageHasBeenRendered) {
- this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop)));
- }
- this._webPageHasBeenRendered = true;
- })
- );
- return view;
+ return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />;
}
addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
- (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url));
+ (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url)));
return this.addDocument(doc, annotationKey);
};
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
return this.addDocumentWrapper(doc, sidebarKey);
};
@observable _draggingSidebar = false;
@@ -784,15 +777,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
- const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
+ const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight']);
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth;
if (ratio >= 1) {
this.layoutDoc.nativeWidth = nativeWidth * ratio;
this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio);
onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
}),
@@ -814,7 +807,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={e => this.sidebarBtnDown(e, true)}>
@@ -825,12 +818,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
if (!nativeWidth) {
- const defaultNativeWidth = this.dataDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym]();
+ const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym]();
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth);
- nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
}
const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth;
@@ -840,27 +833,42 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth;
this._showSidebar = true;
} else {
- this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar;
+ this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar;
this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
- if (!this.layoutDoc._showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) {
- this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '-nativeWidth'] = undefined;
+ if (!this.layoutDoc._layout_showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) {
+ this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '_nativeWidth'] = undefined;
} else {
this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
}
}
});
+ @action
+ onZoomWheel = (e: React.WheelEvent) => {
+ if (this.props.isContentActive(true)) {
+ e.stopPropagation();
+ }
+ };
sidebarWidth = () => {
if (!this.SidebarShown) return 0;
if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
+ _innerCollectionView: CollectionFreeFormView | undefined;
+ zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1;
+ setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView);
+
@computed get content() {
- const interactive =
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+ const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
return (
- <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
- {this.urlContent}
+ <div
+ className={'webBox-cont' + (interactive ? '-interactive' : '')}
+ onKeyDown={e => e.stopPropagation()}
+ style={{
+ width: !this.layoutDoc.layout_forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']) || `100%` : '100%',
+ transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`,
+ }}>
+ {this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc) ? null : this.urlContent}
</div>
);
}
@@ -868,7 +876,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@computed get annotationLayer() {
TraceMobx();
return (
- <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ <div
+ className="webBox-annotationLayer"
+ style={{
+ transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`,
+ height: Doc.NativeHeight(this.Document) || undefined,
+ }}
+ ref={this._annotationLayer}>
{this.inlineTextAnnotations
.sort((a, b) => NumCast(a.y) - NumCast(b.y))
.map(anno => (
@@ -878,51 +892,58 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
@computed get webpage() {
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);
- const renderAnnotations = (docFilters?: () => string[]) => (
+ const renderAnnotations = (docFilters: () => string[]) => (
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={this.setInnerContent}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ originTopLeft={false}
+ isAnnotationOverlayScrollable={true}
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.annotationKey}
- CollectionView={undefined}
setPreviewCursor={this.setPreviewCursor}
setBrushViewer={this.setBrushViewer}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.scrollXf}
NativeDimScaling={returnOne}
- dropAction={'alias'}
+ focus={this.focus}
+ dropAction="embed"
docFilters={docFilters}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={this.addDocument}
+ addDocument={this.addDocumentWrapper}
childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
pointerEvents={this.annotationPointerEvents}
/>
);
return (
<div
- className={'webBox-outerContent'}
+ className="webBox-outerContent"
ref={this._outerRef}
style={{
height: `${100 / scale}%`,
pointerEvents,
}}
- onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe
+ // when active, block wheel events from propagating since they're handled by the iframe
+ onWheel={this.onZoomWheel}
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}>
- <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}>
+ <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}>
{this.content}
{<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>}
{renderAnnotations(this.opaqueFilter)}
@@ -969,15 +990,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth;
+ panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (doc instanceof Doc && property === StyleProp.PointerEvents) {
- if (doc.textInlineAnnotations) return 'none';
+ if (this.inlineTextAnnotations.includes(doc)) return 'none';
}
return this.props.styleProvider?.(doc, props, property);
};
@@ -1016,7 +1037,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
docView={this.props.docViewPath().lastElement()}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotationsCreator}
- selectionText={returnEmptyString}
+ selectionText={this.selectionText}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
/>
@@ -1037,7 +1058,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ref={this._sidebarRef}
{...this.props}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- fieldKey={this.fieldKey + '-' + this._urlHash}
+ fieldKey={this.fieldKey + '_' + this._urlHash}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index 20554b858..eb8064780 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -42,6 +42,8 @@ var ForeignHtmlRenderer = function (styleSheets) {
url = CorsProxy(new URL(webUrl).origin + inurl);
} else if (!inurl.startsWith('http') && !inurl.startsWith('//')) {
url = CorsProxy(webUrl + '/' + inurl);
+ } else if (inurl.startsWith('https')) {
+ url = CorsProxy(inurl);
}
xhr.open('GET', url);
xhr.responseType = 'blob';
@@ -124,7 +126,7 @@ var ForeignHtmlRenderer = function (styleSheets) {
if (url === null) {
break;
}
- searchStartIndex = url.foundAtIndex + url.value.length;
+ searchStartIndex = url.foundAtIndex + url.value.length + 1;
if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue;
const unquoted = removeQuotes(url.value);
if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') {
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
deleted file mode 100644
index b4a382faf..000000000
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { Colors } from "../../global/globalEnums";
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function changeView(view: string) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent";
- selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
-}); \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index a1ca777b3..f3b43501b 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -43,10 +43,6 @@
cursor: pointer;
flex-direction: column;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
svg {
width: 50% !important;
height: 50%;
@@ -68,10 +64,6 @@
justify-content: center;
align-items: center;
justify-items: center;
-
- &:hover {
- filter: brightness(0.85) !important;
- }
}
&.tglBtn,
@@ -166,7 +158,7 @@
width: 100%;
border-radius: 100%;
flex-direction: column;
- margin-top: -4px;
+ // margin-top: -4px;
svg {
width: 60% !important;
@@ -220,10 +212,6 @@
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
}
&.colorBtnLabel {
@@ -248,10 +236,6 @@
align-content: center;
align-items: center;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
.menuButton-dropdownList {
position: absolute;
width: 150px;
@@ -283,10 +267,6 @@
cursor: pointer;
background: transparent;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
&.slider {
color: $white;
cursor: pointer;
@@ -447,11 +427,11 @@
}
.dropbox-background {
- width: 100vw;
- height: 100vh;
- top: 0;
+ width: 200vw;
+ height: 200vh;
+ top: -100vh;
z-index: 20;
- left: 0;
+ left: -100vw;
background: transparent;
position: fixed;
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 1de29f806..fb29f95f4 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -11,12 +11,12 @@ import { ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { WebField } from '../../../../fields/URLField';
import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, StopEvent, Utils } from '../../../../Utils';
+import { aggregateBounds, Utils } from '../../../../Utils';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { LinkManager } from '../../../util/LinkManager';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { ContextMenu } from '../../ContextMenu';
import { DocComponent } from '../../DocComponent';
@@ -27,6 +27,7 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc
import { InkTranscription } from '../../InkTranscription';
import { StyleProp } from '../../StyleProvider';
import { FieldView, FieldViewProps } from '.././FieldView';
+import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
import { OpenWhere } from '../DocumentView';
import { RichTextMenu } from '../formattedText/RichTextMenu';
import { WebBox } from '../WebBox';
@@ -43,16 +44,12 @@ export enum ButtonType {
ToggleButton = 'tglBtn',
ColorButton = 'colorBtn',
ToolButton = 'toolBtn',
- NumberButton = 'numBtn',
+ NumberSliderButton = 'numSliderBtn',
+ NumberDropdownButton = 'numDropdownBtn',
+ NumberInlineButton = 'numInlineBtn',
EditableText = 'editableText',
}
-export enum NumButtonType {
- Slider = 'slider',
- DropdownOptions = 'list',
- Inline = 'inline',
-}
-
export interface ButtonProps extends FieldViewProps {
type?: ButtonType;
}
@@ -61,6 +58,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(FontIconBox, fieldKey);
}
+ @observable noTooltip = false;
showTemplate = (): void => {
const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
@@ -90,7 +88,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
static GetRecognizeGestures() {
return BoolCast(Doc.UserDoc()._recognizeGestures);
}
- static SetRecognizeGesturs(show: boolean) {
+ static SetRecognizeGestures(show: boolean) {
Doc.UserDoc()._recognizeGestures = show;
}
@@ -99,7 +97,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
}
Icon = (color: string) => {
- const icon = StrCast(this.dataDoc.icon, 'user') as any;
+ const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any;
const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
};
@@ -130,100 +128,116 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
/**
* Number button
*/
- @computed get numberButton() {
- const numBtnType: string = StrCast(this.rootDoc.numBtnType);
- const numScript = ScriptCast(this.rootDoc.script);
- const setValue = (value: number) => numScript?.script.run({ value, _readOnly_: false });
-
+ @computed get numberSliderButton() {
+ const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
// Script for checking the outcome of the toggle
- const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
-
+ const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
- if (numBtnType === NumButtonType.Slider) {
- const dropdown = (
- <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
- <input
- type="range"
- step="1"
- min={NumCast(this.rootDoc.numBtnMin, 0)}
- max={NumCast(this.rootDoc.numBtnMax, 100)}
- value={checkResult}
- className={'menu-slider'}
- id="slider"
- onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
- onPointerUp={() => this._batch?.end()}
- onChange={e => {
- e.stopPropagation();
- setValue(Number(e.target.value));
- }}
- />
- </div>
- );
+ const dropdown = (
+ <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
+ <input
+ className="menu-slider"
+ type="range"
+ step="1"
+ min={NumCast(this.rootDoc.numBtnMin, 0)}
+ max={NumCast(this.rootDoc.numBtnMax, 100)}
+ //readOnly={true}
+ value={checkResult}
+ onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))}
+ onPointerUp={() => this._batch?.end()}
+ onChange={undoable(e => {
+ e.stopPropagation();
+ numScript(Number(e.target.value));
+ }, 'set num value')}
+ />
+ </div>
+ );
+ return (
+ <div
+ className="menuButton numBtn slider"
+ onPointerDown={e => e.stopPropagation()}
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ this.noTooltip = this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
+ {checkResult}
+ {label}
+ {this.rootDoc.dropDownOpen ? dropdown : null}
+ </div>
+ );
+ }
+ /**
+ * Number button
+ */
+ @computed get numberDropdownButton() {
+ const numScript = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
+
+ const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
+
+ const items: number[] = [];
+ for (let i = 0; i < 100; i += 2) items.push(i);
+
+ const list = items.map(value => {
return (
- <div className={`menuButton ${this.type} ${numBtnType}`} onPointerDown={e => e.stopPropagation()} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
- {checkResult}
- {label}
- {this.rootDoc.dropDownOpen ? dropdown : null}
+ <div
+ className="list-item"
+ key={`${value}`}
+ style={{
+ backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
+ }}
+ onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}>
+ {value}
</div>
);
- } else if (numBtnType === NumButtonType.DropdownOptions) {
- const items: number[] = [];
- for (let i = 0; i < 100; i++) {
- if (i % 2 === 0) {
- items.push(i);
- }
- }
- const list = items.map(value => {
- return (
- <div
- className="list-item"
- key={`${value}`}
- style={{
- backgroundColor: value === checkResult ? Colors.LIGHT_BLUE : undefined,
- }}
- onClick={() => setValue(value)}>
- {value}
- </div>
- );
- });
- return (
- <div className={`menuButton ${this.type} ${numBtnType}`}>
- <div className={`button`} onClick={action(e => setValue(Number(checkResult) - 1))}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} />
- </div>
- <div
- className={`button ${'number'}`}
- onPointerDown={e => {
- e.stopPropagation();
- e.preventDefault();
- }}
- onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
- <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} />
- </div>
- <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
- </div>
+ });
+ return (
+ <div className="menuButton numBtn list">
+ <div className="button" onClick={undoable(e => numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="minus" />
+ </div>
+ <div
+ className={`button ${'number'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ this.noTooltip = this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
+ <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} readOnly={true} onChange={undoable(e => numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} />
+ </div>
+ <div className={`button`} onClick={undoable(e => numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
+ </div>
- {this.rootDoc.dropDownOpen ? (
- <div>
- <div className="menuButton-dropdownList" style={{ left: '25%' }}>
- {list}
- </div>
- <div
- className="dropbox-background"
- onClick={e => {
- e.stopPropagation();
- this.rootDoc.dropDownOpen = false;
- }}
- />
+ {this.rootDoc.dropDownOpen ? (
+ <div>
+ <div className="menuButton-dropdownList" style={{ left: '25%' }}>
+ {list}
</div>
- ) : null}
- </div>
- );
- } else {
- return <div />;
- }
+ <div
+ className="dropbox-background"
+ onClick={action(e => {
+ e.stopPropagation();
+ this.rootDoc.dropDownOpen = false;
+ this.noTooltip = false;
+ Doc.UnBrushAllDocs();
+ })}
+ />
+ </div>
+ ) : null}
+ </div>
+ );
+ }
+ /**
+ * Number button
+ */
+ @computed get numberInlineButton() {
+ return <div />;
}
/**
@@ -237,7 +251,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ onClick={action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ this.noTooltip = this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })}>
{this.Icon(color)}
{!this.label || !FontIconBox.GetShowLabels() ? null : (
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
@@ -272,7 +290,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const selected = SelectionManager.Docs().lastElement();
if (selected) {
if (StrCast(selected.type) === DocumentType.COL) {
- text = StrCast(selected._viewType);
+ text = StrCast(selected._type_collection);
} else {
dropdown = false;
text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type);
@@ -284,27 +302,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
text = 'User Default';
}
noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
- } else if (script?.script.originalScript.startsWith('setFont')) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
- }
+ } else text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
} catch (e) {
console.log(e);
}
// Get items to place into the list
const list = this.buttonList
- .filter(value => !Doc.noviceMode || noviceList.includes(value))
+ .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
.map(value => (
<div
className="list-item"
- key={`${value}`}
+ key={value}
style={{
- fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined,
+ fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
}}
- onClick={undoBatch(() => script.script.run({ value }))}>
+ onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}>
{value[0].toUpperCase() + value.slice(1)}
</div>
));
@@ -320,7 +334,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }}
- onClick={dropdown ? () => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen) : undefined}>
+ onClick={
+ dropdown
+ ? action(() => {
+ this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
+ this.noTooltip = this.rootDoc.dropDownOpen;
+ Doc.UnBrushAllDocs();
+ })
+ : undefined
+ }>
{dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />}
<div className="menuButton-dropdown-header">{text && text[0].toUpperCase() + text.slice(1)}</div>
{label}
@@ -336,10 +358,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>
<div
className="dropbox-background"
- onClick={e => {
+ onClick={action(e => {
e.stopPropagation();
this.rootDoc.dropDownOpen = false;
- }}
+ this.noTooltip = false;
+ Doc.UnBrushAllDocs();
+ })}
/>
</div>
) : null}
@@ -357,7 +381,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
ev.preventDefault();
ev.stopPropagation();
const s = this.colorScript;
- s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)();
+ s && undoBatch(() => s.script.run({ self: this.rootDoc, value: Utils.colorString(value), _readOnly_: false }).result)();
};
const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent'];
return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />;
@@ -368,7 +392,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
@computed get colorButton() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? 'transparent';
+ const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
const label =
!this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -377,19 +401,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>
);
- // dropdown caret seems superfluous since clicking the color button does the same thing
- // const dropdownCaret = <div
- // className="menuButton-dropDown"
- // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
- // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
- // </div>;
- //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
return (
<div
className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(e => {
this.colorPickerClosed = !this.colorPickerClosed;
+ this.noTooltip = !this.colorPickerClosed;
+ setTimeout(() => Doc.UnBrushAllDocs());
e.stopPropagation();
})}
onPointerDown={e => e.stopPropagation()}>
@@ -408,6 +427,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
e.preventDefault();
e.stopPropagation();
this.colorPickerClosed = true;
+ this.noTooltip = false;
+ Doc.UnBrushAllDocs();
})}
/>
</div>
@@ -484,7 +505,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div className="menuButton editableText">
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} />
<div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}>
- <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} contents={checkResult} />
+ <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} />
</div>
</div>
);
@@ -504,10 +525,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// prettier-ignore
switch (this.type) {
- case ButtonType.DropdownList: return this.dropdownListButton;
- case ButtonType.ColorButton: return this.colorButton;
- case ButtonType.NumberButton: return this.numberButton;
case ButtonType.EditableText: return this.editableText;
+ case ButtonType.DropdownList: button = this.dropdownListButton; break;
+ case ButtonType.ColorButton: button = this.colorButton; break;
+ case ButtonType.NumberDropdownButton: button = this.numberDropdownButton; break;
+ case ButtonType.NumberInlineButton: button = this.numberInlineButton; break;
+ case ButtonType.NumberSliderButton: button = this.numberSliderButton; break;
case ButtonType.DropdownButton: button = this.dropdownButton; break;
case ButtonType.ToggleButton: button = this.toggleButton; break;
case ButtonType.TextButton:
@@ -540,23 +563,49 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
}
- return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
+ return !this.layoutDoc.toolTip || this.noTooltip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
}
}
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setView(view: string) {
const selected = SelectionManager.Docs().lastElement();
- selected ? (selected._viewType = view) : console.log('[FontIconBox.tsx] changeView failed');
+ selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
});
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
- const selected = SelectionManager.Views().lastElement()?.props.Document ?? LinkManager.currentLink;
- if (checkResult) {
- return selected?._backgroundColor ?? 'transparent';
+ const selectedViews = SelectionManager.Views();
+ if (Doc.ActiveTool !== InkTool.None) {
+ if (checkResult) {
+ return ActiveFillColor();
+ }
+ SetActiveFillColor(color ?? 'transparent');
+ } else if (selectedViews.length) {
+ if (checkResult) {
+ const selView = selectedViews.lastElement();
+ const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+ const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+ const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+ return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent';
+ }
+ selectedViews.forEach(dv => {
+ const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+ const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+ const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+ if (contentFrameNumber !== undefined) {
+ CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
+ } else {
+ dv.rootDoc['_' + fieldKey] = color;
+ }
+ });
+ } else {
+ const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : [];
+ if (checkResult) {
+ return selected.lastElement()?._backgroundColor ?? 'transparent';
+ }
+ selected.forEach(doc => (doc._backgroundColor = color));
}
- if (selected) selected._backgroundColor = color;
});
// toggle: Set overlay status of selected document
@@ -566,7 +615,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
}
Doc.SharingDoc().userColor = undefined;
Doc.GetProto(Doc.SharingDoc()).userColor = color;
- Doc.UserDoc().showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().showTitle, 'creationDate');
+ Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date');
});
// toggle: Set overlay status of selected document
@@ -579,127 +628,107 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
});
-/** TEXT
- * setFont
- * setFontSize
- * toggleBold
- * toggleUnderline
- * toggleItalic
- * setAlignment
- * toggleBold
- * toggleItalic
- * toggleUnderline
- **/
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
- if (checkResult) return RichTextMenu.Instance?.fontFamily;
- font && RichTextMenu.Instance.setFontFamily(font);
-});
-
-ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') {
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
+ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
// prettier-ignore
- switch (info) {
- case 'family': return style?.activeFamilies[0];
- case 'size': return style?.activeSizes[0];
- case 'color': return style?.activeColors[0];
- case 'highlight': return style?.activeHighlights[0];
- }
-});
+ const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+ ['grid', {
+ checkResult: (doc:Doc) => doc._freeform_backgroundGrid,
+ setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
+ }],
+ ['snaplines', {
+ checkResult: (doc:Doc) => doc._freeform_snapLines,
+ setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
+ }],
+ ['viewAll', {
+ checkResult: (doc:Doc) => doc._freeform_fitContentsToBox,
+ setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
+ }],
+ ['clusters', {
+ waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
+ checkResult: (doc:Doc) => doc._freeform_useClusters,
+ setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters,
+ }],
+ ['arrange', {
+ waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
+ checkResult: (doc:Doc) => doc._autoArrange,
+ setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
+ }],
+ ['flashcards', {
+ checkResult: (doc:Doc) => Doc.UserDoc().defaultToFlashcards,
+ setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ }],
+ ]);
-ScriptingGlobals.add(function setAlignment(align: 'left' | 'right' | 'center', checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent';
+ return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
}
- if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align);
- else Doc.UserDoc().textAlign = align;
+ const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
+ SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
+ setTimeout(() => batch.end(), 100);
});
-
-ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
- if (active === mapStyle) return Colors.MEDIUM_BLUE;
- return 'transparent';
- }
- editorView?.state && RichTextMenu.Instance.changeListType(mapStyle);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) {
- if (checkResult) return RichTextMenu.Instance.fontColor;
- color && RichTextMenu.Instance.setColor(color);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
+ // prettier-ignore
+ const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void;}> = new Map([
+ ['font', {
+ checkResult: () => RichTextMenu.Instance?.fontFamily,
+ setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
+ }],
+ ['highlight', {
+ checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
+ setDoc: () => value && RichTextMenu.Instance.setHighlight(value),
+ }],
+ ['fontColor', {
+ checkResult: () => RichTextMenu.Instance?.fontColor,
+ setDoc: () => value && RichTextMenu.Instance.setColor(value),
+ }],
+ ['fontSize', {
+ checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
+ setDoc: () => {
+ if (typeof value === 'number') value = value.toString();
+ if (value && Number(value).toString() === value) value += 'px';
+ RichTextMenu.Instance.setFontSize(value);
+ },
+ }],
+ ]);
if (checkResult) {
- return (selected ?? Doc.UserDoc())._fontHighlight;
- }
- if (selected) {
- selected._fontColor = color;
- if (color) {
- editorView?.state && RichTextMenu.Instance.setHighlight(color, editorView, editorView?.dispatch);
- }
+ return map.get(attr)?.checkResult();
}
- Doc.UserDoc()._fontHighlight = color;
+ map.get(attr)?.setDoc?.();
});
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
- if (checkResult) {
- return RichTextMenu.Instance?.fontSize.replace('px', '');
- }
- if (typeof size === 'number') size = size.toString();
- if (size && Number(size).toString() === size) size += 'px';
- RichTextMenu.Instance.setFontSize(size);
-});
-ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
-});
-ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) {
+type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
+type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
+ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
const textView = RichTextMenu.Instance?.TextView;
- if (checkResult) {
- return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (textView) runInAction(() => (textView._recording = !textView._recording));
-});
-
-ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === 'bold') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleBold();
- else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold';
-});
-
-ScriptingGlobals.add(function toggleUnderline(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === 'underline') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleUnderline();
- else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline';
-});
-
-ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === 'italics') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleItalics();
- else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics';
+ const editorView = textView?.EditorView;
+ // prettier-ignore
+ const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) =>
+ [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false),
+ toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]);
+ // prettier-ignore
+ const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
+ [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false),
+ toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]);
+ // prettier-ignore
+ const attrs:attrfuncs[] = [
+ ['dictation', { checkResult: () => textView?._recording ? true:false,
+ toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }],
+ ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
+ toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
+ ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}],
+ ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}],
+ ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]]
+
+ const map = new Map(attrs.concat(alignments).concat(listings));
+ if (checkResult) return map.get(charStyle)?.checkResult() ? Colors.MEDIUM_BLUE : 'transparent';
+ map.get(charStyle)?.toggle();
});
export function checkInksToGroup() {
@@ -795,7 +824,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
}
if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
- if (GestureOverlay.Instance.InkShape === tool) {
+ if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
Doc.ActiveTool = InkTool.None;
GestureOverlay.Instance.InkShape = undefined;
} else {
@@ -804,7 +833,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
}
} else if (tool) {
// pen or eraser
- if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+ if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
Doc.ActiveTool = InkTool.None;
} else {
Doc.ActiveTool = tool as any;
@@ -819,62 +848,39 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- if (checkResult) {
- if (selected?.type === DocumentType.INK) {
- return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent';
- }
- return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent';
- }
- SetActiveIsInkMask(!ActiveIsInkMask());
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.isInkMask = !doc.isInkMask));
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
- if (checkResult) {
- if (selected?.type === DocumentType.INK) {
- return StrCast(selected.fillColor);
- }
- return ActiveFillColor();
- }
- SetActiveFillColor(StrCast(color));
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.fillColor = color));
-});
-
-ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) {
- if (checkResult) {
- const selected = SelectionManager.Docs().lastElement();
- if (selected?.type === DocumentType.INK) {
- return NumCast(selected.strokeWidth);
- }
- return ActiveInkWidth();
- }
- SetActiveInkWidth(width.toString());
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.strokeWidth = Number(width)));
-});
+ // prettier-ignore
+ const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+ ['inkMask', {
+ checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'),
+ setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask),
+ setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+ }],
+ ['fillColor', {
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"),
+ setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
+ setMode: () => SetActiveFillColor(StrCast(value)),
+ }],
+ [ 'strokeWidth', {
+ checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()),
+ setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)),
+ setMode: () => SetActiveInkWidth(value.toString()),
+ }],
+ ['strokeColor', {
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
+ setInk: (doc: Doc) => (doc.color = String(value)),
+ setMode: () => SetActiveInkColor(StrCast(value)),
+ }],
+ ]);
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boolean) {
if (checkResult) {
- const selected = SelectionManager.Docs().lastElement();
- if (selected?.type === DocumentType.INK) {
- return StrCast(selected.color);
- }
- return ActiveInkColor();
+ return map.get(option)?.checkResult();
}
- SetActiveInkColor(StrCast(color));
+ map.get(option)?.setMode();
SelectionManager.Docs()
.filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.color = String(color)));
+ .map(doc => map.get(option)?.setInk(doc));
});
/** WEB
@@ -886,7 +892,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
if (checkResult) {
return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href);
}
- (selected.ComponentView as WebBox).submitURL(url);
+ selected.ComponentView?.setData?.(url);
//selected.rootDoc.data = new WebField(url);
}
});
@@ -911,17 +917,26 @@ ScriptingGlobals.add(function webBack(checkResult?: boolean) {
ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
if (checkResult && selected) {
- const result: boolean = NumCast(selected.schemaPreviewWidth) > 0;
+ const result: boolean = NumCast(selected.schema_previewWidth) > 0;
if (result) return Colors.MEDIUM_BLUE;
else return 'transparent';
} else if (selected) {
- if (NumCast(selected.schemaPreviewWidth) > 0) {
- selected.schemaPreviewWidth = 0;
+ if (NumCast(selected.schema_previewWidth) > 0) {
+ selected.schema_previewWidth = 0;
} else {
- selected.schemaPreviewWidth = 200;
+ selected.schema_previewWidth = 200;
}
}
});
+ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ if (checkResult && selected) {
+ return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent';
+ }
+ if (selected) {
+ selected._schema_singleLine = !selected._schema_singleLine;
+ }
+});
/** STACK
* groupBy
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
index 7f414ddbb..74c3c563c 100644
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
@@ -12,7 +12,7 @@ export class ColorDropdown extends Component<IButtonProps> {
const active: string = StrCast(this.props.rootDoc.dropDownOpen);
const script: string = StrCast(this.props.rootDoc.script);
- const scriptCheck: string = script + "(undefined, true)";
+ const scriptCheck: string = script + '(undefined, true)';
const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
const stroke: boolean = false;
@@ -24,24 +24,21 @@ export class ColorDropdown extends Component<IButtonProps> {
// strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />);
// }
- const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505',
- '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B',
- '#FFFFFF', '#f1efeb'];
+ const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb'];
- const colorBox = (func: (color: ColorState) => void) => <SketchPicker
- disableAlpha={!stroke}
- onChange={func} color={boolResult ? boolResult : "#FFFFFF"}
- presetColors={colorOptions} />;
- const label = !this.props.label || !FontIconBox.GetShowLabels() ? (null) :
- <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: "absolute" }}>
- {this.props.label}
- </div>;
+ const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />;
+ const label =
+ !this.props.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}>
+ {this.props.label}
+ </div>
+ );
- const dropdownCaret = <div
- className="menuButton-dropDown"
- style={{ borderBottomRightRadius: active ? 0 : undefined }}>
- <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" />
- </div>;
+ const dropdownCaret = (
+ <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}>
+ <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" />
+ </div>
+ );
const click = (value: ColorState) => {
const hex: string = value.hex;
@@ -51,26 +48,30 @@ export class ColorDropdown extends Component<IButtonProps> {
}
};
return (
- <div className={`menuButton ${this.props.type} ${active}`}
+ <div
+ className={`menuButton ${this.props.type} ${active}`}
style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }}
- onClick={() => this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen}
+ onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)}
onPointerDown={e => e.stopPropagation()}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} />
- <div className="colorButton-color"
- style={{ backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />
+ <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} />
{label}
{/* {dropdownCaret} */}
- {this.props.rootDoc.dropDownOpen ?
+ {this.props.rootDoc.dropDownOpen ? (
<div>
- <div className="menuButton-dropdownBox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}>
+ <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
{colorBox(click)}
</div>
- <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.props.rootDoc.dropDownOpen = false; }} />
+ <div
+ className="dropbox-background"
+ onClick={e => {
+ e.stopPropagation();
+ this.props.rootDoc.dropDownOpen = false;
+ }}
+ />
</div>
- : null}
+ ) : null}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index fcd6e0c55..aa269d8d6 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -32,7 +32,7 @@ export class DashDocCommentView {
};
this.root = ReactDOM.createRoot(this.dom);
- this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
+ this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
(this as any).dom = this.dom;
}
@@ -48,7 +48,7 @@ export class DashDocCommentView {
}
interface IDashDocCommentViewInternal {
- docid: string;
+ docId: string;
view: any;
getPos: any;
}
@@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
}
onPointerLeaveCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
e.preventDefault();
e.stopPropagation();
}
onPointerEnterCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
e.preventDefault();
e.stopPropagation();
}
@@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
setTimeout(() => {
- expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try {
this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
} catch (e) {}
@@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const state = this.props.view.state;
for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) {
const m = state.doc.nodeAt(i);
- if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) {
+ if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) {
return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean };
}
}
- const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' });
+ const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' });
this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc));
setTimeout(() => {
try {
@@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
return (
<span
className="formattedTextBox-inlineComment"
- id={'DashDocCommentView-' + this.props.docid}
+ id={'DashDocCommentView-' + this.props.docId}
onPointerLeave={this.onPointerLeaveCollapsed}
onPointerEnter={this.onPointerEnterCollapsed}
onPointerUp={this.onPointerUpCollapsed}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 722d0cc4f..c929b7ff3 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -9,7 +9,7 @@ import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { ColorScheme } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
-import { DocumentView } from '../DocumentView';
+import { DocFocusOptions, DocumentView } from '../DocumentView';
import { FormattedTextBox } from './FormattedTextBox';
import React = require('react');
@@ -41,19 +41,33 @@ export class DashDocView {
this.root = ReactDOM.createRoot(this.dom);
this.root.render(
- <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
+ <DashDocViewInternal
+ docId={node.attrs.docId}
+ embedding={node.attrs.embedding}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ hidden={node.attrs.hidden}
+ fieldKey={node.attrs.fieldKey}
+ tbox={tbox}
+ view={view}
+ node={node}
+ getPos={getPos}
+ />
);
}
destroy() {
- this.root.unmount();
- // ReactDOM.unmountComponentAtNode(this.dom);
+ setTimeout(() => {
+ try {
+ this.root.unmount();
+ } catch {}
+ });
}
selectNode() {}
}
interface IDashDocViewInternal {
- docid: string;
- alias: string;
+ docId: string;
+ embedding: string;
tbox: FormattedTextBox;
width: string;
height: string;
@@ -70,25 +84,18 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
_textBox: FormattedTextBox;
@observable _dashDoc: Doc | undefined;
@observable _finalLayout: any;
- @observable _resolvedDataDoc: any;
@observable _width: number = 0;
@observable _height: number = 0;
updateDoc = action((dashDoc: Doc) => {
this._dashDoc = dashDoc;
- this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+ this._finalLayout = dashDoc;
- if (this._finalLayout) {
- if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
- this._finalLayout.rootDocument = dashDoc.aliasOf;
- }
- this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null);
- }
if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') {
try {
this._width = NumCast(this._dashDoc?._width);
this._height = NumCast(this._dashDoc?._height);
- // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ // bcz: an exception will be thrown if two embeddings are open at the same time when a doc view comment is made
this.props.view.dispatch(
this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
...this.props.node.attrs,
@@ -106,15 +113,15 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
super(props);
this._textBox = this.props.tbox;
- DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
+ DocServer.GetRefField(this.props.docId + this.props.embedding).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
- this.props.alias &&
- DocServer.GetRefField(this.props.docid).then(async dashDocBase => {
+ this.props.embedding &&
+ DocServer.GetRefField(this.props.docId).then(async dashDocBase => {
if (dashDocBase instanceof Doc) {
- const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
- aliasedDoc.layoutKey = 'layout';
- this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
- this.updateDoc(aliasedDoc);
+ const embedding = Doc.MakeEmbedding(dashDocBase, this.props.docId + this.props.embedding);
+ embedding.layout_fieldKey = 'layout';
+ this.props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
+ this.updateDoc(embedding);
}
});
} else {
@@ -150,7 +157,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current);
return new Transform(-translateX, -translateY, 1).scale(1 / scale);
};
- outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document, {}); // ideally, this would scroll to show the focus target
+ outerFocus = (target: Doc, options: DocFocusOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target
onKeyDown = (e: any) => {
e.stopPropagation();
@@ -160,12 +167,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
};
onPointerLeave = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = '');
};
onPointerEnter = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = 'orange');
};
@@ -181,6 +188,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
height: this._height,
position: 'absolute',
display: 'inline-block',
+ left: 0,
+ top: 0,
}}
onPointerLeave={this.onPointerLeave}
onPointerEnter={this.onPointerEnter}
@@ -190,12 +199,11 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
onWheel={e => e.preventDefault()}>
<DocumentView
Document={this._finalLayout}
- DataDoc={this._resolvedDataDoc}
addDocument={returnFalse}
- rootSelected={this._textBox.props.isSelected}
+ rootSelected={returnFalse} //{this._textBox.props.isSelected}
removeDocument={this.removeDoc}
isDocumentActive={returnFalse}
- isContentActive={this._textBox.props.isContentActive}
+ isContentActive={emptyFunction}
styleProvider={this._textBox.props.styleProvider}
docViewPath={this._textBox.props.docViewPath}
ScreenToLocalTransform={this.getDocTransform}
@@ -211,8 +219,6 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
docFilters={this.props.tbox?.props.docFilters}
docRangeFilters={this.props.tbox?.props.docRangeFilters}
searchFilterDocs={this.props.tbox?.props.searchFilterDocs}
- ContainingCollectionView={this._textBox.props.ContainingCollectionView}
- ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
/>
</div>
);
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 39005a18b..2642bc144 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,29 +1,33 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom/client';
-import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc';
+import { DataSym, Doc, Field } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { ComputedField } from '../../../../fields/ScriptField';
import { Cast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
+import { OpenWhere } from '../DocumentView';
import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
import React = require('react');
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils';
-import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
-import { Tooltip } from '@material-ui/core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
-import { NodeSelection } from 'prosemirror-state';
-import { OpenWhere } from '../DocumentView';
export class DashFieldView {
dom: HTMLDivElement; // container for label and value
root: any;
+ node: any;
+ tbox: FormattedTextBox;
+ unclickable = () => !this.tbox.props.isSelected() && 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) {
+ this.node = node;
+ this.tbox = tbox;
this.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
@@ -44,23 +48,38 @@ export class DashFieldView {
this.root = ReactDOM.createRoot(this.dom);
this.root.render(
- <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />
+ <DashFieldViewInternal
+ node={node}
+ unclickable={this.unclickable}
+ getPos={getPos}
+ fieldKey={node.attrs.fieldKey}
+ docId={node.attrs.docId}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ hideKey={node.attrs.hideKey}
+ editable={node.attrs.editable}
+ tbox={tbox}
+ />
);
}
destroy() {
- this.root.unmount();
+ setTimeout(() => {
+ try {
+ this.root.unmount();
+ } catch {}
+ });
}
- deselectNode() {
+ @action deselectNode() {
this.dom.classList.remove('ProseMirror-selectednode');
}
- selectNode() {
+ @action selectNode() {
this.dom.classList.add('ProseMirror-selectednode');
}
}
interface IDashFieldViewInternal {
fieldKey: string;
- docid: string;
+ docId: string;
hideKey: boolean;
tbox: FormattedTextBox;
width: number;
@@ -68,6 +87,7 @@ interface IDashFieldViewInternal {
editable: boolean;
node: any;
getPos: any;
+ unclickable: () => boolean;
}
@observer
@@ -77,14 +97,15 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
_fieldKey: string;
_fieldStringRef = React.createRef<HTMLSpanElement>();
@observable _dashDoc: Doc | undefined;
+ @observable _expanded = false;
constructor(props: IDashFieldViewInternal) {
super(props);
this._fieldKey = this.props.fieldKey;
this._textBoxDoc = this.props.tbox.props.Document;
- if (this.props.docid) {
- DocServer.GetRefField(this.props.docid).then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ if (this.props.docId) {
+ DocServer.GetRefField(this.props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
} else {
this._dashDoc = this.props.tbox.rootDoc;
}
@@ -93,142 +114,46 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
this._reactionDisposer?.();
}
- public static multiValueDelimeter = ';';
- public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) {
- const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? (fieldKey === 'PARAMS' ? textBoxDoc[fieldKey] : '');
- const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal;
- return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' };
- }
-
// set the display of the field's value (checkbox for booleans, span of text for strings)
@computed get fieldValueContent() {
- if (this._dashDoc) {
- const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey);
- // field value is a boolean, so use a checkbox or similar widget to display it
- if (boolVal === true || boolVal === false) {
- return (
- <input
- className="dashFieldView-fieldCheck"
- type="checkbox"
- checked={boolVal}
- onChange={e => {
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true);
- }}
- />
- );
- } // field value is a string, so display it as an editable span
- else {
- // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't
- // use React events. Essentially, React events occur after native events have been processed, so corresponding React events
- // will never fire because Prosemirror has handled the native events. So we add listeners for native events here.
- return (
- <span
- className="dashFieldView-fieldSpan"
- contentEditable={true}
- style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
- suppressContentEditableWarning={true}
- defaultValue={strVal}
- ref={r => {
- r?.addEventListener('keydown', e => this.fieldSpanKeyDown(e, r));
- r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false));
- r?.addEventListener(
- 'pointerdown',
- action(e => {
- // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
- // while (target && !target.dataset?.targethrefs) target = target.parentElement;
- this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos()))));
- // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
- // e.stopPropagation();
- })
- );
- }}>
- {strVal}
- </span>
- );
- }
- }
+ 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 }}>
+ <SchemaTableCell
+ Document={this._dashDoc}
+ col={0}
+ deselectCell={emptyFunction}
+ selectCell={emptyFunction}
+ maxWidth={this.props.hideKey ? undefined : () => 100}
+ columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero}
+ selectedCell={() => [this._dashDoc!, 0]}
+ fieldKey={this._fieldKey}
+ rowHeight={returnZero}
+ isRowActive={() => this._expanded && this.props.editable}
+ padding={0}
+ getFinfo={emptyFunction}
+ setColumnValues={returnFalse}
+ allowCRs={true}
+ oneLine={!this._expanded}
+ finishEdit={action(() => (this._expanded = false))}
+ />
+ </div>
+ );
}
- // we need to handle all key events on the input span or else they will propagate to prosemirror.
- @action
- fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
- if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
- navigator.clipboard.writeText(window.getSelection()?.toString() || '');
- return;
- }
- if (e.key === 'Enter') {
- // handle the enter key by "submitting" the current text to Dash's database.
- this.updateText(span.textContent!, true);
- e.preventDefault(); // prevent default to avoid a newline from being generated and wiping out this field view
- }
- if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
- // handle ctrl-A to select all the text within the span
- if (window.getSelection) {
- const range = document.createRange();
- range.selectNodeContents(span);
- window.getSelection()!.removeAllRanges();
- window.getSelection()!.addRange(range);
- }
- e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
- }
- if (!this.props.editable) {
- e.preventDefault();
- }
- e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
- };
-
- @action
- updateText = (nodeText: string, forceMatch: boolean) => {
- if (nodeText) {
- const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText;
- // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
- // holds the different enumerated values for the field in the titles of its collected documents.
- // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
- DocServer.GetRefField(this._fieldKey).then(options => {
- let modText = '';
- options instanceof Doc && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
- if (modText) {
- // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true);
- } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
- else if (nodeText.startsWith(':=')) {
- this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
- } else if (nodeText.startsWith('=:=')) {
- Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
- } else {
- if (Number(newText).toString() === newText) {
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText);
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true);
- } else {
- const splits = newText.split(DashFieldViewInternal.multiValueDelimeter);
- if (this._fieldKey !== 'PARAMS' || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) {
- const strVal = splits.length > 1 ? new List<string>(splits) : newText;
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true);
- }
- }
- }
- });
- }
- };
-
createPivotForField = (e: React.MouseEvent) => {
- let container = this.props.tbox.props.ContainingCollectionView;
- while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
- container = container.props.ContainingCollectionView;
- }
+ let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement();
if (container) {
- const alias = Doc.MakeAlias(container.props.Document);
- alias._viewType = CollectionViewType.Time;
- let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField));
+ const embedding = Doc.MakeEmbedding(container.rootDoc);
+ embedding._type_collection = CollectionViewType.Time;
+ const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders';
+ let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField));
if (!list) {
- alias._columnHeaders = list = new List<SchemaHeaderField>();
+ embedding[colHdrKey] = list = new List<SchemaHeaderField>();
}
list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb'));
list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb'));
- alias._pivotField = this._fieldKey.startsWith('#') ? '#' : this._fieldKey;
- this.props.tbox.props.addDocTab(alias, OpenWhere.addRight);
+ embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey;
+ this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight);
}
};
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 714ae458c..5e62d94c2 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -33,7 +33,6 @@ export class EquationView {
setEditor = (editor?: EquationEditor) => (this._editor = editor);
destroy() {
this.root.unmount();
- // ReactDOM.unmountComponentAtNode(this.dom);
}
setSelection() {
this._editor?.mathField.focus();
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
index 531a60297..cf48e1250 100644
--- a/src/client/views/nodes/formattedText/FootnoteView.tsx
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -83,13 +83,11 @@ export class FootnoteView {
};
toggle = () => {
- console.log('TOGGLE');
if (this.innerView) this.close();
else this.open();
};
close() {
- console.log('CLOSE');
this.innerView?.destroy();
this.innerView = null;
this.dom.textContent = '';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index cbe0a465d..109b62e6f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -24,6 +24,27 @@ audiotag:hover {
transform: scale(2);
transform-origin: bottom center;
}
+.formattedTextBox {
+ touch-action: none;
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $medium-gray;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: inherit;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+ width: 100%;
+ position: relative;
+ top: 0;
+ left: 0;
+}
.formattedTextBox-cont {
touch-action: none;
@@ -51,6 +72,17 @@ audiotag:hover {
position: absolute;
}
}
+.formattedTextBox-alternateButton {
+ align-items: center;
+ flex-direction: column;
+ position: absolute;
+ color: white;
+ background: black;
+ right: 0;
+ bottom: 0;
+ width: 11;
+ height: 11;
+}
.formattedTextBox-outer-selected,
.formattedTextBox-outer {
@@ -149,6 +181,10 @@ audiotag:hover {
}
}
+.gpt-typing-wrapper {
+ padding: 10px;
+}
+
// .menuicon {
// display: inline-block;
// border-right: 1px solid rgba(0, 0, 0, 0.2);
@@ -189,16 +225,15 @@ audiotag:hover {
}
footnote {
- display: inline-block;
+ display: inline-flex;
+ top: -0.5em;
position: relative;
cursor: pointer;
-
- div {
- padding: 0 !important;
- }
+ height: 1em;
+ width: 0.5em;
}
-footnote::after {
+footnote::before {
content: counter(prosemirror-footnote);
vertical-align: super;
font-size: 75%;
@@ -212,15 +247,14 @@ footnote::after {
.footnote-tooltip {
cursor: auto;
font-size: 75%;
- position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ position: relative;
background: silver;
- padding: 3px;
border-radius: 2px;
- max-width: 100px;
- min-width: 50px;
- width: max-content;
+ min-width: 100px;
+ top: 2em;
+ height: max-content;
+ left: -1em;
+ padding: 3px;
}
.prosemirror-attribution {
@@ -235,8 +269,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
@@ -730,8 +763,8 @@ footnote::after {
cursor: auto;
font-size: 75%;
position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ // left: -30px;
+ // top: calc(100% + 10px);
background: silver;
padding: 3px;
border-radius: 2px;
@@ -752,8 +785,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 619c59f0e..fb0c0d2ab 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,7 +1,8 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { isEqual } from 'lodash';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { baseKeymap, selectAll } from 'prosemirror-commands';
import { history } from 'prosemirror-history';
@@ -11,7 +12,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -20,15 +21,18 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils';
import { ComputedField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
+import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { Networking } from '../../../Network';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { MakeTemplate } from '../../../util/DropConverter';
+import { IsFollowLinkScript } from '../../../util/LinkFollower';
import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from '../../../util/SelectionManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -45,9 +49,10 @@ import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
-import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { LinkDocPreview } from '../LinkDocPreview';
+import { PinProps, PresBox } from '../trails';
import { DashDocCommentView } from './DashDocCommentView';
import { DashDocView } from './DashDocView';
import { DashFieldView } from './DashFieldView';
@@ -63,25 +68,28 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
+import { RTFMarkup } from '../../../util/RTFMarkup';
+import { List } from '../../../../fields/List';
const translateGoogleApi = require('translate-google-api');
-
export const GoogleRef = 'googleDocId';
-
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
+export interface FormattedTextBoxProps {
+ allowScroll?: boolean;
+}
@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() {
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
public static LiveTextUndo: UndoManager.Batch | undefined;
- static _globalHighlights: string[] = ['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items'];
+ static _globalHighlightsCache: string = '';
+ static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
static _highlightStyleSheet: any = addStyleSheet();
static _bulletStyleSheet: any = addStyleSheet();
static _userStyleSheet: any = addStyleSheet();
- static _canAnnotate = true;
static _hadSelection: boolean = false;
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
@@ -102,7 +110,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
private _rules: RichTextRules | undefined;
private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle
private _forceDownNode: Node | undefined;
- private _downEvent: any;
private _downX = 0;
private _downY = 0;
private _break = true;
@@ -111,38 +118,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return this._editorView;
}
public get SidebarKey() {
- return this.fieldKey + '-sidebar';
+ return this.fieldKey + '_sidebar';
}
@computed get allSidebarDocs() {
return DocListCast(this.dataDoc[this.SidebarKey]);
}
@computed get noSidebar() {
- return this.props.docViewPath?.()[this.props.docViewPath().length - 2]?.rootDoc.type === DocumentType.RTF || this.props.noSidebar || this.Document._noSidebar;
+ return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_noSidebar;
}
- @computed get sidebarWidthPercent() {
- return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
+ @computed get layout_sidebarWidthPercent() {
+ return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '-backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
- @computed get autoHeight() {
- return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight;
+ @computed get layout_autoHeight() {
+ return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight;
}
@computed get textHeight() {
- return NumCast(this.rootDoc[this.fieldKey + '-height']);
+ return NumCast(this.rootDoc[this.fieldKey + '_height']);
}
@computed get scrollHeight() {
- return NumCast(this.rootDoc[this.fieldKey + '-scrollHeight']);
+ return NumCast(this.rootDoc[this.fieldKey + '_scrollHeight']);
}
@computed get sidebarHeight() {
- return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '-height']);
+ return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '_height']);
}
@computed get titleHeight() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
}
- @computed get autoHeightMargins() {
- return this.titleHeight + NumCast(this.layoutDoc._autoHeightMargins);
+ @computed get layout_autoHeightMargins() {
+ return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins);
}
@computed get _recording() {
return this.dataDoc?.mediaState === 'recording';
@@ -152,7 +159,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
@computed get config() {
this._keymap = buildKeymap(schema, this.props);
- this._rules = new RichTextRules(this.props.Document, this);
+ this._rules = new RichTextRules(this.rootDoc, this);
return {
schema,
plugins: [
@@ -171,6 +178,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
}
+ // State for GPT
+ @observable
+ private gptRes: string = '';
+
public static PasteOnLoad: ClipboardEvent | undefined;
public static SelectOnLoad = '';
public static DontSelectInitialText = false; // whether initial text should be selected or not
@@ -187,13 +198,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return '';
}
public static GetDocFromUrl(url: string) {
- return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid
+ return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
}
constructor(props: any) {
super(props);
FormattedTextBox.Instance = this;
- this.updateHighlights();
this._recordingStart = Date.now();
}
@@ -203,8 +213,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
public RemoveLinkFromDoc(linkDoc?: Doc) {
this.unhighlightSearchTerms();
const state = this._editorView?.state;
- const a1 = linkDoc?.anchor1 as Doc;
- const a2 = linkDoc?.anchor2 as Doc;
+ const a1 = linkDoc?.link_anchor_1 as Doc;
+ const a2 = linkDoc?.link_anchor_2 as Doc;
if (state && a1 && a2 && this._editorView) {
this.removeDocument(a1);
this.removeDocument(a2);
@@ -231,30 +241,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
- getAnchor = (addAsAnnotation: boolean) => this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc;
+ const anchor = Docs.Create.TextConfigDocument({ annotationOn: this.rootDoc });
+ this.addDocument(anchor);
+ this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc);
+ return anchor;
+ };
@action
setupAnchorMenu = () => {
AnchorMenu.Instance.Status = 'marquee';
AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
- !this.layoutDoc.showSidebar && this.toggleSidebar();
+ !this.layoutDoc.layout_showSidebar && this.toggleSidebar();
setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
};
AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
- !this.layoutDoc.showSidebar && this.toggleSidebar();
+ !this.layoutDoc.layout_showSidebar && this.toggleSidebar();
const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true);
setTimeout(() => {
const target = this._sidebarRef.current?.anchorMenuClick(anchor);
if (target) {
anchor.followLinkAudio = true;
DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
- target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
+ target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
}
});
};
AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
- this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
+ this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
return undefined;
});
AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
@@ -284,11 +301,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._editorView.updateState(state);
const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
- const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
- const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box
- const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
- const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
- const json = JSON.stringify(state.toJSON());
+ const newText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+ const newJson = JSON.stringify(state.toJSON());
+ const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
+ const prevLayoutData = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : 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 effectiveAcl = GetEffectiveAcl(dataDoc);
const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"'));
@@ -300,35 +317,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
accumTags.push(node.attrs.fieldKey);
}
});
- const curTags = Object.keys(dataDoc).filter(key => key.startsWith('#'));
- const added = accumTags.filter(tag => !curTags.includes(tag));
- const removed = curTags.filter(tag => !accumTags.includes(tag));
- removed.forEach(r => (dataDoc[r] = undefined));
- added.forEach(a => (dataDoc[a] = a));
+ dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
let unchanged = true;
- if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
+ if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
this._applyingChange = this.fieldKey;
- curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now())));
- if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+ const textChange = newText !== prevData?.Text;
+ textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
+ if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
// 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 (removeSelection(json) !== removeSelection(curLayout?.Data)) {
+ if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) {
const numstring = NumCast(dataDoc[this.fieldKey], null);
- if (numstring !== undefined) {
- dataDoc[this.fieldKey] = Number(curText);
- } else {
- dataDoc[this.fieldKey] = new RichTextField(json, curText);
- }
- dataDoc[this.fieldKey + '-noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
- ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+ dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
+ dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
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((curProto || curTemp).Data)));
- dataDoc[this.fieldKey + '-noTemplate'] = undefined; // mark the data field as not being split from any template it might have
- ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+ 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
+ ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
unchanged = false;
}
this._applyingChange = '';
@@ -356,8 +366,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let linkTime;
let linkAnchor;
let link;
- DocListCast(this.dataDoc.links).forEach((l, i) => {
- const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
+ LinkManager.Links(this.dataDoc).forEach((l, i) => {
+ const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined;
if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') {
linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
linkAnchor = anchor;
@@ -393,7 +403,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
autoLink = () => {
const newAutoLinks = new Set<Doc>();
- const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+ const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords);
if (this._editorView?.state.doc.textContent) {
const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
const f = this._editorView.state.selection.from;
@@ -405,7 +415,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
}
- oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
+ oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
};
updateTitle = () => {
@@ -414,7 +424,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
(title.startsWith('-') || title.startsWith('@')) &&
this._editorView &&
- !this.dataDoc['title-custom'] &&
+ !this.dataDoc.title_custom &&
(Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text')
) {
let node = this._editorView.state.doc;
@@ -446,8 +456,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
alink =
alink ??
- (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+ (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) ||
+ DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!);
newAutoLinks.add(alink);
const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -514,7 +524,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this.setupEditor(this.config, this.fieldKey);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
}
- // if (this.autoHeight) this.tryUpdateScrollHeight();
+ // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
@undoBatch
@@ -533,17 +543,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// embed document when dragg marked as embed
} else if (de.embedKey) {
const target = dragData.droppedDocuments[0];
- target._fitContentsToBox = true;
const node = schema.nodes.dashDoc.create({
width: target[WidthSym](),
height: target[HeightSym](),
title: 'dashDoc',
- docid: target[Id],
+ docId: target[Id],
float: 'unset',
});
- if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
+ if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) {
dragData.removeDocument?.(dragData.draggedDocuments[0]);
}
+ target._freeform_fitContentsToBox = true;
+ target.embedContainer = this.rootDoc;
const view = this._editorView!;
view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
e.stopPropagation();
@@ -596,62 +607,63 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return ret;
}
- updateHighlights = () => {
- const highlights = FormattedTextBox._globalHighlights;
+ updateHighlights = (highlights: string[]) => {
+ if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return;
+ setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join('')));
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
- if (highlights.indexOf('Audio Tags') === -1) {
+ if (!highlights.includes('Audio Tags')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, '');
}
- if (highlights.indexOf('Text from Others') !== -1) {
+ if (highlights.includes('Text from Others')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
}
- if (highlights.indexOf('My Text') !== -1) {
+ if (highlights.includes('My Text')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' });
}
- if (highlights.indexOf('Todo Items') !== -1) {
+ if (highlights.includes('Todo Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
}
- if (highlights.indexOf('Important Items') !== -1) {
+ if (highlights.includes('Important Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' });
}
- if (highlights.indexOf('Bold Text') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror strong > span', { 'font-size': 'large' }, '');
- addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
+ if (highlights.includes('Bold Text')) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, '');
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
}
- if (highlights.indexOf('Disagree Items') !== -1) {
+ if (highlights.includes('Disagree Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' });
}
- if (highlights.indexOf('Ignore Items') !== -1) {
+ if (highlights.includes('Ignore Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
}
- if (highlights.indexOf('By Recent Minute') !== -1) {
+ if (highlights.includes('By Recent Minute')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
const min = Math.round(Date.now() / 1000 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
- setTimeout(this.updateHighlights);
}
- if (highlights.indexOf('By Recent Hour') !== -1) {
+ if (highlights.includes('By Recent Hour')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
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[CssSym] = this.layoutDoc[CssSym] + 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)
};
@observable _showSidebar = false;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
@action
toggleSidebar = (preview: boolean = false) => {
const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', ''));
if (preview) this._showSidebar = true;
- else this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%';
+ else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%';
this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth);
};
sidebarDown = (e: React.PointerEvent) => {
- const batch = UndoManager.StartBatch('sidebar');
+ const batch = UndoManager.StartBatch('toggle sidebar');
setupMoveUpEvents(
this,
e,
@@ -669,22 +681,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100;
+ const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
const width = this.layoutDoc[WidthSym]() + localDelta[0];
- this.layoutDoc._sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
+ this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
this.layoutDoc.width = width;
- this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
};
- @undoBatch
deleteAnnotation = (anchor: Doc) => {
- LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]);
+ const batch = UndoManager.StartBatch('delete link');
+ LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
// const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
// this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
// AnchorMenu.Instance.fadeOut(true);
this.props.select(false);
+ setTimeout(batch.end); // wait for reaction to remove link from document
};
@undoBatch
@@ -717,17 +730,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.split(' ')
.filter(h => h);
const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0];
+ const deleteMarkups = undoBatch(() => {
+ const sel = editor.state.selection;
+ editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor));
+ });
e.persist();
anchorDoc &&
DocServer.GetRefField(anchorDoc).then(
action(anchor => {
+ anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc);
AnchorMenu.Instance.Status = 'annotation';
- AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
+ AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc);
AnchorMenu.Instance.Pinned = false;
- AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
- AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc);
- AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc);
- AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc);
+ AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc);
+ AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc);
+ AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc);
+ AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc);
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
})
);
@@ -741,7 +759,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
description: 'plain',
event: undoBatch(() => {
Doc.setNativeView(this.rootDoc);
- this.layoutDoc.autoHeightMargins = undefined;
+ this.layoutDoc.layout_autoHeightMargins = undefined;
}),
icon: 'eye',
});
@@ -749,18 +767,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
description: 'metadata',
event: undoBatch(() => {
this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout;
- this.rootDoc.layoutKey = 'layout_meta';
- setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._autoHeightMargins = 50), 50);
+ this.rootDoc.layout_fieldKey = 'layout_meta';
+ setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._layout_autoHeightMargins = 50), 50);
}),
icon: 'eye',
});
- const noteTypesDoc = Cast(Doc.UserDoc()['template-notes'], Doc, null);
+ const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null);
DocListCast(noteTypesDoc?.data).forEach(note => {
const icon: IconProp = StrCast(note.icon) as IconProp;
changeItems.push({
description: StrCast(note.title),
event: undoBatch(() => {
- this.layoutDoc.autoHeightMargins = undefined;
+ this.layoutDoc.layout_autoHeightMargins = undefined;
Doc.setNativeView(this.rootDoc);
DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
}),
@@ -772,24 +790,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour'];
(Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
highlighting.push({
- description: (FormattedTextBox._globalHighlights.indexOf(option) === -1 ? 'Highlight ' : 'Unhighlight ') + option,
- event: () => {
+ description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option,
+ event: action(() => {
e.stopPropagation();
- if (FormattedTextBox._globalHighlights.indexOf(option) === -1) {
- FormattedTextBox._globalHighlights.push(option);
+ if (!FormattedTextBox._globalHighlights.has(option)) {
+ FormattedTextBox._globalHighlights.add(option);
} else {
- FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1);
+ FormattedTextBox._globalHighlights.delete(option);
}
- runInAction(() => (this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join('')));
- this.updateHighlights();
- },
- icon: 'expand-arrows-alt',
+ }),
+ icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format',
})
);
const uicontrols: ContextMenuProps[] = [];
- !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
- uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
+ uicontrols.push({
+ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle',
+ event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar),
+ icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye',
+ });
+ uicontrols.push({
+ description: (this.Document._layout_altContentUI ? 'Hide' : 'Show') + ' Alt Content UI',
+ event: () => (this.layoutDoc._layout_altContentUI = !this.layoutDoc._layout_altContentUI),
+ icon: !this.Document._layout_altContentUI ? 'eye-slash' : 'eye',
+ });
uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
!Doc.noviceMode &&
uicontrols.push({
@@ -819,10 +843,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
this.rootDoc.isTemplateDoc = false;
this.rootDoc.isTemplateForField = '';
- this.rootDoc.layoutKey = 'layout';
+ this.rootDoc.layout_fieldKey = 'layout';
MakeTemplate(this.rootDoc, true, title);
setTimeout(() => {
- this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height
+ this.rootDoc._layout_autoHeight = this.layoutDoc._layout_autoHeight; // layout_autoHeight, width and height
this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template
this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields
this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, 'string', null);
@@ -830,7 +854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}, 10);
}
Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc);
- Doc.AddDocToList(Cast(Doc.UserDoc()['template-notes'], Doc, null), 'data', this.rootDoc);
+ Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.rootDoc);
},
icon: 'eye',
});
@@ -838,12 +862,77 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
- optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: 'expand-arrows-alt' });
- optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+ optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
+ optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
+ optionItems.push({
+ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
+ event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
+ icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
+ });
+ !Doc.noviceMode &&
+ optionItems.push({
+ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
+ 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: 'text' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
+ animateRes = (resIndex: number) => {
+ if (resIndex < this.gptRes.length) {
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ setTimeout(() => {
+ this.animateRes(resIndex + 1);
+ }, 20);
+ }
+ };
+
+ askGPT = action(async () => {
+ try {
+ let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
+ if (res) {
+ this.gptRes = res;
+ this.animateRes(0);
+ }
+ } catch (err) {
+ console.log(err);
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ }
+ });
+
+ generateImage = async () => {
+ console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text);
+ try {
+ let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
+ if (image_url) {
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+ const newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
+ y: NumCast(this.rootDoc.y),
+ _height: 200,
+ _width: 200,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+ if (Doc.IsInMyOverlay(this.rootDoc)) {
+ newDoc.overlayX = this.rootDoc.x;
+ newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddToMyOverlay(newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
+ }
+ } catch (err) {
+ console.log(err);
+ return '';
+ }
+ };
+
breakupDictation = () => {
if (this._editorView && this._recording) {
this.stopDictation(true);
@@ -873,14 +962,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this._editorView && this._recordingStart) {
if (this._break) {
const textanchorFunc = () => {
- const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
+ const tanch = Docs.Create.TextConfigDocument({ title: 'dictation anchor' });
return this.addDocument(tanch) ? tanch : undefined;
};
const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
if (link) {
Doc.GetProto(link).isDictation = true;
- const audioanchor = Cast(link.anchor2, Doc, null);
- const textanchor = Cast(link.anchor1, Doc, null);
+ const audioanchor = Cast(link.link_anchor_2, Doc, null);
+ const textanchor = Cast(link.link_anchor_1, Doc, null);
if (audioanchor) {
audioanchor.backgroundColor = 'tan';
const audiotag = this._editorView.state.schema.nodes.audiotag.create({
@@ -911,7 +1000,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
- const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
+ const anchor =
+ anchorDoc ??
+ Docs.Create.TextConfigDocument({
+ //
+ title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')',
+ annotationOn: this.dataDoc,
+ });
const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -934,19 +1029,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return anchorDoc ?? this.rootDoc;
}
- scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
- let didToggle = false;
- if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
- this.toggleSidebar(options.instant);
- didToggle = true;
+ getView = async (doc: Doc) => {
+ if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ !this.SidebarShown && this.toggleSidebar(false);
+ setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
}
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+ focus = (textAnchor: Doc, options: DocFocusOptions) => {
+ const focusSpeed = options.zoomTime ?? 500;
const textAnchorId = textAnchor[Id];
const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
const nodes: Node[] = [];
let hadStart = start !== 0;
frag.forEach((node, index) => {
const examinedNode = findAnchorNode(node, editor);
- if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
+ if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
nodes.push(examinedNode.node);
!hadStart && (start = index + examinedNode.start);
hadStart = true;
@@ -961,9 +1059,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
return undefined;
}
+ if (node.type === this._editorView?.state.schema.nodes.dashDoc) {
+ if (node.attrs.docId === textAnchorId) {
+ return { node, start: 0 };
+ }
+ return undefined;
+ }
if (!node.isText) {
const content = findAnchorFrag(node.content, editor);
- return { node: node.copy(content.frag), start: content.start };
+ if (content.frag.childCount) return { node: content.frag.childCount ? content.frag.child(0) : node, start: content.start };
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor);
@@ -977,8 +1081,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const ret = findAnchorFrag(editor.state.doc.content, editor);
const content = (ret.frag as any)?.content;
- if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
- !options.instant && (this._focusSpeed = 500);
+ if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
+ !options.instant && (this._focusSpeed = focusSpeed);
let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
if (ret.frag.firstChild) {
selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
@@ -988,18 +1092,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed);
setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000));
+ return focusSpeed;
+ } else {
+ return this.props.focus(this.rootDoc, options);
}
}
-
- return this._didScroll ? this._focusSpeed : didToggle ? 1 : undefined; // if we actually scrolled, then return some focusSpeed
};
- getScrollHeight = () => this.scrollHeight;
- // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
+ // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
// Since we also monitor all component height changes, this will update the document's height.
resetNativeHeight = (scrollHeight: number) => {
const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight);
- this.rootDoc[this.fieldKey + '-height'] = scrollHeight;
+ this.rootDoc[this.fieldKey + '_height'] = scrollHeight;
if (nh) this.layoutDoc._nativeHeight = scrollHeight;
};
@@ -1008,34 +1112,39 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
componentDidMount() {
!this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
- this._cachedLinks = DocListCast(this.Document.links);
+ this._cachedLinks = LinkManager.Links(this.Document);
this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
- this._disposers.autoHeight = reaction(
- () => this.autoHeight,
- autoHeight => autoHeight && this.tryUpdateScrollHeight()
+ this._disposers.layout_autoHeight = reaction(
+ () => this.layout_autoHeight,
+ layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight()
+ );
+ this._disposers.highlights = reaction(
+ () => Array.from(FormattedTextBox._globalHighlights).slice(),
+ highlights => this.updateHighlights(highlights),
+ { fireImmediately: true }
);
this._disposers.width = reaction(
() => this.props.PanelWidth(),
width => this.tryUpdateScrollHeight()
);
this._disposers.scrollHeight = reaction(
- () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }),
- ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight),
+ () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
+ ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight),
{ fireImmediately: true }
);
this._disposers.componentHeights = reaction(
- // set the document height when one of the component heights changes and autoHeight is on
- () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
- ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
+ // set the document height when one of the component heights changes and layout_autoHeight is on
+ () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
+ ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
- if (autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) {
+ if (layout_autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) {
this.props.setHeight?.(newHeight);
}
},
{ fireImmediately: true }
);
this._disposers.links = reaction(
- () => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+ () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
this._cachedLinks = newLinks;
@@ -1053,7 +1162,7 @@ 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;
+ 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(whichDoc[this.fieldKey]) };
},
incomingValue => {
@@ -1098,7 +1207,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._disposers.selected = reaction(
() => this.props.isSelected(),
action(selected => {
- this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join('') : '';
+ if (FormattedTextBox._globalHighlights.has('Bold Text')) {
+ this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
+ }
if (RichTextMenu.Instance?.view === this._editorView && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
@@ -1106,6 +1217,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
this.autoLink();
}
+ // Accessing editor and text doc for gpt assisted text edits
+ if (this._editorView && selected) {
+ AnchorMenu.Instance?.setEditorView(this._editorView);
+ AnchorMenu.Instance?.setTextDoc(this.dataDoc);
+ }
}),
{ fireImmediately: true }
);
@@ -1124,7 +1240,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
var quickScroll: string | undefined = '';
this._disposers.scroll = reaction(
- () => NumCast(this.layoutDoc._scrollTop),
+ () => NumCast(this.layoutDoc._layout_scrollTop),
pos => {
if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) {
const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
@@ -1201,7 +1317,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}, 0);
dataDoc.title = exportState.title;
- this.dataDoc['title-custom'] = true;
+ this.dataDoc.title_custom = true;
dataDoc.googleDocUnchanged = true;
} else {
delete dataDoc[GoogleRef];
@@ -1254,11 +1370,11 @@ 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, editable: false }, undefined, [
view.state.schema.marks.linkAnchor.create({
allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }],
location: 'add:right',
- title: `from: ${DocCast(pdfAnchor.context).title}`,
+ title: `from: ${DocCast(pdfAnchor.embedContainer).title}`,
noPreview: true,
docref: false,
}),
@@ -1267,7 +1383,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
]),
]);
- const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted');
+ const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { link_relationship: 'PDF pasted' });
if (link) {
view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
}
@@ -1355,7 +1471,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (startupText) {
dispatch(state.tr.insertText(startupText));
}
- const textAlign = StrCast(this.dataDoc['text-align'], StrCast(Doc.UserDoc().textAlign, 'left'));
+ const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left'));
if (textAlign !== 'left') {
selectAll(this._editorView.state, tr => {
this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
@@ -1366,11 +1482,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()));
- if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+ if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
const selLoadChar = FormattedTextBox.SelectOnLoadChar;
FormattedTextBox.SelectOnLoad = '';
this.props.select(false);
- if (selLoadChar && this._editorView) {
+ 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) ?? [];
@@ -1380,10 +1496,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
.setStoredMarks(storedMarks);
this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
- } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) {
+ } else if (curText && !FormattedTextBox.DontSelectInitialText) {
selectAll(this._editorView.state, this._editorView?.dispatch);
- this.startUndoTypingBatch();
- } else if (this._editorView) {
+ } else {
this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
}
@@ -1392,7 +1507,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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 selectoin after the document has ben fully instantiated.
+ // 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);
this._editorView.state.storedMarks = [
...(this._editorView.state.storedMarks ?? []),
@@ -1414,7 +1529,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
componentWillUnmount() {
- FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
this.unhighlightSearchTerms();
@@ -1429,7 +1543,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
} else (e.nativeEvent as any).handledByInnerReactInstance = true;
if (this.Document.forceActive) e.stopPropagation();
- this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
+ this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
if ((e.target as any).tagName === 'AUDIOTAG') {
e.preventDefault();
e.stopPropagation();
@@ -1456,7 +1570,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
this._downX = e.clientX;
this._downY = e.clientY;
- this._downEvent = true;
FormattedTextBoxComment.textBox = this;
if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
@@ -1465,33 +1578,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// but that's changed, so this shouldn't be needed.
//e.stopPropagation(); // if the text box is selected, then it consumes all down events
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
onSelectEnd = (e: PointerEvent) => {
document.removeEventListener('pointerup', this.onSelectEnd);
- document.removeEventListener('pointermove', this.onSelectMove);
};
onPointerUp = (e: React.PointerEvent): void => {
- if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
- if (!this._downEvent) return;
- this._downEvent = false;
- if (this._editorView?.state.selection.empty && this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
- const editor = this._editorView!;
+ const editor = this._editorView!;
+ const state = editor?.state;
+ 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(true)) {
const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
- !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
+ // !this.props.isSelected(true) &&
+ editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0))));
let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
while (target && !target.dataset?.targethrefs) target = target.parentElement;
FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
- }
-
- if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
- e.stopPropagation();
+ if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) {
+ return;
+ }
}
};
@action
@@ -1520,13 +1630,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@action
onFocused = (e: React.FocusEvent): void => {
//applyDevTools.applyDevTools(this._editorView);
- FormattedTextBox.Focused = this;
this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
- this.startUndoTypingBatch();
+ e.stopPropagation();
};
- @observable public static Focused: FormattedTextBox | undefined;
onClick = (e: React.MouseEvent): void => {
+ if (!this.props.isContentActive()) return;
if ((e.nativeEvent as any).handledByInnerReactInstance) {
e.stopPropagation();
return;
@@ -1558,8 +1667,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this.props.isSelected(true)) {
// if text box is selected, then it consumes all click events
(e.nativeEvent as any).handledByInnerReactInstance = true;
- if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it.
- // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment)
this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
}
this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
@@ -1599,13 +1706,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
startUndoTypingBatch() {
- !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping'));
+ !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title));
}
public endUndoTypingBatch() {
- const wasUndoing = this._undoTyping;
this._undoTyping?.end();
this._undoTyping = undefined;
- return wasUndoing;
}
@action
@@ -1622,7 +1727,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
tr && this._editorView.dispatch(tr);
}
}
- FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
@@ -1640,7 +1744,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
setTimeout(
() =>
translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => {
- this.dataDoc[this.fieldKey + '-translation'] = result1 + '\r\n\r\n' + result[0];
+ this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0];
}),
1000
);
@@ -1650,10 +1754,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
this._lastText = curText;
}
- if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc['title-custom']) {
+ if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc.title_custom) {
UndoManager.RunInBatch(() => {
- this.dataDoc['title-custom'] = true;
- this.dataDoc.showTitle = 'title';
+ 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.rootDoc.title).length + 2))).deleteSelection());
}, 'titler');
@@ -1661,8 +1765,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
+ if ((e.altKey || e.ctrlKey) && e.key === 't') {
e.preventDefault();
+ e.stopPropagation();
+ this.props.setTitleFocus?.();
return;
}
const state = this._editorView!.state;
@@ -1711,8 +1817,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (!LinkDocPreview.LinkInfo && this._scrollRef.current) {
if (!this.props.dontSelectOnLoad) {
this._ignoreScroll = true;
- this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop;
+ this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop;
this._ignoreScroll = false;
+ e.stopPropagation();
+ e.preventDefault();
}
}
};
@@ -1720,13 +1828,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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) {
- const proseHeight = !this.ProseRef
- ? 0
- : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')) + Number(getComputedStyle(child).marginTop.replace('px', '')) + Number(getComputedStyle(child).marginBottom.replace('px', '')), margins);
+ const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0'));
+ const toHgt = (node: Element) => {
+ const { height, marginTop, marginBottom } = getComputedStyle(node);
+ return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
+ };
+ const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins);
const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
if (this.props.setHeight && 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.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight);
+ const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);
if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
} else {
@@ -1735,23 +1846,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
};
- fitContentsToBox = () => this.props.Document._fitContentsToBox;
- sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox);
+ sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
- // console.log("printting allSideBarDocs");
- // console.log(this.allSidebarDocs);
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
};
sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
- setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '-height'] = height);
- sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '_height'] = height);
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
sidebarScreenToLocal = () =>
this.props
.ScreenToLocalTransform()
.translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0)
- .scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1));
+ .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1));
@computed get audioHandle() {
return !this._recording ? null : (
@@ -1781,11 +1890,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
className="formattedTextBox-sidebar-handle"
onPointerDown={this.sidebarDown}
style={{
- backgroundColor: backgroundColor,
- color: color,
+ backgroundColor,
+ color,
opacity: annotated ? 1 : undefined,
}}>
- <FontAwesomeIcon icon={'comment-alt'} />
+ <FontAwesomeIcon icon="comment-alt" />
</div>
);
}
@@ -1796,74 +1905,133 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
- fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- ScreenToLocalTransform={this.sidebarScreenToLocal}
+ usePanelWidth={true}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
showSidebar={this.SidebarShown}
- PanelWidth={this.sidebarWidth}
- setHeight={this.setSidebarHeight}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ fieldKey={this.fieldKey}
+ PanelWidth={this.sidebarWidth}
+ setHeight={this.setSidebarHeight}
/>
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
<ComponentTag
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.sidebarWidth}
xPadding={0}
yPadding={0}
- scaleField={this.SidebarKey + '-scale'}
+ viewField={this.SidebarKey}
isAnnotationOverlay={false}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
NativeDimScaling={this.sidebarContentScaling}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.sidebarRemDocument}
moveDocument={this.sidebarMoveDocument}
addDocument={this.sidebarAddDocument}
- CollectionView={undefined}
ScreenToLocalTransform={this.sidebarScreenToLocal}
renderDepth={this.props.renderDepth + 1}
setHeight={this.setSidebarHeight}
fitContentsToBox={this.fitContentsToBox}
noSidebar={true}
treeViewHideTitle={true}
- fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}-translation` : `${this.fieldKey}-sidebar`}
+ fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
/>
</div>
);
};
return (
- <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
{renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
</div>
);
}
+ cycleAlternateText = () => {
+ if (this.layoutDoc._layout_altContentUI) {
+ const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
+ }
+ };
+ @computed get overlayAlternateIcon() {
+ const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ return (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">
+ toggle (%/) between
+ <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+ <em> primary, </em>
+ </span>
+ <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+ <em>alternate, </em>
+ </span>
+ and show
+ <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+ <em> alternate on hover</em>
+ </span>
+ </div>
+ }>
+ <div
+ className="formattedTextBox-alternateButton"
+ onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())}
+ style={{
+ display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none',
+ background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
+ color: usePath === undefined ? 'black' : 'white',
+ }}>
+ <FontAwesomeIcon icon="turn-up" size="sm" />
+ </div>
+ </Tooltip>
+ );
+ }
+ @computed get fieldKey() {
+ const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}_usePath`]);
+ return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : '');
+ }
+ @observable _isHovering = false;
+ onPassiveWheel = (e:WheelEvent) => {
+ // 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() && !this.props.allowScroll) {
+ if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ _oldWheel:any;
render() {
TraceMobx();
const active = this.props.isContentActive() || this.props.isSelected();
const selected = active;
- const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : '';
const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition);
if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
const minimal = this.props.ignoreAutoHeight;
const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
- const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : '';
+ const selPad = (selected && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
+ const selPaddingClass = selected && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : '';
const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return styleFromLayoutString?.height === '0px' ? null : (
<div
- className="formattedTextBox-cont"
- onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+ className="formattedTextBox"
+ onPointerEnter={action(() => (this._isHovering = true))}
+ onPointerLeave={action(() => (this._isHovering = false))}
+ ref={r => {
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel= r;
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false } );
+ }}
style={{
...(this.props.dontScale
? {}
@@ -1873,8 +2041,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}),
+ display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
transition: 'inherit',
- // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
+ // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined,
color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily),
@@ -1882,11 +2051,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
...styleFromLayoutString,
}}>
<div
- className={`formattedTextBox-cont`}
+ className="formattedTextBox-cont"
ref={this._ref}
style={{
- overflow: this.autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an autoHeight doc in its own tab, or in the lightbox
- height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
+ overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox
+ height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
pointerEvents: interactive ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
@@ -1902,9 +2071,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
className={`formattedTextBox-outer${selected ? '-selected' : ''}`}
ref={this._scrollRef}
style={{
- width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
+ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
- overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined,
+ overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
@@ -1917,13 +2086,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined,
+ pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined,
}}
/>
</div>
- {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
- {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
+ {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 ? null : this.sidebarHandle}
{this.audioHandle}
+ {this.layoutDoc._layout_altContentUI ? this.overlayAlternateIcon : null}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 68b0488a2..4dfe07b24 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -8,6 +8,7 @@ import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc';
import { GetEffectiveAcl } from '../../../../fields/util';
import { Utils } from '../../../../Utils';
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';
@@ -178,6 +179,83 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
return true;
});
+ bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ RTFMarkup.Instance.open();
+ return true;
+ });
+ bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (!state.selection.empty) {
+ const mark = state.schema.marks.summarizeInclusive.create();
+ const tr = state.tr.addMark(state.selection.$from.pos, state.selection.$to.pos, mark);
+ const content = tr.selection.content();
+ tr.selection.replaceWith(tr, schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }));
+ dispatch(tr);
+ }
+ return true;
+ });
+ bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+
+ bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1);
+ const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined);
+ const tr = state.tr;
+ tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ dispatch(
+ tr.setSelection(
+ new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve(
+ // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - (tr.selection.$anchor.nodeBefore?.nodeSize || 0)
+ )
+ )
+ )
+ );
+ return true;
+ });
bind('Ctrl-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 6c6d26af5..5e0041b84 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -20,6 +20,7 @@ import { FormattedTextBox } from './FormattedTextBox';
import { updateBullets } from './ProsemirrorExampleTransfer';
import './RichTextMenu.scss';
import { schema } from './schema_rts';
+import { EquationBox } from '../EquationBox';
const { toggleMark } = require('prosemirror-commands');
@observer
@@ -53,7 +54,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _activeFontColor: string = 'black';
@observable private showColorDropdown: boolean = false;
- @observable private activeHighlightColor: string = 'transparent';
+ @observable private _activeHighlightColor: string = 'transparent';
@observable private showHighlightDropdown: boolean = false;
@observable private currentLink: string | undefined = '';
@@ -88,6 +89,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get fontColor() {
return this._activeFontColor;
}
+ @computed get fontHighlight() {
+ return this._activeHighlightColor;
+ }
@computed get fontFamily() {
return this._activeFontFamily;
}
@@ -97,6 +101,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get textAlign() {
return this._activeAlignment;
}
+ _disposer: IReactionDisposer | undefined;
+ componentDidMount() {
+ this._disposer = reaction(
+ () => SelectionManager.Views(),
+ views => this.updateMenu(undefined, undefined, undefined)
+ );
+ }
+ componentWillUnmount() {
+ this._disposer?.();
+ }
@action
public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) {
@@ -127,7 +141,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
- this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
+ this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
// update link in current selection
this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
@@ -206,6 +220,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize);
m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight));
});
+ } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) {
+ SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._fontSize) && activeSizes.add(StrCast(dv.rootDoc._fontSize)));
}
return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) };
}
@@ -328,6 +344,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
}
+ } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) {
+ SelectionManager.Views().forEach(dv => (dv.rootDoc._fontSize = fontSize));
} else Doc.UserDoc()._fontSize = fontSize;
this.updateMenu(this.view, undefined, this.props);
};
@@ -341,11 +359,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.updateMenu(this.view, undefined, this.props);
};
- setHighlight(color: String, view: EditorView, dispatch: any) {
- const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color });
- if (view.state.selection.empty) return false;
- view.focus();
- this.setMark(highlightMark, view.state, dispatch, false);
+ setHighlight(color: string) {
+ if (this.view) {
+ const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color });
+ this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true);
+ this.view.focus();
+ } else Doc.UserDoc()._fontHighlight = color;
+ this.updateMenu(this.view, undefined, this.props);
}
setColor(color: string) {
@@ -548,7 +568,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
@action setActiveHighlight(color: string) {
- this.activeHighlightColor = color;
+ this._activeHighlightColor = color;
}
@action setCurrentLink(link: string) {
@@ -603,15 +623,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (linkclicked) {
const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
- const anchor1 = await Cast(linkDoc.anchor1, Doc);
- const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc);
+ const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc);
const currentDoc = SelectionManager.Docs().lastElement();
- if (currentDoc && anchor1 && anchor2) {
- if (Doc.AreProtosEqual(currentDoc, anchor1)) {
- return StrCast(anchor2.title);
+ if (currentDoc && link_anchor_1 && link_anchor_2) {
+ if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) {
+ return StrCast(link_anchor_2.title);
}
- if (Doc.AreProtosEqual(currentDoc, anchor2)) {
- return StrCast(anchor1.title);
+ if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) {
+ return StrCast(link_anchor_1.title);
}
}
}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 5675776fb..104aed058 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,11 +1,12 @@
import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { DataSym, Doc } from '../../../../fields/Doc';
+import { DataSym, Doc, StrListCast } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
import { ComputedField } from '../../../../fields/ScriptField';
import { NumCast, StrCast } from '../../../../fields/Types';
import { normalizeEmail } from '../../../../fields/util';
-import { returnFalse, Utils } from '../../../../Utils';
+import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { FormattedTextBox } from './FormattedTextBox';
@@ -28,7 +29,7 @@ export class RichTextRules {
emDash,
// > blockquote
- wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+ wrappingInputRule(/%>$/, schema.nodes.blockquote),
// 1. create numerical ordered list
wrappingInputRule(
@@ -80,17 +81,17 @@ export class RichTextRules {
textDoc.inlineTextCount = numInlines + 1;
const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to
const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation
- const textDocInline = Docs.Create.TextDocument('', { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _fitWidth: true, _autoHeight: true, _fontSize: '9px', title: 'inline comment' });
+ const textDocInline = Docs.Create.TextDocument('', { _layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _layout_fitWidth: true, _layout_autoHeight: true, _fontSize: '9px', title: 'inline comment' });
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.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.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`);
textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
textDoc[inlineFieldKey] = ''; // set a default value for the annotation
const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' });
+ const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
const sm = state.storedMarks || undefined;
const replaced = node
? state.tr
@@ -190,21 +191,6 @@ export class RichTextRules {
}
}),
- // %f create footnote
- new InputRule(new RegExp(/%f$/), (state, match, start, end) => {
- const newNode = schema.nodes.footnote.create({});
- const tr = state.tr;
- tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
- return tr.setSelection(
- new NodeSelection( // select the footnote node to open its display
- tr.doc.resolve(
- // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
- tr.selection.anchor - (tr.selection.$anchor.nodeBefore?.nodeSize || 0)
- )
- )
- );
- }),
-
// activate a style by name using prefix '%<color name>'
new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => {
const color = match[0].substring(1, match[0].length);
@@ -229,6 +215,12 @@ export class RichTextRules {
}),
// stop using active style
+ new InputRule(new RegExp(/%\//), (state, match, start, end) => {
+ setTimeout(this.TextBox.cycleAlternateText);
+ return state.tr.deleteRange(start, end);
+ }),
+
+ // stop using active style
new InputRule(new RegExp(/%%$/), (state, match, start, end) => {
const tr = state.tr.deleteRange(start, end);
const marks = state.tr.selection.$anchor.nodeBefore?.marks;
@@ -248,24 +240,28 @@ export class RichTextRules {
// [[fieldKey:Doc]] => show field of doc
new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
const fieldKey = match[1];
- const docid = match[3]?.replace(':', '');
+ const docId = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
+ const linkToDoc = (target: Doc) => {
+ const rstate = this.TextBox.EditorView?.state;
+ const selection = rstate?.selection.$from.pos;
+ if (rstate) {
+ this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+ }
+
+ DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
+
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate && selection) {
+ this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+ }
+ };
if (!fieldKey) {
- if (docid) {
- DocServer.GetRefField(docid).then(docx => {
- 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 target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
-
- const fstate = this.TextBox.EditorView?.state;
- if (fstate && selection) {
- this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
- }
- });
+ if (docId) {
+ const target = DocServer.QUERY_SERVER_CACHE(docId);
+ if (target) setTimeout(() => linkToDoc(target));
+ else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId)));
+
return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
}
return state.tr;
@@ -274,7 +270,7 @@ export class RichTextRules {
const num = value.match(/^[0-9.]$/);
this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false });
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
}),
@@ -296,43 +292,22 @@ export class RichTextRules {
// create an inline equation node
// eq:<equation>>
- new InputRule(new RegExp(/:eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+ new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
this.TextBox.dataDoc[fieldKey] = match[1];
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)));
}),
- // create an inline view of a document {{ <layoutKey> : <Doc> }}
- // {{:Doc}} => show default view of document
- // {{<layout>}} => show layout for this doc
- // {{<layout> : Doc}} => show layout for another doc
- new InputRule(new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_@\.\? \-0-9]+)?\}\}$/), (state, match, start, end) => {
- const fieldKey = match[1] || '';
- const fieldParam = match[2]?.replace('…', '...') || '';
- const rawdocid = match[3]?.substring(1);
- const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
- if (!fieldKey && !docid) return state.tr;
- docid &&
- DocServer.GetRefField(docid).then(docx => {
- if (!(docx instanceof Doc && docx)) {
- Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid);
- }
- });
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
- const sm = state.storedMarks || undefined;
- return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }),
-
// create an inline view of a tag stored under the '#' field
new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => {
const tag = match[1];
if (!tag) return state.tr;
- this.Document[DataSym]['#' + tag] = '#' + tag;
- const tags = StrCast(this.Document[DataSym].tags, ':');
- if (!tags.includes(`#${tag}:`)) {
- this.Document[DataSym].tags = `${tags + '#' + tag + ':'}`;
+ //this.Document[DataSym]['#' + tag] = '#' + tag;
+ const tags = StrListCast(this.Document[DataSym].tags);
+ if (!tags.includes(tag)) {
+ tags.push(tag);
+ this.Document[DataSym].tags = new List<string>(tags);
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
return state.tr
@@ -374,7 +349,7 @@ export class RichTextRules {
const content = selected.selection.content();
const replaced = node ? selected.replaceRangeWith(start, end, schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]);
}),
new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 4e75d374c..3355e4529 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -43,7 +43,6 @@ export class SummaryView {
className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
destroy() {
this.root.unmount();
- // ReactDOM.unmountComponentAtNode(this.dom);
}
selectNode() {}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 3898490d3..7e17008bb 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -46,7 +46,7 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM(node: any) {
const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), '');
const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
- return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0];
+ return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0];
},
},
noAutoLinkAnchor: {
@@ -349,7 +349,7 @@ export const marks: { [index: string]: MarkSpec } = {
group: 'inline',
toDOM(node: any) {
const uid = node.attrs.userid.replace('.', '').replace('@', '');
- const min = Math.round(node.attrs.modified / 12);
+ const min = Math.round(node.attrs.modified / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index aa2475dca..f27fb18e2 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = {
dashComment: {
attrs: {
- docid: { default: '' },
+ docId: { default: '' },
},
inline: true,
group: 'inline',
@@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = {
title: { default: null },
float: { default: 'left' },
location: { default: 'add:right' },
- docid: { default: '' },
+ docId: { default: '' },
},
group: 'inline',
draggable: true,
@@ -246,8 +246,8 @@ export const nodes: { [index: string]: NodeSpec } = {
float: { default: 'right' },
hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
fieldKey: { default: '' },
- docid: { default: '' },
- alias: { default: '' },
+ docId: { default: '' },
+ embedding: { default: '' },
},
group: 'inline',
draggable: false,
@@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
fieldKey: { default: '' },
- docid: { default: '' },
+ docId: { default: '' },
hideKey: { default: false },
editable: { default: true },
},
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index fd202590e..bf56b4d9e 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -12,7 +12,7 @@
height: 100%;
min-height: 35px;
letter-spacing: 2px;
- overflow: hidden;
+ //overflow: hidden;
transition: 0.7s opacity ease;
.presBox-listCont {
@@ -951,6 +951,7 @@
margin-right: unset;
height: 100%;
position: relative;
+ user-select: none;
}
select {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index d9bc2d981..913018b69 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,15 +1,15 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, ObservableSet, observe, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { ColorState, SketchPicker } from 'react-color';
-import { AnimationSym, Doc, DocListCast, FieldResult, HighlightSym, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
import { Copy, Id } from '../../../../fields/FieldSymbols';
-import { InkTool } from '../../../../fields/InkField';
+import { InkField } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { ObjectField } from '../../../../fields/ObjectField';
import { listSpec } from '../../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { AudioField } from '../../../../fields/URLField';
import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
@@ -19,24 +19,39 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp
import { DocumentManager } from '../../../util/DocumentManager';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SelectionManager } from '../../../util/SelectionManager';
+import { SerializationHelper } from '../../../util/SerializationHelper';
import { SettingsManager } from '../../../util/SettingsManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionDockingView } from '../../collections/CollectionDockingView';
import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
import { CollectionView } from '../../collections/CollectionView';
import { TabDocView } from '../../collections/TabDocView';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
-import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { ScriptingBox } from '../ScriptingBox';
import './PresBox.scss';
import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
const { Howl } = require('howler');
+export interface pinDataTypes {
+ scrollable?: boolean;
+ dataviz?: number[];
+ pannable?: boolean;
+ type_collection?: boolean;
+ inkable?: boolean;
+ filters?: boolean;
+ pivot?: boolean;
+ temporal?: boolean;
+ clippable?: boolean;
+ datarange?: boolean;
+ dataview?: boolean;
+ poslayoutview?: boolean;
+ dataannos?: boolean;
+}
export interface PinProps {
audioRange?: boolean;
activeFrame?: number;
@@ -44,18 +59,8 @@ export interface PinProps {
hidePresBox?: boolean;
pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
pinDocLayout?: boolean; // pin layout info (width/height/x/y)
- pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
pinAudioPlay?: boolean; // pin audio annotation
- pinData?: {
- scrollable?: boolean | undefined;
- pannable?: boolean | undefined;
- temporal?: boolean | undefined;
- clippable?: boolean | undefined;
- dataview?: boolean | undefined;
- textview?: boolean | undefined;
- poslayoutview?: boolean | undefined;
- dataannos?: boolean | undefined;
- };
+ pinData?: pinDataTypes;
}
@observer
@@ -63,9 +68,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(PresBox, fieldKey);
}
+ static navigateToDocScript: ScriptField;
+
+ constructor(props: any) {
+ super(props);
+ if (!PresBox.navigateToDocScript) {
+ PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!;
+ }
+ }
private _disposers: { [name: string]: IReactionDisposer } = {};
public selectedArray = new ObservableSet<Doc>();
+ _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves
+ _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
+ _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
@observable public static Instance: PresBox;
@@ -81,15 +97,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _newDocumentTools: boolean = false;
@observable _openMovementDropdown: boolean = false;
@observable _openEffectDropdown: boolean = false;
+ @observable _openBulletEffectDropdown: boolean = false;
@observable _presentTools: boolean = false;
@observable _treeViewMap: Map<Doc, number> = new Map();
@observable _presKeyEvents: boolean = false;
@observable _forceKeyEvents: boolean = false;
@computed get isTreeOrStack() {
- return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any);
+ return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any);
}
@computed get isTree() {
- return this.layoutDoc._viewType === CollectionViewType.Tree;
+ return this.layoutDoc._type_collection === CollectionViewType.Tree;
}
@computed get presFieldKey() {
return StrCast(this.layoutDoc.presFieldKey, 'data');
@@ -109,12 +126,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get targetDoc() {
return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
}
+ public static targetRenderedDoc = (doc: Doc) => {
+ const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null);
+ return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
+ };
@computed get scrollable() {
- if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+ if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true;
return false;
}
@computed get panable() {
- if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
+ if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
return false;
}
@computed get selectedDocumentView() {
@@ -132,14 +153,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
- _unmounting = false;
@action
componentWillUnmount() {
this._unmounting = true;
if (this._presTimer) clearTimeout(this._presTimer);
document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
this.resetPresentation();
- // Turn of progressivize editors
this.turnOffEdit(true);
Object.values(this._disposers).forEach(disposer => disposer?.());
}
@@ -177,7 +196,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.turnOffEdit(true);
this._disposers.selection = reaction(
() => SelectionManager.Views(),
- views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()
+ views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(),
+ {fireImmediately:true}
+ );
+ this._disposers.editing = reaction(
+ () => this.layoutDoc.presStatus === PresStatus.Edit,
+ editing => {
+ if (editing) {
+ this.childDocs.forEach(doc => {
+ if (doc.presIndexed !== undefined) {
+ this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
+ doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
+ }
+ });
+ }
+ }
);
}
@@ -198,7 +231,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
stopTempMedia = (targetDocField: FieldResult) => {
- const targetDoc = Cast(targetDocField, Doc, null);
+ const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField);
if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
targMedia?.ComponentView?.Pause?.();
@@ -211,7 +244,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
nextSlide = (slideNum?: number) => {
const nextSlideInd = slideNum ?? this.itemIndex + 1;
let curSlideInd = nextSlideInd;
- CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
+ //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
this.clearSelectedArray();
const doGroupWithUp =
(nextSelected: number, force = false) =>
@@ -237,18 +270,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doGroupWithUp(curSlideInd, true)();
};
+ // docs within a slide target that will be progressively revealed
+ progressivizedItems = (doc: Doc) => {
+ const targetList = PresBox.targetRenderedDoc(doc);
+ if (doc.presIndexed !== undefined && targetList) {
+ const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']);
+ return listItems.filter(doc => !doc.layout_unrendered);
+ }
+ };
// Called when the user activates 'next' - to move to the next part of the pres. trail
@action
next = () => {
+ const progressiveReveal = (first: boolean) => {
+ const presIndexed = Cast(this.activeItem?.presIndexed, 'number', null);
+ if (presIndexed !== undefined) {
+ const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
+ targetRenderedDoc._dataTransition = 'all 1s';
+ targetRenderedDoc.opacity = 1;
+ setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000);
+ const listItems = this.progressivizedItems(this.activeItem);
+ if (listItems && presIndexed < listItems.length) {
+ if (!first) {
+ const listItemDoc = listItems[presIndexed];
+ const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc);
+ Doc.linkFollowUnhighlight();
+ Doc.HighlightDoc(listItemDoc);
+ listItemDoc.presEffect = this.activeItem.presBulletEffect;
+ listItemDoc.presTransition = 500;
+ targetView?.setAnimEffect(listItemDoc, 500);
+ if (targetView?.docView && this.activeItem.presBulletExpand) {
+ targetView.docView._animateScalingTo = 1.1;
+ Doc.AddUnHighlightWatcher(() => (targetView!.docView!._animateScalingTo = 0));
+ }
+ listItemDoc.opacity = undefined;
+ this.activeItem.presIndexed = presIndexed + 1;
+ }
+ return true;
+ }
+ }
+ };
+ if (progressiveReveal(false)) return true;
if (this.childDocs[this.itemIndex + 1] !== undefined) {
// Case 1: No more frames in current doc and next slide is defined, therefore move to next slide
const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]);
const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex;
this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1);
+ progressiveReveal(true); // shows first progressive document, but without a transition effect
} else {
if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
// Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
this.nextSlide(0);
+ progressiveReveal(true); // shows first progressive document, but without a transition effect
}
return 0;
}
@@ -259,7 +331,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
back = () => {
const activeItem: Doc = this.activeItem;
- const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
let prevSelected = this.itemIndex;
// Functionality for group with up
let didZoom = activeItem.presMovement;
@@ -285,21 +356,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UnBrushAllDocs();
if (index >= 0 && index < this.childDocs.length) {
this.rootDoc._itemIndex = index;
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
- if (activeFrame !== undefined) {
- const transTime = NumCast(activeItem.presTransition, 500);
- const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
- const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
- if (context) {
- const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
- if (ffview?.childDocs) {
- this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
- context._currentFrame = NumCast(activeFrame);
- }
- }
- }
if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
}
@@ -307,69 +363,150 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.stopTempMedia(from.presentationTargetDoc);
}
// If next slide is audio / video 'Play automatically' then the next slide should be played
- if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
- this.startTempMedia(targetDoc, activeItem);
+ if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') {
+ this.startTempMedia(this.targetDoc, this.activeItem);
}
if (!group) this.clearSelectedArray();
this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
this.turnOffEdit();
this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list
- this.onHideDocument(); //Handles hide after/before
+ this.doHideBeforeAfter(); //Handles hide after/before
}
});
- static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+ static pinDataTypes(target?: Doc): pinDataTypes {
const targetType = target?.type as any;
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
- const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+ const inkable = [DocumentType.INK].includes(targetType);
+ const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking;
+ const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform);
const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
const clippable = [DocumentType.COMPARISON].includes(targetType);
- const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined;
+ const datarange = [DocumentType.FUNCPLOT].includes(targetType);
+ const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
- const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
+ const type_collection = targetType === DocumentType.COL;
+ const filters = true;
+ const pivot = true;
const dataannos = false;
- return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
+ return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
playAnnotation = (anno: AudioField) => {};
@action
- static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) {
- if (!bestTargetView) return;
- const bestTarget = bestTargetView.rootDoc;
+ static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
+ const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
+ if (!bestTarget || activeItem === bestTarget) return;
let changed = false;
- if (pinProps?.pinDocLayout) {
+ if (pinDocLayout) {
if (
bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) ||
bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) ||
- bestTarget.rotation !== NumCast(activeItem.presRot, NumCast(bestTarget.rotation)) ||
+ bestTarget.rotation !== NumCast(activeItem.presRotation, NumCast(bestTarget.rotation)) ||
bestTarget.width !== NumCast(activeItem.presWidth, NumCast(bestTarget.width)) ||
bestTarget.height !== NumCast(activeItem.presHeight, NumCast(bestTarget.height))
) {
bestTarget._dataTransition = `all ${transTime}ms`;
bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x));
bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y));
- bestTarget.rotation = NumCast(activeItem.presRot, NumCast(bestTarget.rotation));
+ bestTarget.rotation = NumCast(activeItem.presRotation, NumCast(bestTarget.rotation));
bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
changed = true;
}
}
- if (pinDataTypes.clippable) {
- if (bestTarget._clipWidth !== activeItem.presPinClipWidth) {
- bestTarget._clipWidth = activeItem.presPinClipWidth;
+
+ const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
+ if (activeFrame !== undefined) {
+ const transTime = NumCast(activeItem.presTransition, 500);
+ const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).embedContainer) : DocCast(activeItem.presentationTargetDoc);
+ const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
+ if (context) {
+ const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ if (ffview?.childDocs) {
+ PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime);
+ ffview.rootDoc._currentFrame = NumCast(activeFrame);
+ }
+ }
+ }
+ if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) {
+ bestTarget._dataTransition = `all ${transTime}ms`;
+ const fkey = Doc.LayoutFieldKey(bestTarget);
+ const setData = bestTargetView?.ComponentView?.setData;
+ if (setData) setData(activeItem.presData);
+ else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+ bestTarget[fkey + '_usePath'] = activeItem.presUsePath;
+ setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+ }
+ if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) {
+ if (bestTarget.xRange !== activeItem.presXRange) {
+ bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy]();
+ changed = true;
+ }
+ if (bestTarget.yRange !== activeItem.presYRange) {
+ bestTarget.yRange = (activeItem.presYRange as ObjectField)?.[Copy]();
+ changed = true;
+ }
+ }
+ if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) {
+ const fkey = '_' + Doc.LayoutFieldKey(bestTarget);
+ if (bestTarget[fkey + '_clipWidth'] !== activeItem.presClipWidth) {
+ bestTarget[fkey + '_clipWidth'] = activeItem.presClipWidth;
changed = true;
}
}
- if (pinDataTypes.temporal) {
- if (bestTarget._currentTimecode !== activeItem.presStartTime) {
- bestTarget._currentTimecode = activeItem.presStartTime;
+ if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) {
+ if (bestTarget._layout_currentTimecode !== activeItem.presStartTime) {
+ bestTarget._layout_currentTimecode = activeItem.presStartTime;
changed = true;
}
}
- if (pinDataTypes.scrollable) {
- if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
- bestTarget._scrollTop = activeItem.presPinViewScroll;
+ if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.presFillColor !== undefined || activeItem.color !== undefined))) {
+ if (bestTarget.fillColor !== activeItem.presFillColor) {
+ Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor;
+ changed = true;
+ }
+ if (bestTarget.color !== activeItem.presColor) {
+ Doc.GetProto(bestTarget).color = activeItem.presColor;
+ changed = true;
+ }
+ if (bestTarget.width !== activeItem.width) {
+ bestTarget._width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+ changed = true;
+ }
+ if (bestTarget.height !== activeItem.height) {
+ bestTarget._height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+ changed = true;
+ }
+ }
+ if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
+ if (bestTarget._type_collection !== activeItem.presViewType) {
+ bestTarget._type_collection = activeItem.presViewType;
+ changed = true;
+ }
+ }
+
+ if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) {
+ if (bestTarget.docFilters !== activeItem.presDocFilters) {
+ bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]);
+ changed = true;
+ }
+ }
+
+ if ((pinDataTypes?.pivot && activeItem.presPivotField !== undefined) || (!pinDataTypes && activeItem.presPivotField !== undefined)) {
+ if (bestTarget.pivotField !== activeItem.presPivotField) {
+ bestTarget.pivotField = activeItem.presPivotField;
+ bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView
+ changed = true;
+ }
+ }
+ if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
+ changed = true;
+ }
+
+ if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
+ if (bestTarget._layout_scrollTop !== activeItem.presViewScroll) {
+ bestTarget._layout_scrollTop = activeItem.presViewScroll;
changed = true;
const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
if (contentBounds) {
@@ -378,59 +515,72 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
}
- if (pinDataTypes.dataannos) {
- const fkey = Doc.LayoutFieldKey(bestTarget);
- Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]);
- }
- if (pinDataTypes.dataview && activeItem.presData !== undefined) {
+ if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
const fkey = Doc.LayoutFieldKey(bestTarget);
- Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
- bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
+ const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered);
+ const newItems = DocListCast(activeItem.presAnnotations).map(doc => {
+ doc.hidden = false;
+ return doc;
+ });
+ const hiddenItems = DocListCast(bestTarget[fkey + '_annotations'])
+ .filter(doc => !doc.layout_unrendered && !newItems.includes(doc))
+ .map(doc => {
+ doc.hidden = true;
+ return doc;
+ });
+ const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]);
+ Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList;
}
- if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
- if (pinDataTypes.poslayoutview) {
+ if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) {
changed = true;
+ const layoutField = Doc.LayoutFieldKey(bestTarget);
+ const transitioned = new Set<Doc>();
StrListCast(activeItem.presPinLayoutData)
- .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
- .forEach(data => {
- const doc = DocServer.GetCachedRefField(data.id) as Doc;
- doc._dataTransition = `all ${transTime}ms`;
- doc.x = data.x;
- doc.y = data.y;
- doc._width = data.w;
- doc._height = data.h;
+ .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string })
+ .forEach(async data => {
+ const doc = DocCast(DocServer.GetCachedRefField(data.id));
+ if (doc) {
+ transitioned.add(doc);
+ const field = !data.data ? undefined : await SerializationHelper.Deserialize(data.data);
+ const tfield = !data.text ? undefined : await SerializationHelper.Deserialize(data.text);
+ doc._dataTransition = `all ${transTime}ms`;
+ doc.x = data.x;
+ doc.y = data.y;
+ data.back && (doc._backgroundColor = data.back);
+ data.fill && (doc._fillColor = data.fill);
+ doc._width = data.w;
+ doc._height = data.h;
+ data.data && (Doc.GetProto(doc).data = field);
+ data.text && (Doc.GetProto(doc).text = tfield);
+ Doc.AddDocToList(Doc.GetProto(bestTarget), layoutField, doc);
+ }
});
- setTimeout(
- () =>
- StrListCast(activeItem.presPinLayoutData)
- .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
- .forEach(action(data => ((DocServer.GetCachedRefField(data.id) as Doc)._dataTransition = undefined))),
- transTime + 10
- );
+ setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
}
- if (pinDataTypes.pannable) {
+ if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) {
const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
if (contentBounds) {
const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
- bestTarget._panX = viewport.panX;
- bestTarget._panY = viewport.panY;
+ bestTarget._freeform_panX = viewport.panX;
+ bestTarget._freeform_panY = viewport.panY;
const dv = DocumentManager.Instance.getDocumentView(bestTarget);
if (dv) {
+ changed = true;
const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
- activeItem.presMovement === PresMovement.Zoom && (bestTarget._viewScale = computedScale);
+ activeItem.presMovement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale);
dv.ComponentView?.brushView?.(viewport);
}
} else {
- if (bestTarget._panX !== activeItem.presPinViewX || bestTarget._panY !== activeItem.presPinViewY || bestTarget._viewScale !== activeItem.presPinViewScale) {
- bestTarget._panX = activeItem.presPinViewX;
- bestTarget._panY = activeItem.presPinViewY;
- bestTarget._viewScale = activeItem.presPinViewScale;
+ if (bestTarget._freeform_panX !== activeItem.presPanX || bestTarget._freeform_panY !== activeItem.presPanY || bestTarget._freeform_scale !== activeItem.presViewScale) {
+ bestTarget._freeform_panX = activeItem.presPanX ?? bestTarget._freeform_panX;
+ bestTarget._freeform_panY = activeItem.presPanY ?? bestTarget._freeform_panY;
+ bestTarget._freeform_scale = activeItem.presViewScale ?? bestTarget._freeform_scale;
changed = true;
}
}
}
if (changed) {
- return bestTargetView.setViewTransition('all', transTime);
+ return bestTargetView?.setViewTransition('all', transTime);
}
}
@@ -443,7 +593,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
pinDoc.presPinLayout = true;
pinDoc.presX = NumCast(targetDoc.x);
pinDoc.presY = NumCast(targetDoc.y);
- pinDoc.presRot = NumCast(targetDoc.rotation);
+ pinDoc.presRotation = NumCast(targetDoc.rotation);
pinDoc.presWidth = NumCast(targetDoc.width);
pinDoc.presHeight = NumCast(targetDoc.height);
}
@@ -453,32 +603,63 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
pinProps.pinData.scrollable ||
pinProps.pinData.temporal ||
pinProps.pinData.pannable ||
+ pinProps.pinData.type_collection ||
pinProps.pinData.clippable ||
+ pinProps.pinData.datarange ||
pinProps.pinData.dataview ||
- pinProps.pinData.textview ||
pinProps.pinData.poslayoutview ||
pinProps?.activeFrame !== undefined;
+ const fkey = Doc.LayoutFieldKey(targetDoc);
if (pinProps.pinData.dataview) {
- const fkey = Doc.LayoutFieldKey(targetDoc);
- pinDoc.presUseAlt = targetDoc[fkey + '-useAlt'];
+ pinDoc.presUsePath = targetDoc[fkey + '_usePath'];
pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
}
if (pinProps.pinData.dataannos) {
const fkey = Doc.LayoutFieldKey(targetDoc);
- pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered));
+ pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered));
+ }
+ if (pinProps.pinData.inkable) {
+ pinDoc.presFillColor = targetDoc.fillColor;
+ pinDoc.presColor = targetDoc.color;
+ pinDoc.presWidth = targetDoc._width;
+ pinDoc.presHeight = targetDoc._height;
+ }
+ if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._layout_scrollTop;
+ if (pinProps.pinData.clippable) {
+ const fkey = Doc.LayoutFieldKey(targetDoc);
+ pinDoc.presClipWidth = targetDoc[fkey + '_clipWidth'];
+ }
+ if (pinProps.pinData.datarange) {
+ pinDoc.presXRange = undefined; //targetDoc?.xrange;
+ pinDoc.presYRange = undefined; //targetDoc?.yrange;
}
- if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
- if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
- if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
- if (pinProps.pinData.poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
+ if (pinProps.pinData.poslayoutview)
+ pinDoc.presPinLayoutData = new List<string>(
+ DocListCast(targetDoc[fkey] as ObjectField).map(d =>
+ JSON.stringify({
+ id: d[Id],
+ x: NumCast(d.x),
+ y: NumCast(d.y),
+ w: NumCast(d._width),
+ h: NumCast(d._height),
+ fill: StrCast(d._fillColor),
+ back: StrCast(d._backgroundColor),
+ data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''),
+ text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''),
+ })
+ )
+ );
+ if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection;
+ if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
+ if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
if (pinProps.pinData.pannable) {
- pinDoc.presPinViewX = NumCast(targetDoc._panX);
- pinDoc.presPinViewY = NumCast(targetDoc._panY);
- pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
+ pinDoc.presPanX = NumCast(targetDoc._freeform_panX);
+ pinDoc.presPanY = NumCast(targetDoc._freeform_panY);
+ pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1);
}
if (pinProps.pinData.temporal) {
- pinDoc.presStartTime = targetDoc._currentTimecode;
- const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(targetDoc.presStartTime) + 0.1);
+ pinDoc.presStartTime = targetDoc._layout_currentTimecode;
+ const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.presStartTime) + 0.1);
pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration);
}
}
@@ -486,9 +667,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// If pinWithView option set then update scale and x / y props of slide
const bounds = pinProps.pinViewport;
pinDoc.presPinView = true;
- pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
- pinDoc.presPinViewX = bounds.left + bounds.width / 2;
- pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+ pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1);
+ pinDoc.presPanX = bounds.left + bounds.width / 2;
+ pinDoc.presPanY = bounds.top + bounds.height / 2;
pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
}
}
@@ -522,62 +703,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
finished();
});
- const createDocView = (doc: Doc, finished?: () => void) => {
- DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
- LightboxView.AddDocTab(doc, OpenWhere.lightbox);
- };
- PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, resetSelection);
+ PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
};
- static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, finished?: () => void) {
+ static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
(DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
return;
}
+ const effect = activeItem.presEffect && activeItem.presEffect !== PresEffect.None ? activeItem.presEffect : undefined;
+ const presTime = NumCast(activeItem.presTransition, effect ? 750 : 500);
const options: DocFocusOptions = {
willPan: activeItem.presMovement !== PresMovement.None,
- willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
+ willZoomCentered: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1),
- zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+ zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime),
effect: activeItem,
noSelect: true,
- originatingDoc: activeItem,
+ openLocation: OpenWhere.addLeft,
+ anchorDoc: activeItem,
easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
zoomTextSelections: BoolCast(activeItem.presZoomText),
playAudio: BoolCast(activeItem.presPlayAudio),
};
- const restoreLayout = () => {
- // After navigating to the document, if it is added as a presPinView then it will
- // adjust the pan and scale to that of the pinView when it was added.
- const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
- if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
- // targetDoc may or may not be displayed. so get the first available document (or alias) view that matches targetDoc and use it
- PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
- }
- };
- const finishAndRestoreLayout = () => {
- finished?.();
- restoreLayout();
- };
- const containerDocContext = DocumentManager.GetContextPath(targetDoc);
-
- let context = containerDocContext.length ? containerDocContext[0] : targetDoc;
if (activeItem.presOpenInLightbox) {
- if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(DocCast(targetDoc.annotationOn) ?? targetDoc))) {
- context = DocCast(targetDoc.annotationOn) ?? targetDoc;
- LightboxView.SetLightboxDoc(context); // openInTab(targetDoc);
+ const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
+ if (!DocumentManager.Instance.getLightboxDocumentView(context)) {
+ LightboxView.SetLightboxDoc(context);
}
}
if (targetDoc) {
if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
- if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(context.annotationOn) ?? context)) {
+ // if target or the doc it annotates is not in the lightbox, then close the lightbox
+ if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
LightboxView.SetLightboxDoc(undefined);
}
- DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finishAndRestoreLayout);
+ DocumentManager.Instance.showDocument(targetDoc, options, finished);
});
- } else finishAndRestoreLayout();
+ } else finished?.();
}
/**
@@ -585,59 +750,90 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* they are hidden each time the presentation is updated.
*/
@action
- onHideDocument = () => {
+ doHideBeforeAfter = () => {
this.childDocs.forEach((doc, index) => {
const curDoc = Cast(doc, Doc, null);
- const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
+ const tagDoc = PresBox.targetRenderedDoc(curDoc);
const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
+ let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
if (curDoc.presHide) {
if (index !== this.itemIndex) {
- tagDoc.opacity = 1;
+ opacity = 1;
}
}
- const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex);
+ const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement();
if (curDoc.presHideBefore && index === hidingIndBef) {
if (index > this.itemIndex) {
- tagDoc.opacity = 0;
+ opacity = 0;
} else if (index === this.itemIndex || !curDoc.presHideAfter) {
- tagDoc.opacity = 1;
+ opacity = 1;
+ setTimeout(() => (tagDoc._dataTransition = undefined), 1000);
}
}
- const hidingIndAft = itemIndexes
- .slice()
- .reverse()
- .find(item => item < this.itemIndex);
+ const hidingIndAft =
+ itemIndexes
+ .slice()
+ .reverse()
+ .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement();
if (curDoc.presHideAfter && index === hidingIndAft) {
if (index < this.itemIndex) {
- tagDoc.opacity = 0;
+ opacity = 0;
} else if (index === this.itemIndex || !curDoc.presHideBefore) {
- tagDoc.opacity = 1;
+ opacity = 1;
}
}
const hidingInd = itemIndexes.find(item => item === this.itemIndex);
if (curDoc.presHide && index === hidingInd) {
if (index === this.itemIndex) {
- tagDoc.opacity = 0;
+ opacity = 0;
}
}
+ opacity !== undefined && (tagDoc.opacity = opacity);
});
};
_exitTrail: Opt<() => void>;
PlayTrail = (docs: Doc[]) => {
- const savedStates = docs.map(doc => (doc._viewType !== CollectionViewType.Freeform ? undefined : { c: doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) }));
+ const savedStates = docs.map(doc => {
+ switch (doc.type) {
+ case DocumentType.COL:
+ if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
+ break;
+ case DocumentType.INK:
+ if (doc.data instanceof InkField) {
+ return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) };
+ }
+ }
+ return undefined;
+ });
this.startPresentation(0);
this._exitTrail = () => {
savedStates
.filter(savedState => savedState)
.map(savedState => {
- const { x, y, s, c } = savedState!;
- c._panX = x;
- c._panY = y;
- c._viewScale = s;
+ switch (savedState?.type) {
+ case CollectionViewType.Freeform:
+ {
+ const { x, y, s, doc } = savedState!;
+ doc._freeform_panX = x;
+ doc._freeform_panY = y;
+ doc._freeform_scale = s;
+ }
+ break;
+ case DocumentType.INK:
+ {
+ const { data, fillColor, color, x, y, doc } = savedState!;
+ doc.x = x;
+ doc.y = y;
+ doc.data = data;
+ doc.fillColor = fillColor;
+ doc.color = color;
+ }
+ break;
+ }
});
LightboxView.SetLightboxDoc(undefined);
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ Doc.RemFromMyOverlay(this.rootDoc);
return PresStatus.Edit;
};
};
@@ -656,7 +852,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//stops the presentaton.
resetPresentation = () => {
this.childDocs
- .map(doc => Cast(doc.presentationTargetDoc, Doc, null))
+ .map(doc => PresBox.targetRenderedDoc(doc))
.filter(doc => doc instanceof Doc)
.forEach(doc => {
try {
@@ -682,21 +878,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
};
+ initializePresState = (startIndex: number) => {
+ this.childDocs.forEach((doc, index) => {
+ const tagDoc = PresBox.targetRenderedDoc(doc);
+ if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0;
+ if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0;
+ if (doc.presIndexed !== undefined && index >= startIndex) {
+ const startInd = NumCast(doc.presIndexedStart);
+ this.progressivizedItems(doc)
+ ?.slice(startInd)
+ .forEach(indexedDoc => (indexedDoc.opacity = 0));
+ doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd);
+ }
+ // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0;
+ });
+ };
+
/**
* The function that starts the presentation at the given index, also checking if actions should be applied
* directly at start.
* @param startIndex: index that the presentation will start at
*/
+ @action
startPresentation = (startIndex: number) => {
+ PresBox.Instance = this;
clearTimeout(this._presTimer);
if (this.childDocs.length) {
this.layoutDoc.presStatus = PresStatus.Autoplay;
- this.childDocs.forEach(doc => {
- const tagDoc = doc.presentationTargetDoc as Doc;
- if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) tagDoc.opacity = 0;
- if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) tagDoc.opacity = 0;
- // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0;
- });
+ this.initializePresState(startIndex);
const func = () => {
const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition);
this._presTimer = setTimeout(() => {
@@ -713,14 +922,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
@action
enterMinimize = () => {
+ this.updateCurrentPresentation(this.rootDoc);
clearTimeout(this._presTimer);
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
this.props.removeDocument?.(this.layoutDoc);
return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]);
};
exitMinimize = () => {
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
+ Doc.RemFromMyOverlay(this.rootDoc);
CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
}
return PresStatus.Edit;
@@ -732,7 +942,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc.overlayY = pt[1];
doc._height = 30;
doc._width = PresBox.minimizedWidth;
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+ Doc.AddToMyOverlay(doc);
+ PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
return (doc.presStatus = PresStatus.Manual);
}
@@ -743,11 +954,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
viewChanged = action((e: React.ChangeEvent) => {
//@ts-ignore
- const viewType = e.target.selectedOptions[0].value as CollectionViewType;
- this.layoutDoc.presFieldKey = this.fieldKey + (viewType === CollectionViewType.Tree ? '-linearized' : '');
+ const type_collection = e.target.selectedOptions[0].value as CollectionViewType;
+ this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : '');
// pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
- this.rootDoc._viewType = viewType;
+ [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.rootDoc._pivotField = undefined);
+ this.rootDoc._type_collection = type_collection;
if (this.isTreeOrStack) {
this.layoutDoc._gridGap = 0;
}
@@ -801,17 +1012,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return false;
}
} else {
- if (!doc.aliasOf) {
- const original = Doc.MakeAlias(doc);
- TabDocView.PinDoc(original, {});
- setTimeout(() => this.removeDocument(doc), 0);
- return false;
- } else {
- if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide';
- doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf);
- doc.presMovement = PresMovement.Zoom;
- if (this._expandBoolean) doc.presExpandInlineButton = true;
- }
+ if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide';
+ doc.presentationTargetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else.
+ doc.presMovement = PresMovement.Zoom;
+ if (this._expandBoolean) doc.presExpandInlineButton = true;
}
});
return true;
@@ -820,8 +1024,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
- isContentActive = (outsideReaction?: boolean) =>
- Doc.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
+ isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction);
+ //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
/**
* For sorting the array so that the order is maintained when it is dropped.
@@ -864,14 +1068,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
presDocView && SelectionManager.SelectView(presDocView, false);
};
+ focusElement = (doc: Doc, options: DocFocusOptions) => {
+ this.selectElement(doc);
+ return undefined;
+ };
+
//Regular click
@action
- selectElement = async (doc: Doc) => {
- CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
- this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
- // if (doc.presPinView) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
- // else
- this.updateCurrentPresentation(DocCast(doc.context));
+ selectElement = (doc: Doc, noNav = false) => {
+ CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.());
+ if (noNav) {
+ const index = this.childDocs.indexOf(doc);
+ if (index >= 0 && index < this.childDocs.length) {
+ this.rootDoc._itemIndex = index;
+ }
+ } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
+ this.updateCurrentPresentation(DocCast(doc.embedContainer));
};
//Command click
@@ -906,19 +1118,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//regular click
@action
- regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true) => {
+ regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => {
this.clearSelectedArray();
this.addToSelectedArray(doc);
this._eleArray.splice(0, this._eleArray.length, ref);
this._dragArray.splice(0, this._dragArray.length, drag);
- focus && this.selectElement(doc);
+ this.selectElement(doc, noNav);
selectPres && this.selectPres();
};
- modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean) => {
+ modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, cmdClick: boolean, shiftClick: boolean) => {
if (cmdClick) this.multiSelect(doc, ref, drag);
else if (shiftClick) this.shiftSelect(doc, ref, drag);
- else this.regularSelect(doc, ref, drag, focus);
+ else this.regularSelect(doc, ref, drag, noNav);
};
static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e);
@@ -947,7 +1159,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
break;
case 'Escape':
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
this.exitClicked();
} else if (this.layoutDoc.presStatus === PresStatus.Edit) {
this.clearSelectedArray();
@@ -1025,7 +1237,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
.filter(doc => Cast(doc.presentationTargetDoc, Doc, null))
.forEach((doc, index) => {
const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
- const srcContext = Cast(tagDoc.context, Doc, null);
+ const srcContext = Cast(tagDoc.embedContainer, Doc, null);
const width = NumCast(tagDoc._width) / 10;
const height = Math.max(NumCast(tagDoc._height) / 10, 15);
const edge = Math.max(width, height);
@@ -1059,15 +1271,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
} else if (doc.presPinView && presCollection === tagDoc && dv) {
// Case B: Document is presPinView and is presCollection
- const scale: number = 1 / NumCast(doc.presPinViewScale);
+ const scale: number = 1 / NumCast(doc.presViewScale);
const height: number = dv.props.PanelHeight() * scale;
const width: number = dv.props.PanelWidth() * scale;
const indWidth = width / 10;
const indHeight = Math.max(height / 10, 15);
const indEdge = Math.max(indWidth, indHeight);
const indFontSize = indEdge * 0.8;
- const xLoc: number = NumCast(doc.presPinViewX) - width / 2;
- const yLoc: number = NumCast(doc.presPinViewY) - height / 2;
+ const xLoc: number = NumCast(doc.presPanX) - width / 2;
+ const yLoc: number = NumCast(doc.presPanY) - height / 2;
docs.push(tagDoc);
order.push(
<>
@@ -1085,7 +1297,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/**
* Method called for viewing paths which adds a single line with
* points at the center of each document added.
- * Design choice: When this is called it sets _fitContentsToBox as true so the
+ * Design choice: When this is called it sets _freeform_fitContentsToBox as true so the
* user can have an overview of all of the documents in the collection.
* (Design needed for when documents in presentation trail are in another
* collection)
@@ -1100,8 +1312,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if ((index = 0)) pathPoints = n1x + ',' + n1y;
else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
} else if (doc.presPinView) {
- const n1x = NumCast(doc.presPinViewX);
- const n1y = NumCast(doc.presPinViewY);
+ const n1x = NumCast(doc.presPanX);
+ const n1y = NumCast(doc.presPanY);
if ((index = 0)) pathPoints = n1x + ',' + n1y;
else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
}
@@ -1133,12 +1345,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (timeInMS > 100000) timeInMS = 100000;
setter(timeInMS);
};
- setTransitionTime = (number: String, change?: number) => {
+
+ @undoBatch
+ updateTransitionTime = (number: String, change?: number) => {
PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
};
// Converts seconds to ms and updates presTransition
- setZoom = (number: String, change?: number) => {
+ @undoBatch
+ updateZoom = (number: String, change?: number) => {
let scale = Number(number) / 100;
if (change) scale += change;
if (scale < 0.01) scale = 0.01;
@@ -1146,8 +1361,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.selectedArray.forEach(doc => (doc.presZoom = scale));
};
- // Converts seconds to ms and updates presDuration
- setDurationTime = (number: String, change?: number) => {
+ /*
+ * Converts seconds to ms and updates presDuration
+ */
+ @undoBatch
+ updateDurationTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
@@ -1155,9 +1373,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.selectedArray.forEach(doc => (doc.presDuration = timeInMS));
};
- /**
- * When the movement dropdown is changes
- */
@undoBatch
updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement)));
@@ -1188,6 +1403,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox;
this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox));
};
+
@undoBatch
@action
updateEaseFunc = (activeItem: Doc) => {
@@ -1201,12 +1417,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
- updateEffect = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffect = effect));
-
- _batch: UndoManager.Batch | undefined = undefined;
+ updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presEffect = effect)));
+ static _sliderBatch: any;
public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
- let batch: any;
return (
<input
type="range"
@@ -1214,13 +1428,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
min={min}
max={max}
value={value}
+ readOnly={true}
style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
className={`toolbar-slider ${active ? '' : 'none'}`}
onPointerDown={e => {
- batch = UndoManager.StartBatch('pres slider');
+ PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
e.stopPropagation();
}}
- onPointerUp={() => batch?.end()}
+ onPointerUp={() => PresBox._sliderBatch.end()}
onChange={e => {
e.stopPropagation();
change(e.target.value);
@@ -1228,11 +1443,161 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
);
};
+
+ @undoBatch
+ @action
+ applyTo = (array: Doc[]) => {
+ this.updateMovement(this.activeItem.presMovement as PresMovement, true);
+ this.updateEffect(this.activeItem.presEffect as PresEffect, false, true);
+ this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
+ this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
+ const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
+ array.forEach(curDoc => {
+ curDoc.presTransition = presTransition;
+ curDoc.presDuration = presDuration;
+ curDoc.presHideBefore = presHideBefore;
+ curDoc.presHideAfter = presHideAfter;
+ });
+ };
+
+ @computed get visibiltyDurationDropdown() {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const targetType = this.targetDoc.type;
+ let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
+ if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
+ return (
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton">
+ <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
+ Hide before
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
+ Hide
+ </div>
+ </Tooltip>
+
+ <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
+ Hide after
+ </div>
+ </Tooltip>
+
+ <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
+ Lightbox
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
+ <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
+ {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
+ </div>
+ </Tooltip>
+ </div>
+ {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : (
+ <>
+ <div className="ribbon-doubleButton">
+ <div className="presBox-subheading">Slide Duration</div>
+ <div className="ribbon-property">
+ <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+ </div>
+ <div className="ribbon-propertyUpDown">
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
+ <FontAwesomeIcon icon={'caret-up'} />
+ </div>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}>
+ <FontAwesomeIcon icon={'caret-down'} />
+ </div>
+ </div>
+ </div>
+ {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
+ <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
+ <div className="slider-text">Short</div>
+ <div className="slider-text">Medium</div>
+ <div className="slider-text">Long</div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ }
+ }
+ @computed get progressivizeDropdown() {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
+ const bulletEffect = (effect: PresEffect) => (
+ <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, true)}>
+ {effect}
+ </div>
+ );
+ return (
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize Collection</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10 }}
+ type="checkbox"
+ onChange={() => {
+ activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined;
+ activeItem.presHideBefore = activeItem.presIndexed !== undefined;
+ const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
+ const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
+ activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
+ // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
+ // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
+ let dataField = Doc.LayoutFieldKey(tagDoc);
+ if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations';
+
+ if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`);
+ else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`);
+ }}
+ checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
+ />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize First Bullet</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presIndexedStart = activeItem.presIndexedStart ? 0 : 1)} checked={!NumCast(activeItem.presIndexedStart)} />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Expand Current Bullet</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} />
+ </div>
+
+ <div className="ribbon-box">
+ Bullet Effect
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ {effect?.toString()}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+ {bulletEffect(PresEffect.None)}
+ {bulletEffect(PresEffect.Fade)}
+ {bulletEffect(PresEffect.Flip)}
+ {bulletEffect(PresEffect.Rotate)}
+ {bulletEffect(PresEffect.Bounce)}
+ {bulletEffect(PresEffect.Roll)}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ return null;
+ }
@computed get transitionDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
+ const activeItem = this.activeItem;
const presEffect = (effect: PresEffect) => (
- <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}>
+ <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}>
{effect}
</div>
);
@@ -1253,14 +1618,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
);
};
- if (activeItem && targetDoc) {
- const type = targetDoc.type;
+ if (activeItem && this.targetDoc) {
const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
const zoom = NumCast(activeItem.presZoom, 1) * 100;
- let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
- if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
- // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
return (
<div
className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
@@ -1270,6 +1631,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
this._openMovementDropdown = false;
this._openEffectDropdown = false;
+ this._openBulletEffectDropdown = false;
})}>
<div className="ribbon-box">
Movement
@@ -1293,33 +1655,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Zoom (% screen filled)</div>
<div className="ribbon-property">
- <input className="presBox-input" type="number" value={zoom} onChange={action(e => this.setZoom(e.target.value))} />%
+ <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
</div>
<div className="ribbon-propertyUpDown">
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}>
<FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
+ {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)}
<div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Transition Speed</div>
+ <div className="presBox-subheading">Transition Time</div>
<div className="ribbon-property">
- <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setTransitionTime(e.target.value))} /> s
+ <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
</div>
<div className="ribbon-propertyUpDown">
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}>
<FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.setTransitionTime)}
+ {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)}
<div className={'slider-headers'}>
<div className="slider-text">Fast</div>
<div className="slider-text">Medium</div>
@@ -1327,62 +1689,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
<div className="ribbon-box">
- Visibility {'&'} Duration
- <div className="ribbon-doubleButton">
- <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
- Hide before
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
- Hide
- </div>
- </Tooltip>
-
- <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
- Hide after
- </div>
- </Tooltip>
-
- <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
- Lightbox
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
- <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
- {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
- </div>
- </Tooltip>
- </div>
- {type === DocumentType.AUDIO || type === DocumentType.VID ? null : (
- <>
- <div className="ribbon-doubleButton">
- <div className="presBox-subheading">Slide Duration</div>
- <div className="ribbon-property">
- <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setDurationTime(e.target.value))} /> s
- </div>
- <div className="ribbon-propertyUpDown">
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}>
- <FontAwesomeIcon icon={'caret-up'} />
- </div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}>
- <FontAwesomeIcon icon={'caret-down'} />
- </div>
- </div>
- </div>
- {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
- <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
- <div className="slider-text">Short</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Long</div>
- </div>
- </>
- )}
- </div>
- <div className="ribbon-box">
Effects
<div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
<div className="presBox-subheading">Play Audio Annotation</div>
@@ -1431,173 +1737,156 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
}
-
- @undoBatch
- @action
- applyTo = (array: Doc[]) => {
- this.updateMovement(this.activeItem.presMovement as PresMovement, true);
- this.updateEffect(this.activeItem.presEffect as PresEffect, true);
- this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
- const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
- array.forEach(curDoc => {
- curDoc.presTransition = presTransition;
- curDoc.presDuration = presDuration;
- curDoc.presHideBefore = presHideBefore;
- curDoc.presHideAfter = presHideAfter;
- });
- };
-
@computed get mediaOptionsDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const clipStart: number = NumCast(activeItem.clipStart);
- const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration']));
- const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
- if (activeItem && targetDoc) {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const clipStart = NumCast(activeItem.clipStart);
+ const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration']));
return (
- <div>
- <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div>
- <div className="ribbon-box">
- Start {'&'} End Time
- <div className={'slider-headers'}>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- Start time (s)
- </div>
- <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
- type="number"
- value={NumCast(activeItem.presStartTime).toFixed(2)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- activeItem.presStartTime = Number(e.target.value);
- })}
- />
- </div>
+ <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div>
+ <div className="ribbon-box">
+ Start {'&'} End Time
+ <div className={'slider-headers'}>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ Start time (s)
</div>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- Duration (s)
- </div>
- <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
- {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
- </div>
+ <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ className="presBox-input"
+ style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
+ type="number"
+ readOnly={true}
+ value={NumCast(activeItem.presStartTime).toFixed(2)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ activeItem.presStartTime = Number(e.target.value);
+ })}
+ />
</div>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- End time (s)
- </div>
- <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
- <input
- className="presBox-input"
- onKeyDown={e => e.stopPropagation()}
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
- type="number"
- value={NumCast(activeItem.presEndTime).toFixed(2)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- activeItem.presEndTime = Number(e.target.value);
- })}
- />
- </div>
- </div>
- </div>
- <div className="multiThumb-slider">
- <input
- type="range"
- step="0.1"
- min={clipStart}
- max={clipEnd}
- value={NumCast(activeItem.presEndTime)}
- style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${'end'}`}
- id="toolbar-slider"
- onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presEndTime');
- const endBlock = document.getElementById('endTime');
- if (endBlock) {
- endBlock.style.color = Colors.LIGHT_GRAY;
- endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
- }
- e.stopPropagation();
- }}
- onPointerUp={() => {
- this._batch?.end();
- const endBlock = document.getElementById('endTime');
- if (endBlock) {
- endBlock.style.color = Colors.BLACK;
- endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
- }
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- activeItem.presEndTime = Number(e.target.value);
- }}
- />
- <input
- type="range"
- step="0.1"
- min={clipStart}
- max={clipEnd}
- value={NumCast(activeItem.presStartTime)}
- style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${'start'}`}
- id="toolbar-slider"
- onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presStartTime');
- const startBlock = document.getElementById('startTime');
- if (startBlock) {
- startBlock.style.color = Colors.LIGHT_GRAY;
- startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
- }
- e.stopPropagation();
- }}
- onPointerUp={() => {
- this._batch?.end();
- const startBlock = document.getElementById('startTime');
- if (startBlock) {
- startBlock.style.color = Colors.BLACK;
- startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
- }
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- activeItem.presStartTime = Number(e.target.value);
- }}
- />
</div>
- <div className="slider-headers">
- <div className="slider-text">{clipStart.toFixed(2)} s</div>
- <div className="slider-text"></div>
- <div className="slider-text">{clipEnd.toFixed(2)} s</div>
- </div>
- </div>
- <div className="ribbon-final-box">
- Playback
- <div className="presBox-subheading">Start playing:</div>
- <div className="presBox-radioButtons">
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
- <div>On click</div>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ Duration (s)
</div>
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
- <div>Automatically</div>
+ <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
+ {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
</div>
</div>
- <div className="presBox-subheading">Stop playing:</div>
- <div className="presBox-radioButtons">
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
- <div>At audio end time</div>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ End time (s)
</div>
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
- <div>On slide change</div>
+ <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ className="presBox-input"
+ onKeyDown={e => e.stopPropagation()}
+ style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
+ type="number"
+ readOnly={true}
+ value={NumCast(activeItem.presEndTime).toFixed(2)}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ activeItem.presEndTime = Number(e.target.value);
+ })}
+ />
</div>
- {/* <div className="checkbox-container">
+ </div>
+ </div>
+ <div className="multiThumb-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presEndTime)}
+ style={{ gridColumn: 1, gridRow: 1 }}
+ className={`toolbar-slider ${'end'}`}
+ id="toolbar-slider"
+ onPointerDown={e => {
+ this._batch = UndoManager.StartBatch('presEndTime');
+ const endBlock = document.getElementById('endTime');
+ if (endBlock) {
+ endBlock.style.color = Colors.LIGHT_GRAY;
+ endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ }
+ e.stopPropagation();
+ }}
+ onPointerUp={() => {
+ this._batch?.end();
+ const endBlock = document.getElementById('endTime');
+ if (endBlock) {
+ endBlock.style.color = Colors.BLACK;
+ endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ }
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ activeItem.presEndTime = Number(e.target.value);
+ }}
+ />
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presStartTime)}
+ style={{ gridColumn: 1, gridRow: 1 }}
+ className={`toolbar-slider ${'start'}`}
+ id="toolbar-slider"
+ onPointerDown={e => {
+ this._batch = UndoManager.StartBatch('presStartTime');
+ const startBlock = document.getElementById('startTime');
+ if (startBlock) {
+ startBlock.style.color = Colors.LIGHT_GRAY;
+ startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ }
+ e.stopPropagation();
+ }}
+ onPointerUp={() => {
+ this._batch?.end();
+ const startBlock = document.getElementById('startTime');
+ if (startBlock) {
+ startBlock.style.color = Colors.BLACK;
+ startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ }
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ activeItem.presStartTime = Number(e.target.value);
+ }}
+ />
+ </div>
+ <div className="slider-headers">
+ <div className="slider-text">{clipStart.toFixed(2)} s</div>
+ <div className="slider-text"></div>
+ <div className="slider-text">{clipEnd.toFixed(2)} s</div>
+ </div>
+ </div>
+ <div className="ribbon-final-box">
+ Playback
+ <div className="presBox-subheading">Start playing:</div>
+ <div className="presBox-radioButtons">
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
+ <div>On click</div>
+ </div>
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
+ <div>Automatically</div>
+ </div>
+ </div>
+ <div className="presBox-subheading">Stop playing:</div>
+ <div className="presBox-radioButtons">
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
+ <div>At audio end time</div>
+ </div>
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
+ <div>On slide change</div>
+ </div>
+ {/* <div className="checkbox-container">
<input className="presBox-checkbox"
type="checkbox"
onChange={() => activeItem.mediaStop = "afterSlide"}
@@ -1614,7 +1903,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</select>
</div>
</div> */}
- </div>
</div>
</div>
</div>
@@ -1622,7 +1910,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
}
-
@computed get newDocumentToolbarDropdown() {
return (
<div
@@ -1790,10 +2077,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, _fitContentsToBox: true, x, y });
- case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
- case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
- case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y })
+ case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
+ case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
+ case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
+ case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y })
}
};
@@ -1817,6 +2104,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onClick={undoBatch(
action(() => {
this.layoutDoc.presStatus = 'manual';
+ this.initializePresState(this.itemIndex);
this.turnOffEdit(true);
this.gotoDocument(this.itemIndex, this.activeItem);
})
@@ -1827,71 +2115,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
- scrollFocus = () => {
- // this.gotoDocument(0);
- // this.startOrPause(false);
- return undefined;
- };
-
- _keyTimer: NodeJS.Timeout | undefined;
-
- /**
- * Returns the collection type as a string for headers
- */
- @computed get stringType() {
- if (this.activeItem) {
- // prettier-ignore
- switch (this.targetDoc.type) {
- case DocumentType.PDF: return 'PDF';
- case DocumentType.RTF: return 'Text node';
- case DocumentType.COL: return 'Collection';
- case DocumentType.AUDIO: return 'Audio';
- case DocumentType.VID: return 'Video';
- case DocumentType.IMG: return 'Image';
- case DocumentType.WEB: return 'Web page';
- case DocumentType.MAP: return 'Map';
- default: return 'Other node';
- }
- }
- return '';
- }
-
- @observable private openActiveColorPicker: boolean = false;
- @observable private openViewedColorPicker: boolean = false;
-
- @undoBatch
- @action
- switchActive = (color: ColorState) => {
- this.targetDoc['pres-text-color'] = String(color.hex);
- return true;
- };
- @undoBatch
- @action
- switchPresented = (color: ColorState) => {
- this.targetDoc['pres-text-viewed-color'] = String(color.hex);
- return true;
- };
-
- @computed get activeColorPicker() {
- return !this.openActiveColorPicker ? null : (
- <SketchPicker
- onChange={this.switchActive}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(this.targetDoc['pres-text-color'])}
- />
- );
- }
-
- @computed get viewedColorPicker() {
- return !this.openViewedColorPicker ? null : (
- <SketchPicker
- onChange={this.switchPresented}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(this.targetDoc['pres-text-viewed-color'])}
- />
- );
- }
-
@action
turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths
@@ -1906,11 +2129,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get toolbar() {
const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
const activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
- return mode === CollectionViewType.Carousel3D ? null : (
+ return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : (
<div id="toolbarContainer" className={'presBox-toolbar'}>
{/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
<FontAwesomeIcon icon={"plus"} />
@@ -1927,14 +2150,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{isMini ? null : (
<>
<div className="toolbar-divider" />
- {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}>
- <div className={"toolbar-button"}
- style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }}
- onClick={this.toggleExpandMode}>
- <FontAwesomeIcon icon={"eye"} />
- </div>
- </Tooltip>
- <div className="toolbar-divider" /> */}
<Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}>
<div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
<FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
@@ -1957,11 +2172,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* presentPanel: The button to start the presentation / open minimized view of the presentation
*/
@computed get topPanel() {
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
return (
- <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+ <div className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
{isMini ? null : (
<select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
<option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
@@ -1984,6 +2198,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onClick={undoBatch(() => {
if (this.childDocs.length) {
this.layoutDoc.presStatus = 'manual';
+ this.initializePresState(this.itemIndex);
this.gotoDocument(this.itemIndex, this.activeItem);
}
})}>
@@ -2008,9 +2223,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get playButtons() {
- const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+ const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
+ const inOverlay = Doc.IsInMyOverlay(this.rootDoc);
// Case 1: There are still other frames and should go through all frames before going to next slide
return (
<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
@@ -2096,7 +2311,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
<div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
- {`${inOverlay ? '' : 'Slide'} ${this.itemIndex + 1} / ${this.childDocs.length}`}
+ {inOverlay ? '' : 'Slide'} {this.itemIndex + 1}
+ {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider"></div>
{this.props.PanelWidth() > 250 ? (
@@ -2122,7 +2338,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
startOrPause = (makeActive = true) => {
makeActive && this.updateCurrentPresentation();
- if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex);
+ if (!this.layoutDoc.presStatus || this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex);
else this.pauseAutoPres();
};
@@ -2143,6 +2359,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc.presStatus = PresStatus.Manual;
}
};
+
@undoBatch
@action
exitClicked = () => {
@@ -2150,7 +2367,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
clearTimeout(this._presTimer);
};
- AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ AddToMap = (treeViewDoc: Doc, index: number[]) => {
+ if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
var indexNum = 0;
for (let i = 0; i < index.length; i++) {
indexNum += index[i] * 10 ** -i;
@@ -2163,27 +2381,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs
}
}
- return this.childDocs;
};
- RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ RemFromMap = (treeViewDoc: Doc, index: number[]) => {
+ if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
if (!this._unmounting && this.isTree) {
this._treeViewMap.delete(treeViewDoc);
this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap));
}
- return this.childDocs;
};
- // TODO: [AL] implement sort function for an array of numbers (e.g. arr[1,2,4] v arr[1,2,1])
sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
render() {
// needed to ensure that the childDocs are loaded for looking up fields
this.childDocs.slice();
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
- const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
+ const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
<div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
<div
@@ -2216,7 +2431,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
<div className="presPanel-button-text">
- Slide {this.itemIndex + 1} / {this.childDocs.length}
+ Slide {this.itemIndex + 1}
+ {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider" />
<div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
@@ -2225,7 +2441,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
) : (
- <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}>
+ <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}>
{this.topPanel}
{this.toolbar}
{this.newDocumentToolbarDropdown}
@@ -2234,20 +2450,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{mode !== CollectionViewType.Invalid ? (
<CollectionView
{...this.props}
- ContainingCollectionDoc={this.props.Document}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.panelHeight}
childIgnoreNativeSize={true}
moveDocument={returnFalse}
ignoreUnrendered={true}
- //childFitWidth={returnTrue}
+ //childLayoutFitWidth={returnTrue}
childOpacity={returnOne}
+ //childLayoutString={PresElementBox.LayoutString('data')}
+ childClickScript={PresBox.navigateToDocScript}
childLayoutTemplate={this.childLayoutTemplate}
childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
filterAddDocument={this.addDocumentFilter}
removeDocument={returnFalse}
dontRegisterView={true}
- focus={this.selectElement}
+ focus={this.focusElement}
scriptContext={this}
ScreenToLocalTransform={this.getTransform}
AddToMap={this.AddToMap}
@@ -2269,15 +2486,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
);
}
- static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
- const openInTab = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
- finished?.();
- };
- PresBox.NavigateToTarget(bestTarget, activeItem, openInTab);
- }
}
ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
- PresBox.NavigateToDoc(bestTarget, activeItem);
+ PresBox.NavigateToTarget(bestTarget, activeItem);
});
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 415253af1..4f95f0c1f 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -44,6 +44,12 @@ $slide-active: #5b9fdd;
display: flex;
align-items: center;
+ .presItem-number {
+ cursor: pointer;
+ &:hover {
+ background-color: $light-blue;
+ }
+ }
.presItem-name {
display: flex;
min-width: 20px;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 788900b46..f197a8a8d 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,22 +1,19 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
-import { Copy, Id } from '../../../../fields/FieldSymbols';
-import { InkField } from '../../../../fields/InkField';
+import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
import { Docs, DocUtils } from '../../../documents/Documents';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { SettingsManager } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
-import { MarqueeView } from '../../collections/collectionFreeForm';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
@@ -42,7 +39,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Idea: this boolean will determine whether to automatically show the video when this preselement is selected.
// @observable static showVideo: boolean = false;
@computed get indexInPres() {
- return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc);
+ return DocListCast(this.presBox?.[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc);
} // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
@computed get expandViewHeight() {
return 100;
@@ -54,18 +51,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return this.presBoxView?.selectedArray;
}
@computed get presBoxView() {
- const vpath = this.props.docViewPath();
- return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined;
+ return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as PresBox;
}
@computed get presBox() {
- return this.props.ContainingCollectionDoc!;
+ return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc;
}
@computed get targetDoc() {
return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc;
}
componentDidMount() {
- this.layoutDoc.hideLinkButton = true;
+ this.layoutDoc.layout_hideLinkButton = true;
this._heightDisposer = reaction(
() => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }),
({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)),
@@ -100,7 +96,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
<div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}>
<DocumentView
- Document={this.rootDoc}
+ Document={PresBox.targetRenderedDoc(this.rootDoc)}
DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
@@ -113,14 +109,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
docFilters={this.props.docFilters}
docRangeFilters={this.props.docRangeFilters}
searchFilterDocs={this.props.searchFilterDocs}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
rootSelected={returnTrue}
addDocument={returnFalse}
removeDocument={returnFalse}
fitContentsToBox={returnTrue}
moveDocument={this.props.moveDocument!}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -139,7 +133,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
onClick={e => {
e.stopPropagation();
e.preventDefault();
- this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+ this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
this.presExpandDocumentClick();
}}>
<div className="presItem-groupNum">{`${ind + 1}.`}</div>
@@ -177,22 +171,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const element = e.target as any;
e.stopPropagation();
e.preventDefault();
- if (element && !(e.ctrlKey || e.metaKey)) {
- if (this.selectedArray?.has(this.rootDoc)) {
- this.selectedArray.size === 1 && this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
- setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
- } else {
- setupMoveUpEvents(
- this,
- e,
- (e: PointerEvent) => {
- this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
- return this.startDrag(e);
- },
- emptyFunction,
- emptyFunction
- );
- }
+ if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) {
+ this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true, false);
+ setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+ this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem);
+ });
}
};
@@ -206,7 +192,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []);
if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
dragData.dropAction = 'move';
- dragData.treeViewDoc = this.presBox._viewType === CollectionViewType.Tree ? this.props.ContainingCollectionDoc : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
+ dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
dragData.moveDocument = this.props.moveDocument;
const dragItem: HTMLElement[] = [];
if (dragArray.length === 1) {
@@ -277,16 +263,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @undoBatch
- removeItem = action((e: React.MouseEvent) => {
+ removePresentationItem = undoable((e: React.MouseEvent) => {
e.stopPropagation();
- if (this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
- this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1;
+ if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
+ runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1));
}
this.props.removeDocument?.(this.rootDoc);
this.presBoxView?.removeFromSelectedArray(this.rootDoc);
this.removeAllRecordingInOverlay();
- });
+ }, 'Remove doc from pres trail');
// set the value/title of the individual pres element
@undoBatch
@@ -304,60 +289,26 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
@undoBatch
@action
- updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => {
+ updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => {
+ const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
activeItem.presX = NumCast(targetDoc.x);
activeItem.presY = NumCast(targetDoc.y);
- activeItem.presRot = NumCast(targetDoc.rotation);
+ activeItem.presRotation = NumCast(targetDoc.rotation);
activeItem.presWidth = NumCast(targetDoc.width);
activeItem.presHeight = NumCast(targetDoc.height);
+ activeItem.presPinLayout = true;
};
/**
* Method called for updating the view of the currently selected document
*
- * @param targetDoc
+ * @param presTargetDoc
* @param activeItem
*/
@undoBatch
@action
- updateCapturedViewContents = (targetDoc: Doc, activeItem: Doc) => {
- switch (targetDoc.type) {
- case DocumentType.PDF:
- case DocumentType.WEB:
- case DocumentType.RTF:
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- if (targetDoc.type === DocumentType.RTF) {
- activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof RichTextField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as RichTextField)[Copy]() : targetDoc.text;
- }
- break;
- case DocumentType.INK:
- activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof InkField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as InkField)[Copy]() : targetDoc.data;
- break;
- case DocumentType.VID:
- case DocumentType.AUDIO:
- activeItem.presStartTime = targetDoc._currentTimecode;
- break;
- case DocumentType.COMPARISON:
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- break;
- case DocumentType.COL:
- activeItem.presPinLayoutData = new List<string>(DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
- default:
- const bestView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
- if (activeItem.presPinViewBounds && bestView) {
- const bounds = MarqueeView.CurViewBounds(targetDoc, bestView.props.PanelWidth(), bestView.props.PanelHeight());
- activeItem.presPinView = true;
- activeItem.presPinViewScale = NumCast(targetDoc._viewScale, 1);
- activeItem.presPinViewX = bounds.left + bounds.width / 2;
- activeItem.presPinViewY = bounds.top + bounds.height / 2;
- activeItem.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
- } else {
- activeItem.presPinViewX = targetDoc._panX;
- activeItem.presPinViewY = targetDoc._panY;
- activeItem.presPinViewScale = targetDoc._viewScale;
- }
- }
+ updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => {
+ const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
+ PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target);
};
@computed get recordingIsInOverlay() {
@@ -367,15 +318,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// a previously recorded video will have timecode defined
static videoIsRecorded = (activeItem: Opt<Doc>) => {
const casted = Cast(activeItem?.recording, Doc, null);
- return casted && 'currentTimecode' in casted;
+ return casted && 'layout_currentTimecode' in casted;
};
removeAllRecordingInOverlay = () => {
- DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
- if (doc.slides === this.rootDoc) {
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
- }
- });
+ DocListCast(Doc.MyOverlayDocs.data).filter(doc => doc.slides === this.rootDoc).forEach(Doc.RemFromMyOverlay);
};
static removeEveryExistingRecordingInOverlay = () => {
@@ -383,9 +330,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
if (doc.slides !== null) {
// if it's a recording video, don't remove from overlay (user can lose data)
- if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return;
-
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) {
+ Doc.RemFromMyOverlay(doc);
+ }
}
});
};
@@ -401,14 +348,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
showRecording = (activeItem: Doc, iconClick: boolean = false) => {
- // if (iconClick) PresElementBox.showVideo = true;
- // if (!PresElementBox.showVideo) return;
-
// remove the overlays on switch *IF* not opened from the specific icon
if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
if (activeItem.recording) {
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ Doc.AddToMyOverlay(DocCast(activeItem.recording));
}
};
@@ -420,7 +364,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// if we already have an existing recording
this.showRecording(activeItem, true);
// // if we already have an existing recording
- // Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null));
} else {
// Remove every recording that already exists in overlay view
// this is a design decision to clear to focus in on the recoding mode
@@ -431,10 +375,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
_width: 384,
_height: 216,
hideDocumentButtonBar: true,
- hideDecorationTitle: true,
+ layout_hideDecorationTitle: true,
hideOpenButton: true,
// hideDeleteButton: true,
- cloneFieldFilter: new List<string>(['system']),
+ cloneFieldFilter: new List<string>(['isSystem']),
});
// attach the recording to the slide, and attach the slide to the recording
@@ -444,54 +388,54 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// make recording box appear in the bottom right corner of the screen
recording.overlayX = window.innerWidth - recording[WidthSym]() - 20;
recording.overlayY = window.innerHeight - recording[HeightSym]() - 20;
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording);
+ Doc.AddToMyOverlay(recording);
}
};
@computed
get toolbarWidth(): number {
const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
- let width: number = NumCast(this.presBox._width);
+ let width: number = NumCast(this.presBox?._width);
if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
if (width === 0) width = 300;
return width;
}
@computed get presButtons() {
- const presBox: Doc = this.presBox; //presBox
- const presBoxColor: string = StrCast(presBox._backgroundColor);
+ const presBox = this.presBox; //presBox
+ const presBoxColor: string = StrCast(presBox?._backgroundColor);
const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
const targetDoc: Doc = this.targetDoc;
const activeItem: Doc = this.rootDoc;
const items: JSX.Element[] = [];
- if (activeItem.presPinLayout) {
- items.push(
- <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
- <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
- L
- </div>
- </Tooltip>
- );
- }
- if (activeItem.presPinData || activeItem.presPinView) {
- items.push(
- <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
- <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
- C
- </div>
- </Tooltip>
- );
- }
- if (!Doc.noviceMode) {
- items.push(
- <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
- <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- );
- }
+ items.push(
+ <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
+ <div
+ className="slideButton"
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedContainerLayout(targetDoc, activeItem), true)}
+ style={{ opacity: activeItem.presPinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+ L
+ </div>
+ </Tooltip>
+ );
+ items.push(
+ <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
+ <div
+ className="slideButton"
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedViewContents(targetDoc, activeItem))}
+ style={{ opacity: activeItem.presPinData || activeItem.presPinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+ C
+ </div>
+ </Tooltip>
+ );
+ items.push(
+ <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
+ <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ );
if (this.indexInPres !== 0) {
items.push(
<Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
@@ -527,7 +471,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
items.push(
<Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
- <div className={'slideButton'} onClick={this.removeItem}>
+ <div className={'slideButton'} onClick={this.removePresentationItem}>
<FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
@@ -537,10 +481,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get mainItem() {
const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false;
- const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres;
+ const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres;
const miniView: boolean = this.toolbarWidth <= 110;
- const presBox: Doc = this.presBox; //presBox
- const presBoxColor: string = StrCast(presBox._backgroundColor);
+ const presBox = this.presBox; //presBox
+ const presBoxColor: string = StrCast(presBox?._backgroundColor);
const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
const activeItem: Doc = this.rootDoc;
@@ -557,15 +501,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
}}
- onClick={e => {
- e.stopPropagation();
- e.preventDefault();
- this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
- this.showRecording(activeItem);
- }}
onDoubleClick={action(e => {
this.toggleProperties();
- this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+ this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
})}
onPointerOver={this.onPointerOver}
onPointerLeave={this.onPointerLeave}
@@ -592,8 +530,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
cursor: isSelected ? 'text' : 'grab',
}}>
- <div>{`${this.indexInPres + 1}. `}</div>
- <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
+ <div
+ className="presItem-number"
+ title="select without navigation"
+ style={{ pointerEvents: this.presBoxView?.isContentActive() ? 'all' : undefined }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ if (this._itemRef.current && this._dragRef.current) {
+ this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, true, false, false);
+ }
+ }}
+ onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div>
+ <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
</div>
{/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
{/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}