aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
authorljungster <parkerljung@gmail.com>2022-08-09 11:52:07 -0500
committerljungster <parkerljung@gmail.com>2022-08-09 11:52:07 -0500
commitda3cb00f809a482a9fdf732f6a656fbc467cce27 (patch)
tree9eb1fd278bc71d080d71bbfb7e3aec482d35f439 /src/client/views/nodes
parent1638527259a072dfc2ab286bd27bbb1751e8434e (diff)
parent26670c8b9eb6e2fd981c3a0997bff5556b60504b (diff)
Merge branch 'parker' of https://github.com/brown-dash/Dash-Web into parker
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/AudioBox.scss320
-rw-r--r--src/client/views/nodes/AudioBox.tsx1059
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx17
-rw-r--r--src/client/views/nodes/ColorBox.tsx97
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx10
-rw-r--r--src/client/views/nodes/DataViz.tsx20
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss0
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx90
-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.scss22
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx258
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx5
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx413
-rw-r--r--src/client/views/nodes/DocumentView.scss30
-rw-r--r--src/client/views/nodes/DocumentView.tsx1635
-rw-r--r--src/client/views/nodes/EquationBox.scss5
-rw-r--r--src/client/views/nodes/EquationBox.tsx100
-rw-r--r--src/client/views/nodes/FieldView.tsx54
-rw-r--r--src/client/views/nodes/FilterBox.tsx537
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx76
-rw-r--r--src/client/views/nodes/ImageBox.scss16
-rw-r--r--src/client/views/nodes/ImageBox.tsx406
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx1
-rw-r--r--src/client/views/nodes/LabelBigText.js81
-rw-r--r--src/client/views/nodes/LabelBox.scss2
-rw-r--r--src/client/views/nodes/LabelBox.tsx76
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx168
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx73
-rw-r--r--src/client/views/nodes/LinkDocPreview.scss24
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx221
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx535
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx117
-rw-r--r--src/client/views/nodes/PDFBox.tsx529
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss123
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.tsx301
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx58
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss214
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx271
-rw-r--r--src/client/views/nodes/RecordingBox/index.ts2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx238
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx790
-rw-r--r--src/client/views/nodes/VideoBox.scss159
-rw-r--r--src/client/views/nodes/VideoBox.tsx1333
-rw-r--r--src/client/views/nodes/WebBox.scss20
-rw-r--r--src/client/views/nodes/WebBox.tsx993
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js30
-rw-r--r--src/client/views/nodes/button/ButtonScripts.ts4
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx30
-rw-r--r--src/client/views/nodes/button/FontIconBox.scss95
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx683
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx3
-rw-r--r--src/client/views/nodes/button/textButton/TextButton.tsx23
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx77
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx191
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx282
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx99
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss553
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx1305
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx107
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts257
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx380
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts559
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx73
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts380
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts361
-rw-r--r--src/client/views/nodes/formattedText/schema_rts.ts13
-rw-r--r--src/client/views/nodes/trails/PresBox.scss7
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx2490
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss412
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx566
76 files changed, 13260 insertions, 7686 deletions
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index a6494e540..d40537776 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,222 +1,214 @@
@import "../global/globalCssVariables.scss";
-
-.audiobox-container,
-.audiobox-container-interactive {
+.audiobox-container {
width: 100%;
height: 100%;
position: inherit;
display: flex;
position: relative;
cursor: default;
+}
+
+.audiobox-recorder {
+ display: flex;
+ flex-direction: row;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ cursor: pointer;
- .audiobox-buttons {
+ .audiobox-dictation {
+ width: 40px;
+ background: $medium-gray;
+ color: $dark-gray;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ color: $black;
+ }
+ }
+
+ .audiobox-start-record {
+ color: $white;
+ background: $dark-gray;
display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: $body-text;
width: 100%;
+ height: 100%;
+ gap: 5px;
+
+ &:hover {
+ background: $black;
+ }
+ }
+
+ .recording-controls {
+ display: flex;
+ flex-direction: row;
align-items: center;
+ justify-content: center;
+ gap: 5px;
+ width: 100%;
height: 100%;
+ background: $dark-gray;
+ color: white;
- .audiobox-dictation {
- position: relative;
+ .record-timecode {
+ font-size: $large-header;
+ }
+
+ .record-button {
+ cursor: pointer;
width: 30px;
- height: 100%;
+ height: 30px;
+ border-radius: 50%;
+ background: $dark-gray;
+ display: flex;
align-items: center;
- display: inherit;
- background: $medium-gray;
- left: 0px;
- color: $dark-gray;
+ justify-content: center;
+
+ svg {
+ width: 15px;
+ }
&:hover {
- color: $black;
- cursor: pointer;
+ background: $black;
}
}
}
+}
- .audiobox-control,
- .audiobox-control-interactive {
- top: 0;
- max-height: 32px;
- width: 100%;
- display: inline-block;
- pointer-events: none;
- }
-
- .audiobox-control-interactive {
- pointer-events: all;
- }
+.audiobox-file {
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: $dark-gray;
+ width: 100%;
+ height: 100%;
+ color: $white;
- .audiobox-record-interactive,
- .audiobox-record {
- pointer-events: all;
+ .audiobox-button {
+ margin: 2.5px;
cursor: pointer;
- width: 100%;
- height: 100%;
- position: relative;
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ background: $dark-gray;
display: flex;
- flex-direction: row;
align-items: center;
justify-content: center;
- gap: 10px;
- color: white;
- font-weight: bold;
+
+ svg {
+ width: 15px;
+ }
+
+ &:hover {
+ background: $black;
+ }
+ }
+
+ svg {
+ width: 10px;
+ }
+
+ input[type="range"] {
+ width: 50px;
+ -webkit-appearance: none;
+ background: none;
+ margin: 5px;
}
- .audiobox-record {
- pointer-events: none;
+ input[type="range"]:focus {
+ outline: none;
}
- .recording {
- margin-top: auto;
- margin-bottom: auto;
+ input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
- height: 100%;
- position: relative;
- padding-right: 5px;
+ height: 6px;
+ cursor: pointer;
+ box-shadow: 0;
+ background: $light-gray;
+ border-radius: 3px;
+ }
+
+ input[type="range"]::-webkit-slider-thumb {
+ box-shadow: 0;
+ border: 0;
+ height: 10px;
+ width: 10px;
+ border-radius: 10px;
+ background: $medium-blue;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -2px;
+ }
+
+ .audiobox-controls {
display: flex;
flex-direction: row;
- justify-content: center;
+ justify-content: space-between;
align-items: center;
- gap: 7px;
- background-color: $medium-blue;
- padding: 10px;
+ width: 100%;
+ height: 30px;
- .time {
- position: relative;
- height: 100%;
- width: 100%;
- font-size: 16px;
- text-align: center;
+ .controls-left {
display: flex;
- justify-content: center;
- align-items: center;
- font-weight: bold;
+ flex-direction: row;
}
- .buttons {
- cursor: pointer;
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- width: 25px;
- padding: 5px;
- color: $dark-gray;
+ .controls-right {
+ display: flex;
+ flex-direction: row;
- &:hover {
- color: $black;
+ .audiobox-button {
+ width: 15px;
+ height: 15px;
+ margin: 0;
+
+ svg {
+ width: 10px;
+ }
}
}
}
- .audiobox-controls {
+ .audiobox-playback {
width: 100%;
height: 100%;
- position: relative;
- display: flex;
- background: $dark-gray;
+ background: $white;
- .audiobox-dictation {
+ .audiobox-timeline {
+ height: calc(100% - 50px);
+ width: 100%;
+ background: $white;
position: absolute;
- width: 40px;
- height: 100%;
- align-items: center;
- display: inherit;
- background: $medium-gray;
- left: 0px;
}
- .audiobox-player {
- margin-top: auto;
- margin-bottom: auto;
+ .audiobox-timeline > div {
width: 100%;
- position: relative;
- padding-right: 5px;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .audiobox-buttons {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 30px;
- height: 30px;
- border-radius: 50%;
- background-color: $dark-gray;
- color: $white;
- display: flex;
- align-items: center;
- justify-content: center;
- left: 5px;
-
- &:hover {
- background-color: $black;
- }
-
- svg {
- width: 100%;
- position: absolute;
- border-width: "thin";
- border-color: "white";
- }
- }
-
- .audiobox-dictation {
- position: relative;
- margin-top: auto;
- margin-bottom: auto;
- width: 25px;
- align-items: center;
- display: inherit;
- background: $medium-gray;
- }
-
- .audiobox-timeline {
- position: absolute;
- width: 100%;
- z-index: 1000;
- overflow: hidden;
- border-right: 5px solid black;
- }
-
- .audioBox-total-time,
- .audioBox-current-time {
- position: absolute;
- font-size: $small-text;
- top: 100%;
- color: $white;
- }
-
- .audioBox-current-time {
- left: 42px;
- }
-
- .audioBox-total-time {
- right: 2px;
- }
+ height: 100%;
}
}
-}
-@media only screen and (max-device-width: 480px) {
- .audiobox-dictation {
- font-size: 5em;
+ .audiobox-timecodes {
display: flex;
- width: 100;
- justify-content: center;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
align-items: center;
- }
-
- .audiobox-container .audiobox-record,
- .audiobox-container-interactive .audiobox-record {
- font-size: 3em;
- }
+ width: 100%;
+ height: 20px;
+ padding: 3px;
+ font-size: $small-text;
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-buttons,
- .audiobox-container .audiobox-controls .audiobox-player .audiobox-dictation,
- .audiobox-container-interactive .audiobox-controls .audiobox-player .audiobox-buttons {
- width: 70px;
+ .bottom-controls-middle {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 93377f1dc..8437736ae 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,136 +1,130 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- action,
- computed,
- IReactionDisposer,
- observable,
- reaction,
- runInAction
-} from "mobx";
-import { observer } from "mobx-react";
-import { DateField } from "../../../fields/DateField";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { ComputedField } from "../../../fields/ScriptField";
-import { Cast, NumCast } from "../../../fields/Types";
-import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime } from "../../../Utils";
-import { DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { SnappingManager } from "../../util/SnappingManager";
-import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import {
- ViewBoxAnnotatableComponent,
- ViewBoxAnnotatableProps
-} from "../DocComponent";
-import { Colors } from "../global/globalEnums";
-import "./AudioBox.scss";
-import { FieldView, FieldViewProps } from "./FieldView";
-import { LinkDocPreview } from "./LinkDocPreview";
-
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { DateField } from '../../../fields/DateField';
+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 { DocUtils } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { DragManager } from '../../util/DragManager';
+import { undoBatch } from '../../util/UndoManager';
+import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import './AudioBox.scss';
+import { FieldView, FieldViewProps } from './FieldView';
+
+/**
+ * AudioBox
+ * Main component: AudioBox.tsx
+ * Supporting Components: CollectionStackedTimeline, AudioWaveform
+ *
+ * AudioBox is a node that supports the recording and playback of audio files in Dash.
+ * When an audio file is importeed into Dash, it is immediately rendered as an AudioBox document.
+ * When a blank AudioBox node is created in Dash, audio recording controls are displayed and the user can start a recording which can be paused or stopped, and can use dictation to create a text transcript.
+ * Recording is done using the MediaDevices API to access the user's device microphone (see recordAudioAnnotation below)
+ * CollectionStackedTimeline handles AudioBox and VideoBox shared behavior, but AudioBox handles playing, pausing, etc because it contains <audio> element
+ * User can trim audio: nondestructive, just sets new bounds for playback and rendering timelin
+ */
+
+// used as a wrapper class for MediaStream from MediaDevices API
declare class MediaRecorder {
constructor(e: any); // whatever MediaRecorder has
}
+
+enum media_state {
+ PendingRecording = 'pendingRecording',
+ Recording = 'recording',
+ Paused = 'paused',
+ Playing = 'playing',
+}
+
@observer
export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(AudioBox, fieldKey);
}
public static Enabled = false;
- static playheadWidth = 40; // width of playhead
- static heightPercent = 75; // height of timeline in percent of height of audioBox.
- static Instance: AudioBox;
+ static topControlsHeight = 30; // height of upper controls above timeline
+ static bottomControlsHeight = 20; // height of lower controls below timeline
+
+ _dropDisposer?: DragManager.DragDropDisposer;
_disposers: { [name: string]: IReactionDisposer } = {};
- _ele: HTMLAudioElement | null = null;
- _stackedTimeline = React.createRef<CollectionStackedTimeline>();
- _recorder: any;
+ _ele: HTMLAudioElement | null = null; // <audio> ref
+ _recorder: any; // MediaRecorder
_recordStart = 0;
- _pauseStart = 0;
- _pauseEnd = 0;
+ _pauseStart = 0; // time when recording is paused (used to keep track of recording timecodes)
_pausedTime = 0;
- _stream: MediaStream | undefined;
- _start: number = 0;
- _play: any = null;
- _ended: boolean = false;
-
- @observable static _scrubTime = 0;
- @observable _markerEnd: number = 0;
- @observable _position: number = 0;
- @observable _waveHeight: Opt<number> = NumCast(this.layoutDoc._height);
- @observable _paused: boolean = false;
- @observable _trimming: boolean = false;
- @observable _trimStart: number = NumCast(this.layoutDoc.clipStart) ? NumCast(this.layoutDoc.clipStart) : 0;
- @observable _trimEnd: number = NumCast(this.layoutDoc.clipEnd) ? NumCast(this.layoutDoc.clipEnd)
- : this.duration;
-
- @computed get mediaState():
- | undefined
- | "pendingRecording"
- | "recording"
- | "paused"
- | "playing" {
- return this.dataDoc.mediaState as
- | undefined
- | "pendingRecording"
- | "recording"
- | "paused"
- | "playing";
- }
- set mediaState(value) {
- this.dataDoc.mediaState = value;
- }
- public static SetScrubTime = action((timeInMillisFrom1970: number) => {
- AudioBox._scrubTime = 0;
- AudioBox._scrubTime = timeInMillisFrom1970;
- });
+ _stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
+ _play: any = null; // timeout for playback
+
+ @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+ @observable _finished: boolean = false; // has playback reached end of clip
+ @observable _volume: number = 1;
+ @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
@computed get recordingStart() {
- return Cast(
- this.dataDoc[this.props.fieldKey + "-recordingStart"],
- DateField
- )?.date.getTime();
+ return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime();
}
- @computed get duration() {
+ @computed get rawDuration() {
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
+
+ @computed get miniPlayer() {
+ return this.props.PanelHeight() < 50;
+ } // used to collapse timeline when node is shrunk
+ @computed get links() {
+ return DocListCast(this.dataDoc.links);
}
- @computed get trimDuration() {
- return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart;
+ @computed get mediaState() {
+ return this.dataDoc.mediaState as media_state;
}
- @computed get anchorDocs() {
- return DocListCast(this.dataDoc[this.annotationKey]);
+ @computed get path() {
+ // returns the path of the audio file
+ const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || '';
+ return path === nullAudio ? '' : path;
}
- @computed get links() {
- return DocListCast(this.dataDoc.links);
+ set mediaState(value) {
+ this.dataDoc.mediaState = value;
}
- @computed get pauseTime() {
- return this._pauseEnd - this._pauseStart;
- } // total time paused to update the correct time
- @computed get heightPercent() {
- return AudioBox.heightPercent;
+
+ @computed get timeline() {
+ return this._stackedTimeline;
+ } // returns CollectionStackedTimeline ref
+
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this._dropDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+
+ this.mediaState === media_state.Recording && this.stopRecording();
}
- constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
- super(props);
- AudioBox.Instance = this;
+ @action
+ componentDidMount() {
+ 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.
- if (this.duration === undefined) {
- runInAction(
- () =>
- (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
- );
+ if (this.path) {
+ this.mediaState = media_state.Paused;
+ this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
+ } else {
+ this.mediaState = undefined as any as media_state;
}
}
getLinkData(l: Doc) {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- const linkTime =
- this._stackedTimeline.current?.anchorStart(la2) ||
- this._stackedTimeline.current?.anchorStart(la1) ||
- 0;
+ 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;
@@ -144,416 +138,524 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.rootDoc,
this.dataDoc,
this.annotationKey,
- "_timecodeToShow" /* audioStart */,
- "_timecodeToHide" /* audioEnd */,
- this._ele?.currentTime ||
- Cast(this.props.Document._currentTimecode, "number", null) ||
- (this.mediaState === "recording"
- ? (Date.now() - (this.recordingStart || 0)) / 1000
- : undefined)
+ '_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.rootDoc
);
- }
-
- componentWillUnmount() {
- Object.values(this._disposers).forEach((disposer) => disposer?.());
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
- }
-
- @action
- componentDidMount() {
- 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.mediaState = this.path ? "paused" : undefined;
-
- this.layoutDoc.clipStart = this.layoutDoc.clipStart ? this.layoutDoc.clipStart : 0;
- this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? this.layoutDoc.clipEnd : this.duration ? this.duration : undefined;
-
- this.path && this.setAnchorTime(NumCast(this.layoutDoc.clipStart));
- this.path && this.timecodeChanged();
-
- this._disposers.triggerAudio = reaction(
- () =>
- !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1
- ? NumCast(this.Document._triggerAudio, null)
- : undefined,
- (start) =>
- start !== undefined &&
- setTimeout(() => {
- this.playFrom(start);
- setTimeout(() => {
- this.Document._currentTimecode = start;
- this.Document._triggerAudio = undefined;
- }, 10);
- }), // wait for mainCont and try again to play
- { fireImmediately: true }
- );
+ };
- this._disposers.audioStop = reaction(
- () =>
- this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo
- ? Cast(this.Document._audioStop, "number", null)
- : undefined,
- (audioStop) =>
- audioStop !== undefined &&
- setTimeout(() => {
- this.Pause();
- setTimeout(() => (this.Document._audioStop = undefined), 10);
- }), // wait for mainCont and try again to play
- { fireImmediately: true }
- );
- }
-
- // for updating the timecode
+ // updates timecode and shows it in timeline, follows links at time
@action
timecodeChanged = () => {
- const htmlEle = this._ele;
- if (this.mediaState !== "recording" && htmlEle) {
- htmlEle.duration &&
- htmlEle.duration !== Infinity &&
- runInAction(
- () => (this.dataDoc[this.fieldKey + "-duration"] = htmlEle.duration)
- );
- this.layoutDoc.clipEnd = this.layoutDoc.clipEnd ? Math.min(this.duration, NumCast(this.layoutDoc.clipEnd)) : this.duration;
- this._trimEnd = this._trimEnd ? Math.min(this.duration, this._trimEnd) : this.duration;
+ if (this.mediaState !== media_state.Recording && this._ele) {
this.links
- .map((l) => this.getLinkData(l))
+ .map(l => this.getLinkData(l))
.forEach(({ la1, la2, linkTime }) => {
- if (
- linkTime > NumCast(this.layoutDoc._currentTimecode) &&
- linkTime < htmlEle.currentTime
- ) {
+ if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < this._ele!.currentTime) {
Doc.linkFollowHighlight(la1);
}
});
- this.layoutDoc._currentTimecode = htmlEle.currentTime;
-
+ this.layoutDoc._currentTimecode = this._ele.currentTime;
+ this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode));
}
- }
+ };
- // pause play back
- Pause = action(() => {
- this._ele!.pause();
- this.mediaState = "paused";
- });
-
- // play audio for documents created during recording
- playFromTime = (absoluteTime: number) => {
- this.recordingStart &&
- this.playFrom((absoluteTime - this.recordingStart) / 1000);
- }
-
- // play back the audio from time
+ // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
@action
- playFrom = (seekTimeInSeconds: number, endTime: number = this._trimEnd, fullPlay: boolean = false) => {
- clearTimeout(this._play);
+ playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
+ clearTimeout(this._play); // abort any previous clip ending
if (Number.isNaN(this._ele?.duration)) {
+ // audio element isn't loaded yet... wait 1/2 second and try again
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
- } else if (this._ele && AudioBox.Enabled) {
- if (seekTimeInSeconds < 0) {
- if (seekTimeInSeconds > -1) {
- setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
- } else {
- this.Pause();
- }
- } else if (this._trimStart <= endTime && seekTimeInSeconds <= this._trimEnd) {
- const start = Math.max(this._trimStart, seekTimeInSeconds);
- const end = Math.min(this._trimEnd, endTime);
+ } else if (this.timeline && this._ele && AudioBox.Enabled) {
+ // trimBounds override requested playback bounds
+ const end = Math.min(this.timeline.trimEnd, endTime ?? this.timeline.trimEnd);
+ const start = Math.max(this.timeline.trimStart, seekTimeInSeconds);
+ // checks if times are within clip range
+ if (seekTimeInSeconds >= 0 && this.timeline.trimStart <= end && seekTimeInSeconds <= this.timeline.trimEnd) {
this._ele.currentTime = start;
this._ele.play();
- runInAction(() => (this.mediaState = "playing"));
- if (endTime !== this.duration) {
- this._play = setTimeout(
- () => {
- this._ended = fullPlay ? true : this._ended;
- this.Pause();
- },
- (end - start) * 1000
- ); // use setTimeout to play a specific duration
- }
+ this.mediaState = media_state.Playing;
+ this.addCurrentlyPlaying();
+ this._play = setTimeout(() => {
+ // need to keep track of if end of clip is reached so on next play, clip restarts
+ if (fullPlay) this._finished = true;
+ // removes from currently playing if playback has reached end of range marker
+ else this.removeCurrentlyPlaying();
+ this.Pause();
+ }, (end - start) * 1000);
} else {
this.Pause();
}
}
- }
+ };
+
+ // removes from currently playing display
+ @action
+ removeCurrentlyPlaying = () => {
+ if (CollectionStackedTimeline.CurrentlyPlaying) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+ }
+ };
+
+ // adds doc to currently playing display
+ @action
+ addCurrentlyPlaying = () => {
+ if (!CollectionStackedTimeline.CurrentlyPlaying) {
+ CollectionStackedTimeline.CurrentlyPlaying = [];
+ }
+ if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ }
+ };
// update the recording time
updateRecordTime = () => {
- if (this.mediaState === "recording") {
+ if (this.mediaState === media_state.Recording) {
setTimeout(this.updateRecordTime, 30);
- if (this._paused) {
- this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
- } else {
- this.layoutDoc._currentTimecode =
- (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ if (!this._paused) {
+ this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
}
}
- }
+ };
// starts recording
recordAudioAnnotation = async () => {
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
- this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(
- new Date()
- );
+ this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField();
DocUtils.ActiveRecordings.push(this);
this._recorder.ondataavailable = async (e: any) => {
- console.log("Data available", e);
const [{ result }] = await Networking.UploadFilesToServer(e.data);
- console.log("Data result", result);
if (!(result instanceof Error)) {
- this.props.Document[this.props.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+ this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._recordStart = new Date().getTime();
- runInAction(() => (this.mediaState = "recording"));
- setTimeout(this.updateRecordTime, 0);
+ runInAction(() => (this.mediaState = media_state.Recording));
+ setTimeout(this.updateRecordTime);
this._recorder.start();
- setTimeout(() => this._recorder && this.stopRecording(), 60 * 60 * 1000); // stop after an hour
- }
+ setTimeout(this.stopRecording, 60 * 60 * 1000); // stop after an hour
+ };
+
+ // stops recording
+ @action
+ stopRecording = () => {
+ if (this._recorder) {
+ this._recorder.stop();
+ 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.mediaState = media_state.Paused;
+ this._stream?.getAudioTracks()[0].stop();
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+ }
+ };
// context menu
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
funcs.push({
- description:
- (this.layoutDoc.hideAnchors ? "Don't hide" : "Hide") + " anchors",
- event: () => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
- icon: "expand-arrows-alt",
+ description: (this.layoutDoc.hideAnchors ? "Don't hide" : 'Hide') + ' anchors',
+ event: e => (this.layoutDoc.hideAnchors = !this.layoutDoc.hideAnchors),
+ icon: 'expand-arrows-alt',
+ });
+ funcs.push({
+ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered',
+ event: e => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks),
+ icon: 'expand-arrows-alt',
});
funcs.push({
- description:
- (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") +
- " play when link is selected",
- event: () =>
- (this.layoutDoc.dontAutoPlayFollowedLinks =
- !this.layoutDoc.dontAutoPlayFollowedLinks),
- icon: "expand-arrows-alt",
+ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
+ event: e => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
+ icon: 'expand-arrows-alt',
});
funcs.push({
- description:
- (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") +
- " anchors onClick",
- event: () =>
- (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
- icon: "expand-arrows-alt",
+ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto" : 'Auto') + ' play anchors onClick',
+ event: e => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+ icon: 'expand-arrows-alt',
});
ContextMenu.Instance?.addItem({
- description: "Options...",
+ description: 'Options...',
subitems: funcs,
- icon: "asterisk",
+ icon: 'asterisk',
});
- }
-
- // stops the recording
- stopRecording = action(() => {
- this._recorder.stop();
- this._recorder = undefined;
- this.dataDoc[this.fieldKey + "-duration"] =
- (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
- this.mediaState = "paused";
- this._trimEnd = this.duration;
- this.layoutDoc.clipStart = 0;
- this.layoutDoc.clipEnd = this.duration;
- this._stream?.getAudioTracks()[0].stop();
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
- });
+ };
// button for starting and stopping the recording
- recordClick = (e: React.MouseEvent) => {
- if (e.button === 0 && !e.ctrlKey) {
- this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
- e.stopPropagation();
- }
- }
+ Record = (e: React.PointerEvent) => {
+ e.button === 0 &&
+ !e.ctrlKey &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action(() => {
+ this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
+ }),
+ false
+ );
+ };
// for play button
Play = (e?: any) => {
- let start;
- if (this._ended || this._ele!.currentTime === this.duration) {
- start = this._trimStart;
- this._ended = false;
- }
- else {
- start = this._ele!.currentTime;
+ e?.stopPropagation?.();
+
+ if (this.timeline && this._ele) {
+ const eleTime = this._ele.currentTime;
+
+ // if curr timecode outside of trim bounds, set it to start
+ let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+ // restarts clip if reached end on last play
+ if (this._finished) {
+ this._finished = false;
+ start = this.timeline.trimStart;
+ }
+
+ this.playFrom(start, this.timeline.trimEnd, true);
}
+ };
- this.playFrom(start, this._trimEnd, true);
- e?.stopPropagation?.();
- }
+ // pause play back
+ @action
+ 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();
+ }
+ };
- // creates a text document for dictation
+ // for dictation button, creates a text document for dictation
onFile = (e: any) => {
- const newDoc = CurrentUserUtils.GetNewTextDoc(
- "",
- NumCast(this.props.Document.x),
- NumCast(this.props.Document.y) +
- NumCast(this.props.Document._height) +
- 10,
- NumCast(this.props.Document._width),
- 2 * NumCast(this.props.Document._height)
- );
- Doc.GetProto(newDoc).recordingSource = this.dataDoc;
- Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(
- `self.recordingSource["${this.props.fieldKey}-recordingStart"]`
- );
- Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(
- "self.recordingSource.mediaState"
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action(() => {
+ const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+ 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)) {
+ newDoc.x = this.rootDoc.x;
+ newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ }),
+ false
);
- this.props.addDocument?.(newDoc);
- e.stopPropagation();
- }
+ };
- // ref for updating time
+ // sets <audio> ref for updating time
setRef = (e: HTMLAudioElement | null) => {
- e?.addEventListener("timeupdate", this.timecodeChanged);
- e?.addEventListener("ended", this.Pause);
+ e?.addEventListener('timeupdate', this.timecodeChanged);
+ e?.addEventListener('ended', () => {
+ this._finished = true;
+ this.Pause();
+ });
this._ele = e;
- }
-
- // returns the path of the audio file
- @computed get path() {
- const field = Cast(this.props.Document[this.props.fieldKey], AudioField);
- const path = field instanceof AudioField ? field.url.href : "";
- return path === nullAudio ? "" : path;
- }
-
- // returns the html audio element
- @computed get audio() {
- return <audio ref={this.setRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
- <source src={this.path} type="audio/mpeg" />
- Not supported.
- </audio>;
- }
+ };
// pause the time during recording phase
- @action
- recordPause = (e: React.MouseEvent) => {
- this._pauseStart = new Date().getTime();
- this._paused = true;
- this._recorder.pause();
- e.stopPropagation();
- }
+ recordPause = (e: React.PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action(() => {
+ this._pauseStart = new Date().getTime();
+ this._paused = true;
+ this._recorder.pause();
+ }),
+ false
+ );
+ };
// continue the recording
- @action
- recordPlay = (e: React.MouseEvent) => {
- this._pauseEnd = new Date().getTime();
- this._paused = false;
- this._recorder.resume();
- e.stopPropagation();
- }
+ recordPlay = (e: React.PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action(() => {
+ this._paused = false;
+ this._pausedTime += new Date().getTime() - this._pauseStart;
+ this._recorder.resume();
+ }),
+ false
+ );
+ };
- playing = () => this.mediaState === "playing";
+ // plays link
playLink = (link: Doc) => {
- const stack = this._stackedTimeline.current;
if (link.annotationOn === this.rootDoc) {
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
- this.playFrom(stack?.anchorStart(link) || 0, stack?.anchorEnd(link));
+ this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode =
- stack?.anchorStart(link) || 0;
+ this._ele!.currentTime = this.layoutDoc._currentTimecode = this.timeline?.anchorStart(link) || 0;
}
} else {
this.links
- .filter((l) => l.anchor1 === link || l.anchor2 === link)
- .forEach((l) => {
+ .filter(l => l.anchor1 === link || l.anchor2 === link)
+ .forEach(l => {
const { la1, la2 } = this.getLinkData(l);
- const startTime = stack?.anchorStart(la1) || stack?.anchorStart(la2);
- const endTime = stack?.anchorEnd(la1) || stack?.anchorEnd(la2);
+ const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2);
+ const endTime = this.timeline?.anchorEnd(la1) || this.timeline?.anchorEnd(la2);
if (startTime !== undefined) {
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
- endTime
- ? this.playFrom(startTime, endTime)
- : this.playFrom(startTime);
+ this.playFrom(startTime, endTime);
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode =
- startTime;
+ this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
}
}
});
}
- }
+ };
- // shows trim controls
@action
- startTrim = () => {
- if (!this.duration) {
- this.timecodeChanged();
- }
- if (this.mediaState === "playing") {
- this.Pause();
- }
- this._trimming = true;
- }
+ timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
- // hides trim controls and displays new clip
- @action
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+
+ setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+
+ playing = () => this.mediaState === media_state.Playing;
+
+ isActiveChild = () => this._isAnyChildContentActive;
+
+ // timeline dimensions
+ timelineWidth = () => this.props.PanelWidth();
+ timelineHeight = () => this.props.PanelHeight() - (AudioBox.topControlsHeight + AudioBox.bottomControlsHeight);
+
+ // ends trim, hides trim controls and displays new clip
+ @undoBatch
finishTrim = () => {
- if (this.mediaState === "playing") {
- this.Pause();
- }
- this.layoutDoc.clipStart = this._trimStart;
- this.layoutDoc.clipEnd = this._trimEnd;
- this._trimming = false;
- this.setAnchorTime(Math.max(Math.min(this._trimEnd, this._ele!.currentTime), this._trimStart));
- }
+ this.Pause();
+ this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this._ele!.currentTime), this.timeline?.trimStart || 0));
+ this.timeline?.StopTrimming();
+ };
+
+ // displays trim controls to start trimming clip
+ startTrim = (scope: TrimScope) => {
+ this.Pause();
+ this.timeline?.StartTrimming(scope);
+ };
+
+ // for trim button, double click displays full clip, single displays curr trim bounds
+ onClipPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ this.timeline &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action((e: PointerEvent, doubleTap?: boolean) => {
+ if (doubleTap) {
+ this.startTrim(TrimScope.All);
+ } else if (this.timeline) {
+ this.Pause();
+ this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+ }
+ })
+ );
+ };
+ // for zoom slider, sets timeline waveform zoom
+ zoom = (zoom: number) => {
+ this.timeline?.setZoom(zoom);
+ };
+
+ // for volume slider sets volume
@action
- setStartTrim = (newStart: number) => {
- this._trimStart = newStart;
- }
+ setVolume = (volume: number) => {
+ if (this._ele) {
+ this._volume = volume;
+ this._ele.volume = volume;
+ if (this._muted) {
+ this.toggleMute();
+ }
+ }
+ };
+ // toggles audio muted
@action
- setEndTrim = (newEnd: number) => {
- this._trimEnd = newEnd;
+ toggleMute = () => {
+ if (this._ele) {
+ this._muted = !this._muted;
+ this._ele.muted = this._muted;
+ }
+ };
+
+ setupTimelineDrop = (r: HTMLDivElement | null) => {
+ if (r && this.timeline) {
+ this._dropDisposer?.();
+ this._dropDisposer = DragManager.MakeDropTarget(
+ r,
+ (e, de) => {
+ const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ de.complete.docDragData && this.timeline.internalDocDrop(e, de, de.complete.docDragData, xp);
+ },
+ this.layoutDoc,
+ undefined
+ );
+ }
+ };
+
+ // UI for recording, initially displayed when new audio created in Dash
+ @computed get recordingControls() {
+ return (
+ <div className="audiobox-recorder">
+ <div className="audiobox-dictation" onPointerDown={this.onFile}>
+ <FontAwesomeIcon size="2x" icon="file-alt" />
+ </div>
+ {[media_state.Recording, media_state.Playing].includes(this.mediaState) ? (
+ <div className="recording-controls" onClick={e => e.stopPropagation()}>
+ <div className="record-button" onPointerDown={this.Record}>
+ <FontAwesomeIcon size="2x" icon="stop" />
+ </div>
+ <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>
+ ) : (
+ <div className="audiobox-start-record" onPointerDown={this.Record}>
+ <FontAwesomeIcon icon="microphone" />
+ RECORD
+ </div>
+ )}
+ </div>
+ );
}
- isActiveChild = () => this._isAnyChildContentActive;
- timelineWhenChildContentsActiveChanged = (isActive: boolean) =>
- this.props.whenChildContentsActiveChanged(
- runInAction(() => (this._isAnyChildContentActive = isActive))
- )
- timelineScreenToLocal = () =>
- this.props
- .ScreenToLocalTransform()
- .translate(
- -AudioBox.playheadWidth,
- (-(100 - this.heightPercent) / 200) * this.props.PanelHeight()
- )
- setAnchorTime = (time: number) => {
- (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+ // UI for playback, displayed for imported or recorded clips, hides timeline and collapses controls when node is shrunk vertically
+ @computed get playbackControls() {
+ return (
+ <div
+ className="audiobox-file"
+ style={{
+ pointerEvents: this._isAnyChildContentActive || this.props.isContentActive() ? 'all' : 'none',
+ flexDirection: this.miniPlayer ? 'row' : 'column',
+ justifyContent: this.miniPlayer ? 'flex-start' : 'space-between',
+ }}>
+ <div className="audiobox-controls">
+ <div className="controls-left">
+ <div
+ className="audiobox-button"
+ title={this.mediaState === media_state.Paused ? 'play' : 'pause'}
+ onPointerDown={
+ this.mediaState === media_state.Paused
+ ? this.Play
+ : e => {
+ e.stopPropagation();
+ this.Pause();
+ }
+ }>
+ <FontAwesomeIcon icon={this.mediaState === media_state.Paused ? 'play' : 'pause'} size={'1x'} />
+ </div>
+
+ {!this.miniPlayer && (
+ <div className="audiobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish' : 'trim'} onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} />
+ </div>
+ )}
+ </div>
+ <div className="controls-right">
+ <div
+ className="audiobox-button"
+ title={this._muted ? 'unmute' : 'mute'}
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.toggleMute();
+ }}>
+ <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+ </div>
+ <input
+ type="range"
+ step="0.1"
+ min="0"
+ max="1"
+ value={this._muted ? 0 : this._volume}
+ className="toolbar-slider volume"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+ />
+ </div>
+ </div>
+
+ <div className="audiobox-playback" style={{ width: this.miniPlayer ? 0 : '100%' }}>
+ <div className="audiobox-timeline">{this.renderTimeline}</div>
+ </div>
+
+ {this.audio}
+
+ <div className="audiobox-timecodes">
+ <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
+ {this.miniPlayer ? (
+ <div>/</div>
+ ) : (
+ <div className="bottom-controls-middle">
+ <FontAwesomeIcon icon="search-plus" />
+ <input
+ type="range"
+ step="0.1"
+ min="1"
+ max="5"
+ value={this.timeline?._zoomFactor}
+ className="toolbar-slider"
+ id="zoom-slider"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ this.zoom(Number(e.target.value));
+ }}
+ />
+ </div>
+ )}
+
+ <div className="timecode-duration">{this.timeline && formatTime(Math.round(this.timeline.clipDuration))}</div>
+ </div>
+ </div>
+ );
}
- timelineHeight = () =>
- (((this.props.PanelHeight() * this.heightPercent) / 100) *
- this.heightPercent) /
- 100 // panelHeight * heightPercent is player height. * heightPercent is timeline height (as per css inline)
- timelineWidth = () => this.props.PanelWidth() - AudioBox.playheadWidth;
+ // gets CollectionStackedTimeline
@computed get renderTimeline() {
return (
<CollectionStackedTimeline
- ref={this._stackedTimeline}
- {...this.props}
+ ref={action((r: any) => (this._stackedTimeline = r))}
+ {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + "-dictation"}
+ dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.path}
renderDepth={this.props.renderDepth + 1}
- startTag={"_timecodeToShow" /* audioStart */}
- endTag={"_timecodeToHide" /* audioEnd */}
- focus={DocUtils.DefaultFocus}
+ startTag={'_timecodeToShow' /* audioStart */}
+ endTag={'_timecodeToHide' /* audioEnd */}
bringToFront={emptyFunction}
CollectionView={undefined}
- duration={this.duration}
playFrom={this.playFrom}
- setTime={this.setAnchorTime}
+ setTime={this.setPlayheadTime}
playing={this.playing}
- whenChildContentsActiveChanged={
- this.timelineWhenChildContentsActiveChanged
- }
+ whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
moveDocument={this.moveDocument}
addDocument={this.addDocument}
removeDocument={this.removeDocument}
@@ -565,141 +667,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
playLink={this.playLink}
PanelWidth={this.timelineWidth}
PanelHeight={this.timelineHeight}
- trimming={this._trimming}
- trimStart={this._trimStart}
- trimEnd={this._trimEnd}
- trimDuration={this.trimDuration}
- setStartTrim={this.setStartTrim}
- setEndTrim={this.setEndTrim}
+ rawDuration={this.rawDuration}
/>
);
}
+ // returns the html audio element
+ @computed get audio() {
+ return (
+ <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))}>
+ <source src={this.path} type="audio/mpeg" />
+ Not supported.
+ </audio>
+ );
+ }
+
render() {
- const interactive =
- SnappingManager.GetIsDragging() || this.props.isContentActive()
- ? "-interactive"
- : "";
return (
- <div
- className="audiobox-container"
- onContextMenu={this.specificContextMenu}
- onClick={
- !this.path && !this._recorder ? this.recordAudioAnnotation : undefined
- }
- style={{
- pointerEvents:
- this.props.layerProvider?.(this.layoutDoc) === false
- ? "none"
- : undefined,
- }}
- >
- {!this.path ? (
- <div className="audiobox-buttons">
- <div className="audiobox-dictation" onClick={this.onFile}>
- <FontAwesomeIcon
- style={{
- width: "30px"
- }}
- icon="file-alt"
- size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
- />
- </div>
- {this.mediaState === "recording" || this.mediaState === "paused" ? (
- <div className="recording" onClick={(e) => e.stopPropagation()}>
- <div className="recording-buttons" onClick={this.recordClick}>
- <FontAwesomeIcon
- icon={"stop"}
- size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
- />
- </div>
- <div
- className="recording-buttons"
- onClick={this._paused ? this.recordPlay : this.recordPause}
- >
- <FontAwesomeIcon
- icon={this._paused ? "play" : "pause"}
- size={this.props.PanelHeight() < 36 ? "1x" : "2x"}
- />
- </div>
- <div className="time">
- {formatTime(
- Math.round(NumCast(this.layoutDoc._currentTimecode))
- )}
- </div>
- </div>
- ) : (
- <div
- className={`audiobox-record${interactive}`}
- style={{ backgroundColor: Colors.DARK_GRAY }}
- >
- <FontAwesomeIcon icon="microphone" />
- RECORD
- </div>
- )}
- </div>
- ) : (
- <div
- className="audiobox-controls"
- style={{
- pointerEvents:
- this._isAnyChildContentActive || this.props.isContentActive()
- ? "all"
- : "none",
- }}
- >
- <div className="audiobox-dictation" />
- <div
- className="audiobox-player"
- style={{ height: `${AudioBox.heightPercent}%` }}
- >
- <div
- className="audiobox-buttons"
- title={this.mediaState === "paused" ? "play" : "pause"}
- onClick={this.mediaState === "paused" ? this.Play : this.Pause}
- >
- {" "}
- <FontAwesomeIcon
- icon={this.mediaState === "paused" ? "play" : "pause"}
- size={"1x"}
- />
- </div>
- <div
- className="audiobox-buttons"
- title={this._trimming ? "finish" : "trim"}
- onClick={this._trimming ? this.finishTrim : this.startTrim}
- >
- <FontAwesomeIcon
- icon={this._trimming ? "check" : "cut"}
- size={"1x"}
- />
- </div>
- <div
- className="audiobox-timeline"
- style={{
- top: 0,
- height: `100%`,
- left: AudioBox.playheadWidth,
- width: `calc(100% - ${AudioBox.playheadWidth}px)`,
- background: "white",
- }}
- >
- {this.renderTimeline}
- </div>
- {this.audio}
- <div className="audioBox-current-time">
- {this._trimming ?
- formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))
- : formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this._trimStart)))}
- </div>
- <div className="audioBox-total-time">
- {this._trimming || !this._trimEnd ?
- formatTime(Math.round(NumCast(this.duration)))
- : formatTime(Math.round(NumCast(this.trimDuration)))}
- </div>
- </div>
- </div>
- )}
+ <div ref={this.setupTimelineDrop} className="audiobox-container" onContextMenu={this.specificContextMenu} style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined }}>
+ {!this.path ? this.recordingControls : this.playbackControls}
</div>
);
}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index c2a526804..284584a3d 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, Opt } from "../../../fields/Doc";
import { List } from "../../../fields/List";
@@ -6,7 +6,7 @@ import { listSpec } from "../../../fields/Schema";
import { ComputedField } from "../../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { TraceMobx } from "../../../fields/util";
-import { DashColor, numberRange } from "../../../Utils";
+import { DashColor, numberRange, OmitKeys } from "../../../Utils";
import { DocumentManager } from "../../util/DocumentManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
@@ -21,14 +21,12 @@ import React = require("react");
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined;
- layerProvider: ((doc: Doc, assign?: boolean) => boolean) | undefined;
renderCutoffProvider: (doc: Doc) => boolean;
zIndex?: number;
highlight?: boolean;
jitterRotation: number;
dataTransition?: string;
replica: string;
- renderIndex: number;
CollectionFreeFormView: CollectionFreeFormView;
}
@@ -39,12 +37,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
@observable _contentView: DocumentView | undefined | null;
get displayName() { return "CollectionFreeFormDocumentView(" + this.rootDoc.title + ")"; } // this makes mobx trace() statements more descriptive
get maskCentering() { return this.props.Document.isInkMask ? InkingStroke.MaskDim / 2 : 0; }
- get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${this.props.jitterRotation}deg)`; }
+ get transform() { return `translate(${this.X - this.maskCentering}px, ${this.Y - this.maskCentering}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; }
get X() { return this.dataProvider ? this.dataProvider.x : NumCast(this.Document.x); }
get Y() { return this.dataProvider ? this.dataProvider.y : NumCast(this.Document.y); }
get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : NumCast(this.Document.zIndex); }
get Opacity() { return this.dataProvider ? this.dataProvider.opacity : undefined; }
get Highlight() { return this.dataProvider?.highlight; }
+ @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); }
@computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); }
@computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); }
@@ -159,22 +158,22 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
...this.props,
CollectionFreeFormDocumentView: this.returnThis,
styleProvider: this.styleProvider,
- layerProvider: this.props.layerProvider,
ScreenToLocalTransform: this.screenToLocalTransform,
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
};
const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined);
+ const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && background && (!background.startsWith("linear") && DashColor(background).alpha() !== 1) ? "multiply" : undefined);
return <div className={"collectionFreeFormDocumentView-container"}
style={{
outline: this.Highlight ? "orange solid 2px" : "",
width: this.panelWidth(),
height: this.panelHeight(),
transform: this.transform,
- transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition),
+ 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,
- mixBlendMode,
+ mixBlendMode: mixBlendMode,
display: this.ZInd === -99 ? "none" : undefined
}} >
{this.props.renderCutoffProvider(this.props.Document) ?
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index d975baf9b..c229a966a 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,42 +1,47 @@
-import React = require("react");
-import { action } from "mobx";
-import { observer } from "mobx-react";
+import React = require('react');
+import { action } from 'mobx';
+import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
-import { InkTool } from "../../../fields/InkField";
-import { StrCast } from "../../../fields/Types";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch } from "../../util/UndoManager";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from "../InkingStroke";
-import "./ColorBox.scss";
+import { InkTool } from '../../../fields/InkField';
+import { StrCast } from '../../../fields/Types';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoBatch } from '../../util/UndoManager';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
+import './ColorBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
-import { RichTextMenu } from "./formattedText/RichTextMenu";
+import { RichTextMenu } from './formattedText/RichTextMenu';
@observer
export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(ColorBox, fieldKey);
+ }
@undoBatch
@action
static switchColor(color: ColorState) {
- // Doc.UserDoc().backgroundColor = Utils.colorString(color); // bcz: this can't go here ... needs a proper home in the settings panel
SetActiveInkColor(color.hex);
SelectionManager.Views().map(view => {
- const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
- view.props.Document.layout instanceof Doc ? view.props.Document.layout :
- view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
+ const targetDoc =
+ view.props.Document.dragFactory instanceof Doc
+ ? view.props.Document.dragFactory
+ : view.props.Document.layout instanceof Doc
+ ? view.props.Document.layout
+ : view.props.Document.isTemplateForField
+ ? view.props.Document
+ : Doc.GetProto(view.props.Document);
if (targetDoc) {
- if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) { // this situation typically occurs when you have a link dot
- targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template...
- }
- else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== "") {
- Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + "-color"] = color.hex;
+ if (view.props.LayoutTemplate?.() || view.props.LayoutTemplateString) {
+ // this situation typically occurs when you have a link dot
+ targetDoc.backgroundColor = color.hex; // bcz: don't know how to change the color of an inline template...
+ } else if (RichTextMenu.Instance?.TextViewFieldKey && window.getSelection()?.toString() !== '') {
+ Doc.Layout(view.props.Document)[RichTextMenu.Instance.TextViewFieldKey + '-color'] = color.hex;
} else {
- Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ""); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment
+ Doc.Layout(view.props.Document)._backgroundColor = color.hex + (color.rgb.a ? Math.round(color.rgb.a * 255).toString(16) : ''); // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment
}
}
});
@@ -44,24 +49,34 @@ 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]());
- return <div className={`colorBox-container${this.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}%` }} >
-
- <SketchPicker
- onChange={c => CurrentUserUtils.SelectedTool === InkTool.None && ColorBox.switchColor(c)}
- color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986',
- '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- />
+ return (
+ <div
+ className={`colorBox-container${this.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}%` }}>
+ <SketchPicker
+ onChange={c => Doc.ActiveTool === InkTool.None && ColorBox.switchColor(c)}
+ color={StrCast(SelectionManager.Views()?.[0]?.rootDoc?._backgroundColor, ActiveInkColor())}
+ presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
+ />
- <div style={{ width: this.props.PanelWidth() / scaling, display: "flex", paddingTop: "10px" }}>
- <div> {ActiveInkWidth()}</div>
- <input type="range" defaultValue={ActiveInkWidth()} min={1} max={100} onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- SetActiveInkWidth(e.target.value);
- SelectionManager.Views().filter(i => StrCast(i.rootDoc.type) === DocumentType.INK).map(i => i.rootDoc.strokeWidth = Number(e.target.value));
- }} />
+ <div style={{ width: this.props.PanelWidth() / scaling, display: 'flex', paddingTop: '10px' }}>
+ <div> {ActiveInkWidth()}</div>
+ <input
+ type="range"
+ defaultValue={ActiveInkWidth()}
+ min={1}
+ max={100}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ SetActiveInkWidth(e.target.value);
+ SelectionManager.Views()
+ .filter(i => StrCast(i.rootDoc.type) === DocumentType.INK)
+ .map(i => (i.rootDoc.strokeWidth = Number(e.target.value)));
+ }}
+ />
+ </div>
</div>
- </div>;
+ );
}
}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 5919cd8f2..5ea6d567a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -3,7 +3,7 @@ import { action, 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, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
@@ -76,18 +76,18 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
const clearButton = (which: string) => {
return <div className={`clear-button ${which}`}
onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding
- onClick={e => this.clearDoc(e, `compareBox-${which}`)}>
+ onClick={e => this.clearDoc(e, which)}>
<FontAwesomeIcon className={`clear-button ${which}`} icon={"times"} size="sm" />
</div>;
};
const displayDoc = (which: string) => {
const whichDoc = Cast(this.dataDoc[which], Doc, null);
- //if (whichDoc?.type === DocumentType.MARKER)
+ // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
return whichDoc ? <>
<DocumentView
ref={(r) => {
- whichDoc !== targetDoc && r?.focus(targetDoc);
+ whichDoc !== targetDoc && r?.focus(whichDoc);
}}
{...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
isContentActive={returnFalse}
@@ -96,7 +96,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
Document={targetDoc}
DataDoc={undefined}
hideLinkButton={true}
- pointerEvents={"none"} />
+ pointerEvents={returnNone} />
{clearButton(which)}
</> : // placeholder image if doc is missing
<div className="placeholder">
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
new file mode 100644
index 000000000..df4c8f937
--- /dev/null
+++ b/src/client/views/nodes/DataViz.tsx
@@ -0,0 +1,20 @@
+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
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
new file mode 100644
index 000000000..592723ee9
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -0,0 +1,90 @@
+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"
+}
+
+
+@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";
+ }
+ }
+
+ constructor(props: any) {
+ super(props);
+ if (!this.rootDoc._dataVizView) {
+ // TODO: nda - this might not always want to default to "table"
+ this.rootDoc._dataVizView = "table";
+ }
+ }
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+
+ @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]});
+ }
+ this.pairs = pairs;
+ return pairs;
+ }
+
+ @computed get selectView() {
+ switch(this.currView) {
+ case "table":
+ return (<TableBox pairs={this.pairs} />)
+ case "histogram":
+ return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ }
+ }
+
+ @computed get pairVals() {
+ return this.createPairs();
+ }
+
+ componentDidMount() {
+ this.createPairs();
+ }
+
+ // 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";
+ }
+
+ render() {
+ return (
+ <div className="dataViz">
+ <button onClick={(e) => this.changeViewHandler(e)}>Change View</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
new file mode 100644
index 000000000..595cecebf
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/DrawHelper.ts
@@ -0,0 +1,247 @@
+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
new file mode 100644
index 000000000..5aac9dc77
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.scss
@@ -0,0 +1,18 @@
+// 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
new file mode 100644
index 000000000..00dc2ef46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
@@ -0,0 +1,159 @@
+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.scss b/src/client/views/nodes/DataVizBox/TableBox.scss
new file mode 100644
index 000000000..1264d6a46
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.scss
@@ -0,0 +1,22 @@
+.table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-left: 10px;
+ margin-right: 10px;
+}
+
+.table-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 5px;
+ border-bottom: 1px solid #ccc;
+}
+
+.table-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
new file mode 100644
index 000000000..dfa8262d8
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/TableBox.tsx
@@ -0,0 +1,37 @@
+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/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 005133eb0..381436a56 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,46 +1,48 @@
-import { computed } from "mobx";
-import { observer } from "mobx-react";
-import { AclPrivate, Doc, Opt } from "../../../fields/Doc";
-import { ScriptField } from "../../../fields/ScriptField";
-import { Cast, StrCast } from "../../../fields/Types";
-import { GetEffectiveAcl, TraceMobx } from "../../../fields/util";
-import { emptyPath, OmitKeys, Without } from "../../../Utils";
-import { DirectoryImportBox } from "../../util/Import & Export/DirectoryImportBox";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionSchemaView } from "../collections/collectionSchema/CollectionSchemaView";
-import { CollectionView } from "../collections/CollectionView";
-import { InkingStroke } from "../InkingStroke";
-import { PresElementBox } from "../nodes/trails/PresElementBox";
-import { SearchBox } from "../search/SearchBox";
-import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
-import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
-import { AudioBox } from "./AudioBox";
-import { ColorBox } from "./ColorBox";
-import { ComparisonBox } from "./ComparisonBox";
-import { DocumentViewProps } from "./DocumentView";
-import "./DocumentView.scss";
-import { EquationBox } from "./EquationBox";
-import { FieldView, FieldViewProps } from "./FieldView";
-import { FilterBox } from "./FilterBox";
-import { FontIconBox } from "./button/FontIconBox";
-import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox";
-import { FunctionPlotBox } from "./FunctionPlotBox";
-import { ImageBox } from "./ImageBox";
-import { KeyValueBox } from "./KeyValueBox";
-import { LabelBox } from "./LabelBox";
-import { LinkAnchorBox } from "./LinkAnchorBox";
-import { LinkBox } from "./LinkBox";
-import { PDFBox } from "./PDFBox";
-import { PresBox } from "./trails/PresBox";
-import { ScreenshotBox } from "./ScreenshotBox";
-import { ScriptingBox } from "./ScriptingBox";
-import { SliderBox } from "./SliderBox";
-import { VideoBox } from "./VideoBox";
-import { WebBox } from "./WebBox";
-import React = require("react");
-import XRegExp = require("xregexp");
-import { MapBox } from "./MapBox/MapBox";
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { AclPrivate, Doc, Opt } from '../../../fields/Doc';
+import { ScriptField } from '../../../fields/ScriptField';
+import { Cast, StrCast } from '../../../fields/Types';
+import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { emptyPath, OmitKeys, Without } from '../../../Utils';
+import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBox';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
+import { CollectionView } from '../collections/CollectionView';
+import { InkingStroke } from '../InkingStroke';
+import { PresElementBox } from '../nodes/trails/PresElementBox';
+import { SearchBox } from '../search/SearchBox';
+import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo';
+import { YoutubeBox } from './../../apis/youtube/YoutubeBox';
+import { AudioBox } from './AudioBox';
+import { FontIconBox } from './button/FontIconBox';
+import { ColorBox } from './ColorBox';
+import { ComparisonBox } from './ComparisonBox';
+import { DataVizBox } from './DataVizBox/DataVizBox';
+import { DocumentViewProps } from './DocumentView';
+import './DocumentView.scss';
+import { EquationBox } from './EquationBox';
+import { FieldView, FieldViewProps } from './FieldView';
+import { FilterBox } from './FilterBox';
+import { FormattedTextBox, FormattedTextBoxProps } from './formattedText/FormattedTextBox';
+import { FunctionPlotBox } from './FunctionPlotBox';
+import { ImageBox } from './ImageBox';
+import { KeyValueBox } from './KeyValueBox';
+import { LabelBox } from './LabelBox';
+import { LinkAnchorBox } from './LinkAnchorBox';
+import { LinkBox } from './LinkBox';
+import { MapBox } from './MapBox/MapBox';
+import { PDFBox } from './PDFBox';
+import { RecordingBox } from './RecordingBox';
+import { ScreenshotBox } from './ScreenshotBox';
+import { ScriptingBox } from './ScriptingBox';
+import { SliderBox } from './SliderBox';
+import { PresBox } from './trails/PresBox';
+import { VideoBox } from './VideoBox';
+import { WebBox } from './WebBox';
+import React = require('react');
+import XRegExp = require('xregexp');
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -58,7 +60,6 @@ class ObserverJsxParser1 extends JsxParser {
const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
-
interface HTMLtagProps {
Document: Doc;
RootDoc: Doc;
@@ -66,16 +67,17 @@ interface HTMLtagProps {
onClick?: ScriptField;
onInput?: ScriptField;
scaling: number;
+ children?: JSX.Element[];
}
//"<HTMLdiv borderRadius='100px' onClick={this.bannerColor=this.bannerColor==='red'?'green':'red'} overflow='hidden' position='absolute' width='100%' height='100%' transform='rotate({2*this.x+this.y}deg)'> <ImageBox {...props} fieldKey={'data'}/> <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px' transform='translate(-40px, 45px) rotate(-45deg)' position='absolute' color='{this.bannerColor===`green`?`light`:`dark`}blue' backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'> {this.title}</HTMLspan></HTMLdiv>"
-//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%'
-// transform='rotate({2*this.x+this.y}deg)'
+//"<HTMLdiv borderRadius='100px' overflow='hidden' position='absolute' width='100%' height='100%'
+// transform='rotate({2*this.x+this.y}deg)'
// onClick = { this.bannerColor = this.bannerColor === 'red' ? 'green' : 'red' } >
// <ImageBox {...props} fieldKey={'data'}/>
-// <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px'
-// transform='translate(-40px, 45px) rotate(-45deg)' position='absolute'
-// color='{this.bannerColor===`green`?`light`:`dark`}blue'
+// <HTMLspan width='200px' top='0' height='35px' textAlign='center' paddingTop='10px'
+// transform='translate(-40px, 45px) rotate(-45deg)' position='absolute'
+// color='{this.bannerColor===`green`?`light`:`dark`}blue'
// backgroundColor='{this.bannerColor===`green`?`dark`:`light`}blue'>
// {this.title}
// </HTMLspan>
@@ -85,45 +87,51 @@ export class HTMLtag extends React.Component<HTMLtagProps> {
click = (e: React.MouseEvent) => {
const clickScript = (this.props as any).onClick as Opt<ScriptField>;
clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, scale: this.props.scaling });
- }
+ };
onInput = (e: React.FormEvent<HTMLDivElement>) => {
const onInputScript = (this.props as any).onInput as Opt<ScriptField>;
onInputScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, value: (e.target as any).textContent });
- }
+ };
render() {
const style: { [key: string]: any } = {};
- const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "scaling", "Document", "key", "onInput", "onClick", "__proto__"]).omit;
- const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value
- return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.props.RootDoc, this: this.props.Document, scale: this.props.scaling }).result as string || "";
+ const divKeys = OmitKeys(this.props, ['children', 'htmltag', 'RootDoc', 'scaling', 'Document', 'key', 'onInput', 'onClick', '__proto__']).omit;
+ const replacer = (match: any, expr: string, offset: any, string: any) => {
+ // bcz: this executes a script to convert a propery expression string: { script } into a value
+ return (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: 'number' })?.script.run({ self: this.props.RootDoc, this: this.props.Document, scale: this.props.scaling }).result as string) || '';
};
Object.keys(divKeys).map((prop: string) => {
const p = (this.props as any)[prop] as string;
style[prop] = p?.replace(/{([^.'][^}']+)}/g, replacer);
});
const Tag = this.props.htmltag as keyof JSX.IntrinsicElements;
- return <Tag style={style} onClick={this.click} onInput={this.onInput as any}>
- {this.props.children}
- </Tag>;
+ return (
+ <Tag style={style} onClick={this.click} onInput={this.onInput as any}>
+ {this.props.children}
+ </Tag>
+ );
}
}
@observer
-export class DocumentContentsView extends React.Component<DocumentViewProps & FormattedTextBoxProps & {
- isSelected: (outsideReaction: boolean) => boolean,
- select: (ctrl: boolean) => void,
- scaling?: () => number,
- setHeight: (height: number) => void,
- layoutKey: string,
-}> {
+export class DocumentContentsView extends React.Component<
+ DocumentViewProps &
+ FormattedTextBoxProps & {
+ isSelected: (outsideReaction: boolean) => boolean;
+ select: (ctrl: boolean) => void;
+ NativeDimScaling?: () => number;
+ setHeight?: (height: number) => void;
+ layoutKey: 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 (typeof layout === "string") return layout;
- return "<p>Loading layout</p>";
+ 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 (typeof layout === 'string') return layout;
+ return '<p>Loading layout</p>';
}
get dataDoc() {
@@ -134,37 +142,38 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
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?.() || (this.props.LayoutTemplateString && this.props.Document) ||
+ 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);
+ return Doc.expandTemplateLayout(template, this.props.Document, params ? '(' + params + ')' : this.props.layoutKey);
}
CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
- const docOnlyProps = [ // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews
- "freezeDimensions",
- "hideResizeHandles",
- "hideTitle",
- "treeViewDoc",
- "contentPointerEvents",
- "radialMenu",
- "LayoutTemplateString",
- "LayoutTemplate",
- "dontCenter",
- "ContentScaling",
- "contextMenuItems",
- "onClick",
- "onDoubleClick",
- "onPointerDown",
- "onPointerUp",
+ const docOnlyProps = [
+ // 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',
+ 'LayoutTemplate',
+ 'dontCenter',
+ 'contextMenuItems',
+ 'onClick',
+ 'onDoubleClick',
+ 'onPointerDown',
+ 'onPointerUp',
];
const list = {
- ...OmitKeys(this.props, [...docOnlyProps], "").omit,
+ ...OmitKeys(this.props, [...docOnlyProps], '').omit,
RootDoc: Cast(this.layoutDoc?.rootDocument, Doc, null) || this.layoutDoc,
Document: this.layoutDoc,
DataDoc: this.dataDoc,
onClick: onClick,
- onInput: onInput
+ onInput: onInput,
};
return { props: list };
}
@@ -179,17 +188,17 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
// replace code content with a script >{content}< as in <HTMLdiv>{this.title}</HTMLdiv>
const replacer = (match: any, prefix: string, expr: string, postfix: string, offset: any, string: any) => {
- return prefix + (ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string || "") + postfix;
+ return prefix + ((ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ this: this.props.Document }).result as string) || '') + postfix;
};
layoutFrame = layoutFrame.replace(/(>[^{]*)[^=]\{([^.'][^<}]+)\}([^}]*<)/g, replacer);
- // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'>
+ // replace HTML<tag> with corresponding HTML tag as in: <HTMLdiv> becomes <HTMLtag Document={props.Document} htmltag='div'>
const replacer2 = (match: any, p1: string, offset: any, string: any) => {
- return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.scaling?.() || 1}' htmltag='${p1}'`;
+ return `<HTMLtag RootDoc={props.RootDoc} Document={props.Document} scaling='${this.props.NativeDimScaling?.() || 1}' htmltag='${p1}'`;
};
layoutFrame = layoutFrame.replace(/<HTML([a-zA-Z0-9_-]+)/g, replacer2);
- // replace /HTML<tag> with </HTMLdiv> as in: </HTMLdiv> becomes </HTMLtag>
+ // replace /HTML<tag> with </HTMLdiv> as in: </HTMLdiv> becomes </HTMLtag>
const replacer3 = (match: any, p1: string, offset: any, string: any) => {
return `</HTMLtag`;
};
@@ -199,16 +208,16 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
const makeFuncProp = (func: string) => {
const splits = layoutFrame.split(`${func}=`);
if (splits.length > 1) {
- const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] });
+ const code = XRegExp.matchRecursive(splits[1], '{', '}', '', { valueNames: ['between', 'left', 'match', 'right', 'between'] });
layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1);
- const script = code[1].value.replace(/^‘/, "").replace(/’$/, ""); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts
- return ScriptField.MakeScript(script, { this: Doc.name, self: Doc.name, scale: "number", value: "string" });
+ const script = code[1].value.replace(/^‘/, '').replace(/’$/, ''); // ‘’ are not valid quotes in javascript so get rid of them -- they may be present to make it easier to write complex scripts - see headerTemplate in currentUserUtils.ts
+ return ScriptField.MakeScript(script, { this: Doc.name, self: Doc.name, scale: 'number', value: 'string' });
}
return undefined;
// add input function to props
};
- const onClick = makeFuncProp("onClick");
- const onInput = makeFuncProp("onInput");
+ const onClick = makeFuncProp('onClick');
+ const onInput = makeFuncProp('onInput');
const bindings = this.CreateBindings(onClick, onInput);
return { bindings, layoutFrame };
}
@@ -217,24 +226,55 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
TraceMobx();
const { bindings, layoutFrame } = this.renderData;
- return (this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate) ? (null) :
+ return this.props.renderDepth > 12 || !layoutFrame || !this.layoutDoc || GetEffectiveAcl(this.layoutDoc) === AclPrivate ? null : (
<ObserverJsxParser
key={42}
blacklistedAttrs={emptyPath}
renderInWrapper={false}
components={{
- FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView,
- CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
- ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox,
+ FormattedTextBox,
+ ImageBox,
+ DirectoryImportBox,
+ FontIconBox,
+ LabelBox,
+ EquationBox,
+ SliderBox,
+ FieldView,
+ CollectionFreeFormView,
+ CollectionDockingView,
+ CollectionSchemaView,
+ CollectionView,
+ WebBox,
+ KeyValueBox,
+ PDFBox,
+ VideoBox,
+ AudioBox,
+ RecordingBox,
+ PresBox,
+ YoutubeBox,
+ PresElementBox,
+ SearchBox,
+ FilterBox,
+ FunctionPlotBox,
+ ColorBox,
+ DashWebRTCVideo,
+ LinkAnchorBox,
+ InkingStroke,
+ LinkBox,
+ ScriptingBox,
+ MapBox,
ScreenshotBox,
- HTMLtag, ComparisonBox
+ DataVizBox,
+ HTMLtag,
+ ComparisonBox,
}}
bindings={bindings}
jsx={layoutFrame}
showWarnings={true}
-
- onError={(test: any) => { console.log("DocumentContentsView:" + test); }}
- />;
+ onError={(test: any) => {
+ console.log('DocumentContentsView:' + test);
+ }}
+ />
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 433a0bf48..56de2d1fc 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -1,3 +1,4 @@
+
import { observer } from "mobx-react";
import * as React from "react";
import { DocumentView } from "./DocumentView";
@@ -51,7 +52,7 @@ export class DocumentIconContainer extends React.Component {
};
},
getVars() {
- const docs = DocumentManager.Instance.DocumentViews;
+ const docs = Array.from(DocumentManager.Instance.DocumentViews);
const capturedVariables: { [name: string]: Field } = {};
usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document);
return { capturedVariables };
@@ -59,6 +60,6 @@ export class DocumentIconContainer extends React.Component {
};
}
render() {
- return DocumentManager.Instance.DocumentViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+ return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index 9ab3171d3..0f3eb14bc 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -2,7 +2,9 @@
.documentLinksButton-wrapper {
transform-origin: top left;
+ width: 100%;
}
+
.documentLinksButton-menu {
width: 100%;
height: 100%;
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 7e6ca4248..5939d1680 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -1,28 +1,24 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast, DocListCastAsync, Opt, WidthSym } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { Cast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, 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 { Colors } from "../global/globalEnums";
-import { LightboxView } from "../LightboxView";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, Opt } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+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 { Colors } from '../global/globalEnums';
import './DocumentLinksButton.scss';
-import { DocumentView } from "./DocumentView";
-import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
-import { TaskCompletionBox } from "./TaskCompletedBox";
-import React = require("react");
-import { Transform } from "../../util/Transform";
+import { DocumentView } from './DocumentView';
+import { LinkDescriptionPopup } from './LinkDescriptionPopup';
+import { TaskCompletionBox } from './TaskCompletedBox';
+import React = require('react');
-const higflyout = require("@hig/flyout");
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -32,7 +28,7 @@ interface DocumentLinksButtonProps {
AlwaysOn?: boolean;
InMenu?: boolean;
StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed)
- ContentScaling?: () => number;
+ scaling?: () => number; // how uch doc is scaled so that link buttons can invert it
}
@observer
export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> {
@@ -46,80 +42,71 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
@observable public static invisibleWebDoc: Opt<Doc>;
public static invisibleWebRef = React.createRef<HTMLDivElement>();
- @action public static ClearLinkEditor() { DocumentLinksButton.LinkEditorDocView = undefined; }
-
- @action @undoBatch
+ @action
+ @undoBatch
onLinkButtonMoved = (e: PointerEvent) => {
if (this.props.InMenu && this.props.StartLink) {
if (this._linkButton.current !== null) {
- const linkDrag = UndoManager.StartBatch("Drag Link");
- this.props.View && DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, {
- 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");
- }
- linkDrag?.end();
- },
- hideSource: false
- });
+ const linkDrag = UndoManager.StartBatch('Drag Link');
+ this.props.View &&
+ DragManager.StartLinkDrag(this._linkButton.current, this.props.View, this.props.View.ComponentView?.getAnchor, e.pageX, e.pageY, {
+ 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');
+ }
+ linkDrag?.end();
+ },
+ hideSource: false,
+ });
return true;
}
return false;
}
return false;
- }
+ };
onLinkMenuOpen = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
- if (doubleTap) {
- const rootDoc = this.props.View.rootDoc;
- const docid = Doc.CurrentUserEmail + Doc.GetProto(rootDoc)[Id] + "-pivotish";
- DocServer.GetRefField(docid).then(async docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(rootDoc);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- let wid = rootDoc[WidthSym]();
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([rootAlias()], { title: this.props.View.Document.title + "-pivot", _width: 500, _height: 500, }, docid);
- const docs = await DocListCastAsync(Doc.GetProto(target).data);
- if (!target.pivotFocusish) (Doc.GetProto(target).pivotFocusish = target);
- DocListCast(rootDoc.links).forEach(link => {
- const other = LinkManager.getOppositeAnchor(link, rootDoc);
- const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
- if (otherdoc && !docs?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- const alias = Doc.MakeAlias(otherdoc);
- alias.x = wid;
- alias.y = 0;
- alias._lockedPosition = false;
- wid += otherdoc[WidthSym]();
- Doc.AddDocToList(Doc.GetProto(target), "data", alias);
- }
- });
- LightboxView.SetLightboxDoc(target);
- });
- }
- else DocumentLinksButton.LinkEditorDocView = this.props.View;
- }));
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ this.onLinkButtonMoved,
+ emptyFunction,
+ action((e, doubleTap) => {
+ if (doubleTap) {
+ DocumentView.showBackLinks(this.props.View.rootDoc);
+ }
+ }),
+ undefined,
+ undefined,
+ action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View))
+ );
+ };
@undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, e, this.onLinkButtonMoved, emptyFunction, action((e, doubleTap) => {
- if (doubleTap && this.props.InMenu && this.props.StartLink) {
- //action(() => Doc.BrushDoc(this.props.View.Document));
- if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
- DocumentLinksButton.StartLink = undefined;
- DocumentLinksButton.StartLinkView = undefined;
- } else {
- DocumentLinksButton.StartLink = this.props.View.props.Document;
- DocumentLinksButton.StartLinkView = this.props.View;
+ setupMoveUpEvents(
+ this,
+ e,
+ this.onLinkButtonMoved,
+ emptyFunction,
+ action((e, doubleTap) => {
+ if (doubleTap && this.props.InMenu && this.props.StartLink) {
+ //action(() => Doc.BrushDoc(this.props.View.Document));
+ if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
+ DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.StartLinkView = undefined;
+ } else {
+ DocumentLinksButton.StartLink = this.props.View.props.Document;
+ DocumentLinksButton.StartLinkView = this.props.View;
+ }
}
- }
- }));
- }
+ })
+ );
+ };
- @action @undoBatch
+ @action
+ @undoBatch
onLinkClick = (e: React.MouseEvent): void => {
if (this.props.InMenu && this.props.StartLink) {
DocumentLinksButton.AnnotationId = undefined;
@@ -127,109 +114,125 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
- } else { //if this LinkButton's Document is undefined
+ } else {
+ //if this LinkButton's Document is undefined
DocumentLinksButton.StartLink = this.props.View.props.Document;
DocumentLinksButton.StartLinkView = this.props.View;
}
//action(() => Doc.BrushDoc(this.props.View.Document));
- } else if (!this.props.InMenu) {
- DocumentLinksButton.LinkEditorDocView = this.props.View;
}
- }
-
+ };
completeLink = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, 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?.() || 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?
+ setupMoveUpEvents(
+ this,
+ 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?.() || 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;
+ LinkManager.currentLink = linkDoc;
- runInAction(() => {
- if (linkDoc) {
- TaskCompletionBox.textDisplayed = "Link Created";
- TaskCompletionBox.popupX = e.screenX;
- TaskCompletionBox.popupY = e.screenY - 133;
- TaskCompletionBox.taskCompleted = true;
+ 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;
+ 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;
- }
+ 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);
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2500
+ );
+ }
+ });
}
- });
- }
- }
- })));
- }
+ }
+ })
+ )
+ );
+ };
- public static finishLinkClick = undoBatch(action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView,) => {
- if (startLink === endLink) {
- DocumentLinksButton.StartLink = undefined;
- DocumentLinksButton.StartLinkView = undefined;
- DocumentLinksButton.AnnotationId = undefined;
- DocumentLinksButton.AnnotationUri = undefined;
- //!this.props.StartLink
- } else if (startLink !== endLink) {
- endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
- startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
- const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "link", undefined, undefined, true);
+ public static finishLinkClick = undoBatch(
+ action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView) => {
+ if (startLink === endLink) {
+ DocumentLinksButton.StartLink = undefined;
+ DocumentLinksButton.StartLinkView = undefined;
+ DocumentLinksButton.AnnotationId = undefined;
+ DocumentLinksButton.AnnotationUri = undefined;
+ //!this.props.StartLink
+ } else if (startLink !== endLink) {
+ endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
+ startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
+ const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
- LinkManager.currentLink = linkDoc;
+ LinkManager.currentLink = linkDoc;
- if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { // if linking from a Hypothes.is annotation
- Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
- Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
- Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri;
- const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink);
- Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId,
- (startIsAnnotation ? startLink : endLink)); // edit annotation to add a Dash hyperlink to the linked doc
- }
+ if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) {
+ // if linking from a Hypothes.is annotation
+ Doc.GetProto(linkDoc as Doc).linksToAnnotation = true;
+ Doc.GetProto(linkDoc as Doc).annotationId = DocumentLinksButton.AnnotationId;
+ Doc.GetProto(linkDoc as Doc).annotationUri = DocumentLinksButton.AnnotationUri;
+ const dashHyperlink = Doc.globalServerPath(startIsAnnotation ? endLink : startLink);
+ Hypothesis.makeLink(StrCast(startIsAnnotation ? endLink.title : startLink.title), dashHyperlink, DocumentLinksButton.AnnotationId, startIsAnnotation ? startLink : endLink); // edit annotation to add a Dash hyperlink to the linked doc
+ }
- if (linkDoc) {
- TaskCompletionBox.textDisplayed = "Link Created";
- TaskCompletionBox.popupX = screenX;
- TaskCompletionBox.popupY = screenY - 133;
- TaskCompletionBox.taskCompleted = true;
+ if (linkDoc) {
+ TaskCompletionBox.textDisplayed = 'Link Created';
+ TaskCompletionBox.popupX = screenX;
+ TaskCompletionBox.popupY = screenY - 133;
+ TaskCompletionBox.taskCompleted = true;
- if (LinkDescriptionPopup.showDescriptions === "ON" || !LinkDescriptionPopup.showDescriptions) {
- LinkDescriptionPopup.popupX = screenX;
- LinkDescriptionPopup.popupY = screenY - 100;
- LinkDescriptionPopup.descriptionPopup = true;
- }
+ if (LinkDescriptionPopup.showDescriptions === 'ON' || !LinkDescriptionPopup.showDescriptions) {
+ LinkDescriptionPopup.popupX = screenX;
+ LinkDescriptionPopup.popupY = 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;
- }
+ 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);
+ setTimeout(
+ action(() => {
+ TaskCompletionBox.taskCompleted = false;
+ }),
+ 2500
+ );
+ }
}
- }
- }));
+ })
+ );
@action clearLinks() {
DocumentLinksButton.StartLink = undefined;
@@ -240,9 +243,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.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) {
results.push(link);
}
});
@@ -251,47 +252,45 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
/**
* gets the JSX of the link button (btn used to start/complete links) OR the link-view button (btn on bottom left of each linked node)
- *
+ *
* todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button
*/
@computed get linkButtonInner() {
- const btnDim = "30px";
- const link = <img style={{ width: "22px", height: "16px" }} src={`/assets/${"link.png"}`} />;
- const isActive = (DocumentLinksButton.StartLink === this.props.View.props.Document) && this.props.StartLink;
- return (!this.props.InMenu ?
- <div className="documentLinksButton-cont"
- style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}
- >
- <div className={"documentLinksButton"}
- onPointerDown={this.onLinkMenuOpen} onClick={this.onLinkClick}
+ const btnDim = '30px';
+ const link = <img style={{ width: '22px', height: '16px' }} src={`/assets/${'link.png'}`} />;
+ const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink;
+ return !this.props.InMenu ? (
+ <div className="documentLinksButton-cont" style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}>
+ <div
+ className={'documentLinksButton'}
+ onPointerDown={this.onLinkMenuOpen}
+ onClick={this.onLinkClick}
style={{
backgroundColor: Colors.LIGHT_BLUE,
color: Colors.BLACK,
- fontSize: "20px",
+ fontSize: '20px',
width: btnDim,
height: btnDim,
}}>
{Array.from(this.filteredLinks).length}
</div>
</div>
- :
- <div className="documentLinksButton-menu" ref={this._linkButton}>
- {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? //if the origin node is not this node
- <div className={"documentLinksButton-endLink"}
+ ) : (
+ <div className="documentLinksButton-menu">
+ {this.props.InMenu && !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)}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
- : (null)
- }
- {
- this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
- <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
- </div>
- :
- (null)
- }
+ ) : null}
+ {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
+ <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
+ </div>
+ ) : null}
</div>
);
}
@@ -299,21 +298,23 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
render() {
TraceMobx();
- const menuTitle = this.props.StartLink ? "Drag or tap to start link" : "Tap to complete link";
- const buttonTitle = "Tap to view links; double tap to open link collection";
+ const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link';
+ const buttonTitle = 'Tap to view links; double tap to open link collection';
const title = this.props.InMenu ? menuTitle : buttonTitle;
//render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu
- return (!Array.from(this.filteredLinks).length && !this.props.AlwaysOn) ? (null) :
- <div className="documentLinksButton-wrapper" >
- {
- (this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) ||
- (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ?
- <Tooltip title={<div className="dash-tooltip">{title}</div>}>
- {this.linkButtonInner}
- </Tooltip>
- : this.linkButtonInner
- }
- </div>;
+ return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : (
+ <div
+ className="documentLinksButton-wrapper"
+ style={{
+ transform: this.props.InMenu ? undefined : `scale(${this.props.scaling})`,
+ }}>
+ {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? (
+ <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
+ ) : (
+ this.linkButtonInner
+ )}
+ </div>
+ );
}
}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4565f8504..c02692bfb 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,11 +1,16 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.documentView-effectsWrapper {
border-radius: inherit;
}
+// documentViews have a docView-hack tag which is replaced by this tag when capturing bitmaps (when the dom is converted to an html string)
+.documentView-hack {
+ display: inline; // this swap is needed because for some reason when capturing bitmaps, things don't appear unless dispay:inline is explicitly set.
+}
+
.documentView-customBorder {
- width:100%;
+ width: 100%;
height: 100%;
position: absolute;
top: 0;
@@ -19,7 +24,7 @@
width: 100%;
height: 100%;
border-radius: inherit;
- transition: outline .3s linear;
+ transition: outline 0.3s linear;
cursor: grab;
// background: $white; //overflow: hidden;
@@ -57,7 +62,7 @@
.documentView-audioBackground {
display: inline-block;
- width: 10%;
+ width: 25px;
height: 25;
position: absolute;
top: 10px;
@@ -83,7 +88,7 @@
width: 100%;
overflow: hidden;
- >.documentView-node {
+ > .documentView-node {
position: absolute;
}
}
@@ -153,7 +158,7 @@
top: 0;
width: 100%;
height: 14;
- background: rgba(0, 0, 0, .4);
+ background: rgba(0, 0, 0, 0.4);
text-align: center;
text-overflow: ellipsis;
white-space: pre;
@@ -182,19 +187,18 @@
transition: opacity 0.5s;
}
}
-
}
.documentView-node:hover,
.documentView-node-topmost:hover {
- >.documentView-styleWrapper {
- >.documentView-titleWrapper-hover {
+ > .documentView-styleWrapper {
+ > .documentView-titleWrapper-hover {
display: inline-block;
}
}
- >.documentView-styleWrapper {
- >.documentView-captionWrapper {
+ > .documentView-styleWrapper {
+ > .documentView-captionWrapper {
opacity: 1;
}
}
@@ -220,6 +224,6 @@
.documentView-node:first-child {
position: relative;
- background: "#B59B66"; //$white;
+ background: '#B59B66'; //$white;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8682a43e6..1ee1aec5a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,54 +1,57 @@
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast } from "../../../fields/Doc";
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
-import { List } from "../../../fields/List";
-import { ObjectField } from "../../../fields/ObjectField";
-import { listSpec } from "../../../fields/Schema";
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from "../../../fields/Types";
-import { AudioField } from "../../../fields/URLField";
+import { BoolCast, Cast, 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, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnTrue, returnVal, simulateMouseClick, Utils } from "../../../Utils";
+import { emptyFunction, hasDescendantTarget, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DocumentType } from '../../documents/DocumentTypes';
-import { Networking } from "../../Network";
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
-import { DocumentManager } from "../../util/DocumentManager";
-import { DragManager, dropActionType } from "../../util/DragManager";
+import { DocServer } from '../../DocServer';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
+import { DocumentManager } from '../../util/DocumentManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
+import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from "../../util/SelectionManager";
+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, CollectionViewType } from '../collections/CollectionView';
-import { ContextMenu } from "../ContextMenu";
+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 { DocComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
-import { InkingStroke } from "../InkingStroke";
-import { LightboxView } from "../LightboxView";
-import { StyleLayers, StyleProp } from "../StyleProvider";
-import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "./DocumentContentsView";
+import { InkingStroke } from '../InkingStroke';
+import { LightboxView } from '../LightboxView';
+import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
+import { DocumentContentsView } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
-import "./DocumentView.scss";
-import { FormattedTextBox } from "./formattedText/FormattedTextBox";
+import './DocumentView.scss';
+import { FieldViewProps } from './FieldView';
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { LinkAnchorBox } from './LinkAnchorBox';
-import { LinkDocPreview } from "./LinkDocPreview";
+import { LinkDocPreview } from './LinkDocPreview';
import { RadialMenu } from './RadialMenu';
-import { ScriptingBox } from "./ScriptingBox";
+import { ScriptingBox } from './ScriptingBox';
import { PresBox } from './trails/PresBox';
-import React = require("react");
+import React = require('react');
const { Howl } = require('howler');
interface Window {
@@ -62,16 +65,16 @@ declare class MediaRecorder {
export enum ViewAdjustment {
resetView = 1,
- doNothing = 0
+ doNothing = 0,
}
-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 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
- willZoom?: boolean; // determines whether to zoom in on target document
- scale?: number; // percent of containing frame to zoom into document
- afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
+ willZoom?: boolean; // determines whether to zoom in on target document
+ scale?: number; // percent of containing frame to zoom into document
+ afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
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)
}
@@ -79,11 +82,12 @@ export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>
export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void;
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?: () => 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, smooth: boolean) => Opt<number>; // returns the duration of the focus
- 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
- reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox 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
+ 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
+ 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
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
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)
@@ -96,26 +100,34 @@ export interface DocComponentView {
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 };
- snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: 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 };
+ snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number };
search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
}
+// These props are passed to both FieldViews and DocumentViews
export interface DocumentViewSharedProps {
+ fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews
+ DocumentView?: () => DocumentView;
renderDepth: number;
Document: Doc;
DataDoc?: Doc;
- fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document
+ 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>;
+ suppressSetHeight?: boolean;
thumbShown?: () => boolean;
+ isHovering?: () => boolean;
setContentView?: (view: DocComponentView) => any;
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
PanelWidth: () => number;
PanelHeight: () => number;
docViewPath: () => DocumentView[];
- layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean);
+ childHideDecorationTitle?: () => boolean;
+ 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>;
focus: DocFocusFunc;
fitWidth?: (doc: Doc) => boolean;
@@ -126,7 +138,7 @@ export interface DocumentViewSharedProps {
whenChildContentsActiveChanged: (isActive: boolean) => void;
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
addDocTab: (doc: Doc, where: string) => 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)
+ 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;
@@ -140,38 +152,44 @@ 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.
- pointerEvents?: string;
+ 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;
updateFilterDoc?: (doc: Doc) => void;
- // Parker added both of these
- originalBackgroundColor?: string;
- isNoteTakingView?: boolean;
+ dontHideOnDrag?: boolean;
}
+
+// these props are specific to DocuentViews
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
- freezeDimensions?: boolean;
hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
- hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
- hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
+ hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
+ hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
+ hideDocumentButtonBar?: boolean;
+ hideOpenButton?: boolean;
+ hideDeleteButton?: boolean;
treeViewDoc?: Doc;
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";
- ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal
+ 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
LayoutTemplate?: () => Opt<Doc>;
- contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[];
+ contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[];
onClick?: () => ScriptField;
onDoubleClick?: () => ScriptField;
onPointerDown?: () => ScriptField;
onPointerUp?: () => ScriptField;
+ onBrowseClick?: () => ScriptField | undefined;
+ onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
}
+// these props are only available in DocumentViewIntenral
export interface DocumentViewInternalProps extends DocumentViewProps {
NativeWidth: () => number;
NativeHeight: () => number;
@@ -184,6 +202,7 @@ 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.
+ _animateScaleTime = 300; // milliseconds;
@observable _animateScalingTo = 0;
@observable _mediaState = 0;
@observable _pendingDoubleClick = false;
@@ -202,34 +221,88 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
- private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; }
- public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
- public get ContentDiv() { return this._mainCont.current; }
- 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 ContentScale() { return this.props.ContentScaling?.() || 1; }
- @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.layoutDoc, this.props, StyleProp.Hidden); }
- @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); }
- @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); }
- @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); }
- @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); }
- @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 docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); }
- @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; }
- @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; }
- @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); }
- @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); }
- @computed get nativeWidth() { return this.props.NativeWidth(); }
- @computed get nativeHeight() { return this.props.NativeHeight(); }
- @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); }
- @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); }
- @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); }
- @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); }
-
- componentWillUnmount() { this.cleanupHandlers(true); }
- componentDidMount() { this.setupHandlers(); }
+ private get topMost() {
+ return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
+ }
+ public get displayName() {
+ return 'DocumentView(' + this.props.Document.title + ')';
+ } // this makes mobx trace() statements more descriptive
+ public get ContentDiv() {
+ return this._mainCont.current;
+ }
+ 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 NativeDimScaling() {
+ return this.props.NativeDimScaling?.() || 1;
+ }
+ @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);
+ }
+ @computed get boxShadow() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow);
+ }
+ @computed get borderRounding() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
+ }
+ @computed get hideLinkButton() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ':selected' : ''));
+ }
+ @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 docContents() {
+ return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents);
+ }
+ @computed get headerMargin() {
+ return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
+ }
+ @computed get titleHeight() {
+ return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
+ }
+ @computed get pointerEvents() {
+ return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
+ }
+ @computed get finalLayoutKey() {
+ return StrCast(this.Document.layoutKey, 'layout');
+ }
+ @computed get nativeWidth() {
+ return this.props.NativeWidth();
+ }
+ @computed get nativeHeight() {
+ return this.props.NativeHeight();
+ }
+ @computed get onClickHandler() {
+ return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
+ }
+ @computed get onDoubleClickHandler() {
+ return this.props.onDoubleClick?.() ?? Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick;
+ }
+ @computed get onPointerDownHandler() {
+ return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown);
+ }
+ @computed get onPointerUpHandler() {
+ return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp);
+ }
+
+ componentWillUnmount() {
+ this.cleanupHandlers(true);
+ }
+ componentDidMount() {
+ this.setupHandlers();
+ }
//componentDidUpdate() { this.setupHandlers(); }
setupHandlers() {
this.cleanupHandlers(false);
@@ -239,6 +312,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
}
}
+ @action
cleanupHandlers(unbrush: boolean) {
this._dropDisposer?.();
this._multiTouchDisposer?.();
@@ -250,8 +324,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
this.removeMoveListeners();
this.removeEndListeners();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.removeEventListener('pointerup', this.onPointerUp);
if (RadialMenu.Instance._display === false) {
this.addHoldMoveListeners();
this.addHoldEndListeners();
@@ -260,7 +334,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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];
@@ -271,7 +345,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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();
@@ -286,10 +360,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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 (!e.nativeEvent.cancelBubble && !this.props.isSelected()) {
+ if (!this.props.isSelected()) {
e.stopPropagation();
e.preventDefault();
@@ -298,7 +372,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
this.removeEndListeners();
this.addEndListeners();
}
- }
+ };
handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
SelectionManager.DeselectAll();
@@ -307,33 +381,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (touch) {
this._downX = touch.clientX;
this._downY = touch.clientY;
- if (!e.nativeEvent.cancelBubble) {
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation();
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
+ 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 && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ } 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);
+ 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>) => {
@@ -344,8 +417,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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 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);
@@ -354,33 +427,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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.ContentScale;
- const actualdW = Math.max(width + (dW * scale), 20);
- const actualdH = Math.max(height + (dH * scale), 20);
+ 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);
+ 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));
+ 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;
+ if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width;
else layoutDoc._height = actualdH;
- }
- else {
+ } else {
if (!fixedAspect) {
- Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc));
+ 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;
+ if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height;
else layoutDoc._width = actualdW;
}
} else {
@@ -392,7 +464,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
e.stopPropagation();
e.preventDefault();
}
- }
+ };
@action
onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
@@ -401,132 +473,162 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), 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 });
+ (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, "add:right"), 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 });
+ 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]); //put a set of
- if (this.props.isNoteTakingView) {
- dragData.draggedDocuments.forEach((doc) => {
- doc.backgroundColor = "#C9DAEF"
- doc.opacity = 0.5
- })
- }
- const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0);
- dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top);
+ const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ 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]);
+ 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.dimSource :
- // dragEffects field, set dim
+ //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
+ // 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.onDragStar &&!this.props.isNoteTakingView)},
- () => setTimeout(action(() => {
- ffview && (ffview.ChildDrag = undefined)
- //TODO: is there a better way than adding another field to the props? Not quite sure how "this" works tbh
- if (this.props.isNoteTakingView) {
- this.props.Document.backgroundColor = "";
- this.props.Document.opacity = 1;
- }
- }))); // this needs to happen after the drop event is processed.
+ 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)))
+ ); // this needs to happen after the drop event is processed.
ffview?.setupDragLines(false);
}
}
onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey && !e.nativeEvent.cancelBubble) {
+ if (e.altKey) {
e.stopPropagation();
e.preventDefault();
- if (e.key === "†" || e.key === "t") {
- if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title";
+ 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), 0);
- else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text...
+ else if (!this._titleRef.current.setIsFocused(true)) {
+ // if focus didn't change, focus on interior text...
this._titleRef.current?.setIsFocused(false);
this._componentView?.setFocus?.();
}
}
}
- }
+ };
focus = (anchor: Doc, options?: DocFocusOptions) => {
- LightboxView.SetCookie(StrCast(anchor["cookies-set"]));
+ 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]);
- });
+ 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 timeout, the right _componentView should have been created, so call it to update its view spec values
setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
- const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
- const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing;
+ const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
+ const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
- ...options, afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0))
+ ...options,
+ afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)),
});
-
- }
+ };
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!e.nativeEvent.cancelBubble && !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.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
let stopPropagate = true;
let preventDefault = true;
- !StrListCast(this.props.Document._layerTags).includes(StyleLayers.Background) && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : 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
+ 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.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
+ 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 } = e;
- const func = () => this.onDoubleClickHandler.script.run({
- this: this.layoutDoc,
- self: this.rootDoc,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX, clientY, shiftKey
- }, console.log);
- UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click");
+ const func = () =>
+ this.onDoubleClickHandler.script.run(
+ {
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ thisContainer: this.props.ContainingCollectionDoc,
+ documentView: this.props.DocumentView(),
+ clientX,
+ clientY,
+ shiftKey,
+ },
+ console.log
+ );
+ UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
} else if (!Doc.IsSystem(this.rootDoc)) {
- UndoManager.RunInBatch(() =>
- LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.())
- , "double tap");
+ UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
}
- } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
+ } else if (this.onClickHandler?.script && !isScriptBox()) {
+ // bcz: hack? don't execute script if you're clicking on a scripting box itself
const { clientX, clientY, shiftKey } = 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
- }, console.log).result?.select === true ? this.props.select(false) : "";
- const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click");
+ 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,
+ },
+ console.log
+ ).result?.select === true
+ ? this.props.select(false)
+ : '';
+ const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
if (this.onDoubleClickHandler) {
- this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350);
+ runInAction(() => (this._pendingDoubleClick = true));
+ this._timeout = setTimeout(() => {
+ this._timeout = undefined;
+ clickFunc();
+ }, 350);
} else clickFunc();
- } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey);
+ } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+ 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 & isTemplaetForField 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)) {
+ // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField 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
} else {
- runInAction(() => this._pendingDoubleClick = true);
- this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350);
+ runInAction(() => (this._pendingDoubleClick = true));
+ this._timeout = setTimeout(
+ action(() => {
+ this._pendingDoubleClick = false;
+ this._timeout = undefined;
+ }),
+ 350
+ );
this.props.select(e.ctrlKey || e.shiftKey);
}
preventDefault = false;
@@ -537,8 +639,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
});
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].includes(CurrentUserUtils.SelectedTool))) {
+ 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
@@ -548,49 +651,51 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
this._downX = e.clientX;
this._downY = e.clientY;
- if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) &&
+ if (Doc.ActiveTool === InkTool.None && !(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
- !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ if (
+ (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ !this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
!e.ctrlKey &&
(e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
- !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ !DocListCast(Doc.MyOverlayDocs?.data).includes(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);
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointermove', this.onPointerMove);
}
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ document.addEventListener('pointerup', this.onPointerUp);
}
- }
+ };
onPointerMove = (e: PointerEvent): void => {
if (e.cancelBubble) return;
- if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) 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 && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ 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.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType);
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.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();
}
- }
+ };
cleanupPointerEvents = () => {
this.cleanUpInteractions();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.removeEventListener('pointerup', this.onPointerUp);
+ };
onPointerUp = (e: PointerEvent): void => {
this.cleanupPointerEvents();
@@ -598,62 +703,75 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
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);
+ 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
+ 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
}
- }
+ };
- @undoBatch @action
- toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => {
+ @undoBatch
+ @action
+ toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = !this.Document._isLinkButton;
- setPushpin && (this.Document.isPushpin = this.Document._isLinkButton);
+ if (setPushpin) {
+ this.Document.isPushpin = !this.Document.isPushpin;
+ this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton;
+ } else {
+ this.Document._isLinkButton = !this.Document._isLinkButton;
+ }
if (this.Document._isLinkButton && !this.onClickHandler) {
- this.Document.followLinkZoom = zoom;
+ zoom !== undefined && (this.Document.followLinkZoom = zoom);
this.Document.followLinkLocation = location;
} else if (this.Document._isLinkButton && this.onClickHandler) {
this.Document._isLinkButton = false;
- this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
+ this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
}
- }
- @undoBatch @action
+ };
+ @undoBatch
+ @action
toggleTargetOnClick = (): void => {
this.Document.ignoreClick = false;
this.Document._isLinkButton = true;
this.Document.isPushpin = true;
- }
- @undoBatch @action
- followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => {
+ };
+ @undoBatch
+ @action
+ followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
this.Document.ignoreClick = false;
this.Document._isLinkButton = true;
this.Document.isPushpin = false;
this.Document.followLinkZoom = zoom;
this.Document.followLinkLocation = location;
- }
- @undoBatch @action
+ };
+ @undoBatch
+ @action
selectOnClick = (): void => {
this.Document.ignoreClick = false;
this.Document._isLinkButton = false;
this.Document.isPushpin = false;
this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
+ };
@undoBatch
noOnClick = (): void => {
this.Document.ignoreClick = false;
this.Document._isLinkButton = false;
- }
+ };
@undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
- @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" });
+ @undoBatch setToggleDetail = () =>
+ (this.Document.onClick = ScriptField.MakeScript(
+ `toggleDetail(documentView, "${StrCast(this.Document.layoutKey)
+ .replace('layout_', '')
+ .replace(/^layout$/, 'detail')}")`,
+ { documentView: 'any' }
+ ));
- @undoBatch @action
+ @undoBatch
+ @action
drop = async (e: Event, de: DragManager.DropEvent) => {
if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
- if (this.props.Document === CurrentUserUtils.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.");
+ 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;
@@ -665,34 +783,34 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document;
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, "link", undefined, undefined, undefined, [de.x, de.y]);
+ de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
}
}
- }
+ };
@undoBatch
@action
makeIntoPortal = async () => {
const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
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");
+ 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');
}
- this.Document.followLinkLocation = "inPlace";
+ this.Document.followLinkLocation = 'inPlace';
this.Document.followLinkZoom = true;
this.Document._isLinkButton = true;
- }
+ };
@action
onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
- if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) {
+ if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) {
e.preventDefault();
e.stopPropagation();
//!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false);
}
// the touch onContextMenu is button 0, the pointer onContextMenu is button 2
if (e) {
- if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) {
+ if ((e.button === 0 && !e.ctrlKey) || e.isDefaultPrevented()) {
e.preventDefault();
return;
}
@@ -700,7 +818,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
e.stopPropagation();
e.persist();
- if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
+ if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
return;
}
}
@@ -709,17 +827,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
if (e && !(e.nativeEvent as any).dash) {
- const onDisplay = () => setTimeout(() => {
- DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ const onDisplay = () =>
setTimeout(() => {
- const ele = document.elementFromPoint(e.clientX, e.clientY);
- simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY);
+ DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ setTimeout(() => {
+ const ele = document.elementFromPoint(e.clientX, e.clientY);
+ simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY);
+ });
});
- });
- if (navigator.userAgent.includes("Macintosh")) {
+ if (navigator.userAgent.includes('Macintosh')) {
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay);
- }
- else {
+ } else {
onDisplay();
}
return;
@@ -727,227 +845,292 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
- cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
- this.props.contextMenuItems?.().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 }));
+ cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: 'sticky-note' })
+ );
+ this.props
+ .contextMenuItems?.()
+ .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 appearance = cm.findByDescription("UI Controls...");
- const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : [];
- !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" });
- !Doc.UserDoc().noviceMode && appearanceItems.push({
- description: "Add a Field", event: () => {
- const alias = Doc.MakeAlias(this.rootDoc);
- alias.layout = FormattedTextBox.LayoutString("newfield");
- alias.title = "newfield";
- alias._yMargin = 10;
- 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" });
+ 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, 'add:right'), 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' });
- if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
- !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" });
- const existingOnClick = cm.findByDescription("OnClick...");
- const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
+ !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' });
+ const existingOnClick = cm.findByDescription('OnClick...');
+ const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
- const zorders = cm.findByDescription("ZOrder...");
- const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : [];
+ const zorders = cm.findByDescription('ZOrder...');
+ const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
if (this.props.bringToFront !== emptyFunction) {
- 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: 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" });
+ 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: 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',
+ });
}
- !zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" });
+ !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
- onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
- !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" });
- onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+ !Doc.noviceMode && 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", true, 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.isPushpin ? "Remove" : "Make") + " Pushpin", 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" });
+ 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', true, 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.isPushpin ? 'Remove' : 'Make') + ' Pushpin', 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" });
+ 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' });
}
}
const funcs: ContextMenuProps[] = [];
- if (!Doc.UserDoc().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 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" });
+ 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 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' });
}
- const more = cm.findByDescription("More...");
- const moreItems = more && "subitems" in more ? more.subitems : [];
+ const more = cm.findByDescription('More...');
+ const moreItems = more && 'subitems' in more ? more.subitems : [];
if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" });
- if (!Doc.UserDoc().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" });
+ (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' });
if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
- moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
- moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
- moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
+ moreItems.push({ description: 'Export to Google Photos Album', event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: 'caret-square-right' });
+ moreItems.push({ description: 'Tag Child Images via Google Photos', event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: 'caret-square-right' });
+ moreItems.push({ description: 'Write Back Link to Album', event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: 'caret-square-right' });
}
- moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" });
+ moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
}
}
- if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.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" });
+ if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && 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' });
}
-
- const help = cm.findByDescription("Help...");
- const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
- !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
- helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
- !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
- !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" });
- cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
+ !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+
+ const help = cm.findByDescription('Help...');
+ const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+ helpItems.push({ description: 'Show Fields ', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), 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 }), 'add:right'), 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' });
+ cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
}
if (!this.topMost) 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)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined;
+ return this.collectionFilters().length || this.collectionRangeDocFilters().length ? 'hasFilter' : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).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);
- contentScaling = () => this.ContentScale;
onClickFunc = () => this.onClickHandler;
- setHeight = (height: number) => {
- if (this.props.renderDepth !== -1) {
- this.layoutDoc._height = height;
- }
- }
- setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
+ setHeight = (height: number) => (this.layoutDoc._height = height);
+ setContentView = action((view: { getAnchor?: () => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() === false ? false : (
- CurrentUserUtils.SelectedTool !== InkTool.None ||
- SnappingManager.GetIsDragging() ||
- this.rootSelected() ||
- this.props.Document.forceActive ||
- this.props.isSelected(outsideReaction) ||
- this._componentView?.isAnyChildContentActive?.() ||
- this.props.isContentActive()) ? true : undefined;
- }
+ return this.props.isContentActive() === false
+ ? false
+ : Doc.ActiveTool !== InkTool.None ||
+ SnappingManager.GetIsDragging() ||
+ this.rootSelected() ||
+ this.props.Document.forceActive ||
+ this.props.isSelected(outsideReaction) ||
+ this._componentView?.isAnyChildContentActive?.() ||
+ this.props.isContentActive()
+ ? true
+ : undefined;
+ };
@observable _retryThumb = 1;
- thumbShown = () => !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb &&
- !this._componentView?.isAnyChildContentActive?.() ? true : false;
+ thumbShown = () => {
+ return !this.props.isSelected() &&
+ LightboxView.LightboxDoc !== this.rootDoc &&
+ this.thumb &&
+ !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
+ !Doc.isBrushedHighlightedDegree(this.props.Document) &&
+ !this._componentView?.isAnyChildContentActive?.()
+ ? true
+ : false;
+ };
+ linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale;
@computed get contents() {
TraceMobx();
- const audioView = !this.layoutDoc._showAudio ? (null) :
- <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter} >
- <FontAwesomeIcon className="documentView-audioFont"
- style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }}
- icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" />
- </div>;
- return <div className="documentView-contentsView"
- style={{
- pointerEvents: this.props.pointerEvents as any ? this.props.pointerEvents as any : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"),
- height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
- }}>
- {!this._retryThumb || !this.thumbShown() ? (null) :
- <img style={{ background: "white", top: 0, position: "absolute" }} src={this.thumb} // + '?d=' + (new Date()).getTime()}
- width={this.props.PanelWidth()} height={this.props.PanelHeight()}
- onError={(e: any) => {
- setTimeout(action(() => this._retryThumb = 0), 0);
- setTimeout(action(() => this._retryThumb = 1), 150);
- }} />}
- <DocumentContentsView key={1}
- {...this.props}
- docViewPath={this.props.viewPath}
- thumbShown={this.thumbShown}
- setContentView={this.setContentView}
- scaling={this.contentScaling}
- PanelHeight={this.panelHeight}
- setHeight={this.setHeight}
- isContentActive={this.isContentActive}
- ScreenToLocalTransform={this.screenToLocal}
- rootSelected={this.rootSelected}
- onClick={this.onClickFunc}
- focus={this.focus}
- layoutKey={this.finalLayoutKey} />
- {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
- {this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) :
- <DocumentLinksButton View={this.props.DocumentView()}
- ContentScaling={this.props.ContentScaling}
- Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />
- }
- {audioView}
- </div>;
+ const audioView = !this.layoutDoc._showAudio ? null : (
+ <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter}>
+ <FontAwesomeIcon
+ className="documentView-audioFont"
+ style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }}
+ icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'microphone' : 'file-audio'}
+ size="sm"
+ />
+ </div>
+ );
+
+ 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',
+ height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+ }}>
+ {!this._retryThumb || !this.thumbShown() ? null : (
+ <img
+ style={{ background: 'white', top: 0, position: 'relative' }}
+ src={this.thumb} // + '?d=' + (new Date()).getTime()}
+ width={this.props.PanelWidth()}
+ height={this.props.PanelHeight()}
+ onError={(e: any) => {
+ setTimeout(
+ action(() => (this._retryThumb = 0)),
+ 0
+ );
+ setTimeout(
+ action(() => (this._retryThumb = 1)),
+ 150
+ );
+ }}
+ />
+ )}
+ <DocumentContentsView
+ key={1}
+ {...this.props}
+ docViewPath={this.props.viewPath}
+ thumbShown={this.thumbShown}
+ isHovering={this.isHovering}
+ setContentView={this.setContentView}
+ NativeDimScaling={this.props.NativeDimScaling}
+ PanelHeight={this.panelHeight}
+ setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
+ isContentActive={this.isContentActive}
+ ScreenToLocalTransform={this.screenToLocal}
+ rootSelected={this.rootSelected}
+ onClick={this.onClickFunc}
+ focus={this.focus}
+ layoutKey={this.finalLayoutKey}
+ />
+ {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
+ {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? null : (
+ <DocumentLinksButton
+ View={this.props.DocumentView()}
+ scaling={this.linkButtonInverseScaling}
+ Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -30]}
+ />
+ )}
+ {audioView}
+ </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";
+ 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)
+ 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 => {
switch (property) {
- case StyleProp.ShowTitle: return "";
- case StyleProp.PointerEvents: return "none";
- case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox
- default: return this.props.styleProvider?.(doc, props, property);
+ case StyleProp.ShowTitle:
+ return '';
+ case StyleProp.PointerEvents:
+ return 'none';
+ case StyleProp.LinkSource:
+ return this.props.Document; // pass the LinkSource to the LinkAnchorBox
+ default:
+ 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.,
+ };
+ // 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.,
// - 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.
// For each of these, we create LinkAnchorBox's on the border of the DocumentView.
@computed get directLinks() {
- 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))
+ 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))
);
}
- @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); }
- @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links
+ @computed get allLinks() {
+ TraceMobx();
+ return LinkManager.Instance.getAllRelatedLinks(this.rootDoc);
+ }
+ @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.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) 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.hidden);
- return filtered.map((link, i) =>
+ return filtered.map((link, i) => (
<div className="documentView-anchorCont" key={link[Id]}>
- <DocumentView {...this.props}
+ <DocumentView
+ {...this.props}
+ isContentActive={returnFalse}
Document={link}
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
@@ -958,80 +1141,87 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
styleProvider={this.anchorStyleProvider}
removeDocument={this.hideLinkAnchor}
LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} />
- </div >);
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)}
+ />
+ </div>
+ ));
}
@action
onPointerEnter = () => {
const self = this;
- const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]);
+ const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']);
if (audioAnnos && audioAnnos.length && this._mediaState === 0) {
const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
- anno.data instanceof AudioField && new Howl({
- src: [anno.data.url.href],
- format: ["mp3"],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => self._mediaState = 0);
- }
- });
+ anno.data instanceof AudioField &&
+ new Howl({
+ src: [anno.data.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: function () {
+ runInAction(() => (self._mediaState = 0));
+ },
+ });
this._mediaState = 1;
}
- }
+ };
recordAudioAnnotation = () => {
let gumStream: any;
let recorder: any;
const self = this;
- navigator.mediaDevices.getUserMedia({
- audio: true
- }).then(function (stream) {
- gumStream = stream;
- recorder = new MediaRecorder(stream);
- recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(e.data);
- if (!(result instanceof Error)) {
- const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 });
- audioDoc.treeViewExpandedView = "layout";
- const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc));
- if (audioAnnos === undefined) {
- self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]);
- } else {
- audioAnnos.push(audioDoc);
+ navigator.mediaDevices
+ .getUserMedia({
+ audio: true,
+ })
+ .then(function (stream) {
+ gumStream = stream;
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer(e.data);
+ if (!(result instanceof Error)) {
+ const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 });
+ audioDoc.treeViewExpandedView = 'layout';
+ const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc));
+ if (audioAnnos === undefined) {
+ self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]);
+ } else {
+ audioAnnos.push(audioDoc);
+ }
}
- }
- };
- runInAction(() => self._mediaState = 2);
- recorder.start();
- setTimeout(() => {
- recorder.stop();
- runInAction(() => self._mediaState = 0);
- gumStream.getAudioTracks()[0].stop();
- }, 5000);
- });
- }
+ };
+ runInAction(() => (self._mediaState = 2));
+ recorder.start();
+ setTimeout(() => {
+ recorder.stop();
+ runInAction(() => (self._mediaState = 0));
+ gumStream.getAudioTracks()[0].stop();
+ }, 5000);
+ });
+ };
- captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption");
+ captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, 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 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) :
- <div className="documentView-captionWrapper"
+ const captionView = !showCaption ? null : (
+ <div
+ className="documentView-captionWrapper"
style={{
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
- maxHeight: `max(100%, ${20 * ffscale()}px)`
+ maxHeight: `max(100%, ${20 * ffscale()}px)`,
}}>
- <FormattedTextBox {...OmitKeys(this.props, ['children']).omit}
+ <FormattedTextBox
+ {...OmitKeys(this.props, ['children']).omit}
yPadding={10}
xPadding={10}
fieldKey={showCaption}
- fontSize={12 * Math.max(1, 2 * ffscale() / 3)}
+ fontSize={12 * Math.max(1, (2 * ffscale()) / 3)}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
noSidebar={true}
@@ -1039,202 +1229,321 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
isContentActive={this.isContentActive}
onClick={this.onClickFunc}
/>
- </div>;
- const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc);
- const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)");
- const titleView = !showTitle ? (null) :
- <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{
- position: this.headerMargin ? "relative" : "absolute",
- height: this.titleHeight,
- color: lightOrDark(background),
- background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
- }}>
- <EditableView ref={this._titleRef}
- contents={showTitle.split(";").map(field => field.trim()).map(field => targetDoc[field]?.toString()).join("\\")}
- display={"block"}
+ </div>
+ );
+ const targetDoc = 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)'
+ );
+ const titleView = !showTitle ? null : (
+ <div
+ className={`documentView-titleWrapper${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
+ color: lightOrDark(background),
+ background,
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ }}>
+ <EditableView
+ ref={this._titleRef}
+ contents={showTitle
+ .split(';')
+ .map(field => field.trim())
+ .map(field => targetDoc[field]?.toString())
+ .join('\\')}
+ display={'block'}
fontSize={10}
- GetValue={() => showTitle.split(";").length === 1 ? showTitle + "=" + Field.toString(targetDoc[showTitle.split(";")[0]] as any as Field) : "#" + showTitle}
+ GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
SetValue={undoBatch((input: string) => {
- if (input?.startsWith("#")) {
+ if (input?.startsWith('#')) {
if (this.props.showTitle) {
this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else {
- Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : "creationDate";
+ Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
}
} 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;
+ 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);
}
return true;
})}
/>
- </div>;
- return this.props.hideTitle || (!showTitle && !showCaption) ?
- this.contents :
- <div className="documentView-styleWrapper" >
- {!this.headerMargin ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>}
+ </div>
+ );
+ return this.props.hideTitle || (!showTitle && !showCaption) ? (
+ this.contents
+ ) : (
+ <div className="documentView-styleWrapper">
+ {!this.headerMargin ? (
+ <>
+ {' '}
+ {this.contents} {titleView}{' '}
+ </>
+ ) : (
+ <>
+ {' '}
+ {titleView} {this.contents}{' '}
+ </>
+ )}
{captionView}
- </div>;
+ </div>
+ );
}
- @observable _: string = "";
+ isHovering = () => this._isHovering;
+ @observable _isHovering = false;
+ @observable _: string = '';
@computed get renderDoc() {
TraceMobx();
- const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png");
+ 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={{
- background: isButton || thumb ? undefined : this.backgroundColor,
- opacity: this.opacity,
- color: StrCast(this.layoutDoc.color, "inherit"),
- fontFamily: StrCast(this.Document._fontFamily, "inherit"),
- fontSize: Cast(this.Document._fontSize, "string", null),
- transformOrigin: this._animateScalingTo ? "center center" : undefined,
- transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform 0.5s 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>;
+ if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null;
+ return (
+ this.docContents ?? (
+ <div
+ className={`documentView-node${this.topMost ? '-topmost' : ''}`}
+ id={this.props.Document[Id]}
+ onPointerEnter={action(() => (this._isHovering = true))}
+ onPointerLeave={action(() => (this._isHovering = false))}
+ style={{
+ background: isButton || thumb ? undefined : this.backgroundColor,
+ opacity: this.opacity,
+ 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>
+ )
+ );
}
render() {
TraceMobx();
- const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
- const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
- const highlightStyle = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"][highlightIndex];
+ const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : Doc.DocBrushStatus.unbrushed) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
+ const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+ const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
const excludeTypes = !this.props.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
- highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
+ highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== '[pres element template]'; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
- const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
- this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
-
- // Return surrounding highlight
- return <div className={DocumentView.ROOT_DIV} ref={this._mainCont}
- onContextMenu={this.onContextMenu}
- onKeyDown={this.onKeyDown}
- onPointerDown={this.onPointerDown}
- onClick={this.onClick}
- onPointerEnter={e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document)}
- onPointerLeave={e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document)}
- style={{
- borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents,
- outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px",
- border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
- boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined
- }}>
- {!borderPath.path ? internal :
- <>
- {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
+ const boxShadow = this.props.treeViewDoc
+ ? null
+ : highlighting && this.borderRounding && highlightStyle !== 'dashed'
+ ? `0 0 0 ${highlightIndex}px ${highlightColor}`
+ : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+
+ // Return surrounding highlight
+ return (
+ <div
+ className={`${DocumentView.ROOT_DIV} docView-hack`}
+ ref={this._mainCont}
+ onContextMenu={this.onContextMenu}
+ onKeyDown={this.onKeyDown}
+ onPointerDown={this.onPointerDown}
+ onClick={this.onClick}
+ onPointerEnter={action(e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))}
+ onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))}
+ style={{
+ display: this.hidden ? 'inline' : undefined,
+ borderRadius: this.borderRounding,
+ pointerEvents: this.pointerEvents,
+ outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
+ border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
+ boxShadow,
+ clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
+ }}>
+ {!borderPath.path ? (
+ internal
+ ) : (
+ <>
+ {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
{internal}
</div> */}
- {internal}
- <div key="border2" className="documentView-customBorder" style={{ pointerEvents: "none" }} >
- <svg style={{ overflow: "visible" }} 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); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
- />
- : (null)}
- </div>;
+ {internal}
+ <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
+ <svg style={{ overflow: 'visible' }} 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}
+ </div>
+ );
}
}
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
- public static ROOT_DIV = "documentView-effectsWrapper";
- public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
+ public static ROOT_DIV = 'documentView-effectsWrapper';
+ public get displayName() {
+ return 'DocumentView(' + this.props.Document?.title + ')';
+ } // this makes mobx trace() statements more descriptive
public ContentRef = React.createRef<HTMLDivElement>();
private _disposers: { [name: string]: IReactionDisposer } = {};
+ 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);
+ });
+ }
+
@observable public docView: DocumentViewInternal | undefined | null;
- get Document() { return this.props.Document; }
- get topMost() { return this.props.renderDepth === 0; }
- get rootDoc() { return this.docView?.rootDoc || this.Document; }
- get dataDoc() { return this.docView?.dataDoc || this.Document; }
- get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; }
- get ContentDiv() { return this.docView?.ContentDiv; }
- get ComponentView() { return this.docView?._componentView; }
- get allLinks() { return this.docView?.allLinks || []; }
- get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; }
- get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; }
-
- @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; }
- @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); }
+ showContextMenu(pageX: number, pageY: number) {
+ return this.docView?.onContextMenu(undefined, pageX, pageY);
+ }
+ get Document() {
+ return this.props.Document;
+ }
+ get topMost() {
+ return this.props.renderDepth === 0;
+ }
+ get rootDoc() {
+ return this.docView?.rootDoc || this.Document;
+ }
+ get dataDoc() {
+ return this.docView?.dataDoc || this.Document;
+ }
+ get finalLayoutKey() {
+ return this.docView?.finalLayoutKey || 'layout';
+ }
+ get ContentDiv() {
+ return this.docView?.ContentDiv;
+ }
+ get ComponentView() {
+ return this.docView?._componentView;
+ }
+ get allLinks() {
+ return this.docView?.allLinks || [];
+ }
+ get LayoutFieldKey() {
+ return this.docView?.LayoutFieldKey || 'layout';
+ }
+ get fitWidth() {
+ return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth;
+ }
+
+ @computed get docViewPath(): DocumentView[] {
+ return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
+ }
+ @computed get layoutDoc() {
+ 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.props.freezeDimensions));
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
}
@computed get nativeHeight() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
- returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
+ }
+ @computed get shouldNotScale() {
+ return (this.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+ }
+ @computed get effectiveNativeWidth() {
+ return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
+ }
+ @computed get effectiveNativeHeight() {
+ return this.shouldNotScale ? 0 : this.nativeHeight || NumCast(this.layoutDoc.height);
}
- @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); }
- @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); }
- @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); }
@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
+ return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or 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 panelWidth() {
+ return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth();
+ }
@computed get panelHeight() {
- if (this.effectiveNativeHeight) {
- return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling);
+ 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);
}
return this.props.PanelHeight();
}
- @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; }
- @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; }
- @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; }
- @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
+ @computed get Xshift() {
+ return this.effectiveNativeWidth ? (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()))
+ ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2)
+ : 0;
+ }
+ @computed get centeringX() {
+ return this.props.dontCenter?.includes('x') ? 0 : this.Xshift;
+ }
+ @computed get centeringY() {
+ return this.props.dontCenter?.includes('y') ? 0 : this.Yshift;
+ }
- toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
+ 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.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+ if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
}
- const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse();
+ const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
- const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
+ const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
}
return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
- }
-
- public iconify() {
- const layoutKey = Cast(this.Document.layoutKey, "string", null);
- if (layoutKey !== "layout_icon") {
- this.switchViews(true, "icon");
- if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", "");
+ };
+
+ public iconify(finished?: () => void) {
+ this.ComponentView?.updateIcon?.();
+ const layoutKey = Cast(this.Document.layoutKey, 'string', null);
+ if (layoutKey !== 'layout_icon') {
+ this.switchViews(true, 'icon', finished);
+ if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
} else {
- const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null);
- this.switchViews(deiconifyLayout ? true : false, deiconifyLayout);
+ const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
+ this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
this.Document.deiconifyLayout = undefined;
+ this.props.bringToFront(this.rootDoc);
}
}
@undoBatch
@@ -1242,14 +1551,27 @@ export class DocumentView extends React.Component<DocumentViewProps> {
setCustomView = (custom: boolean, layout: string): void => {
Doc.setNativeView(this.props.Document);
custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
- }
- switchViews = action((custom: boolean, view: string) => {
- this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
- setTimeout(action(() => {
- this.setCustomView(custom, view);
- this.docView && (this.docView._animateScalingTo = 1); // expand it
- setTimeout(action(() => this.docView && (this.docView._animateScalingTo = 0)), 400);
- }), 400);
+ };
+ switchViews = action((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;
+ } else {
+ this.setCustomView(custom, view);
+ }
+ this.docView && (this.docView._animateScalingTo = 1); // expand it
+ setTimeout(
+ action(() => {
+ this.docView && (this.docView._animateScalingTo = 0);
+ finished?.();
+ }),
+ this.docView!._animateScaleTime - 10
+ );
+ }),
+ this.docView!._animateScaleTime - 10
+ );
});
startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
@@ -1261,12 +1583,18 @@ export class DocumentView extends React.Component<DocumentViewProps> {
NativeHeight = () => this.effectiveNativeHeight;
PanelWidth = () => this.panelWidth;
PanelHeight = () => this.panelHeight;
- ContentScale = () => this.nativeScaling;
+ NativeDimScaling = () => this.nativeScaling;
selfView = () => this;
- screenToLocalTransform = () => {
- return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
- }
+ screenToLocalTransform = () =>
+ this.props
+ .ScreenToLocalTransform()
+ .translate(-this.centeringX, -this.centeringY)
+ .scale(1 / this.nativeScaling);
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,
+ output => output
+ );
this._disposers.height = reaction(
() => NumCast(this.layoutDoc._height),
action(height => {
@@ -1285,37 +1613,76 @@ export class DocumentView extends React.Component<DocumentViewProps> {
TraceMobx();
const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
+ const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES;
const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
- return (<div className="contentFittingDocumentView">
- {!this.props.Document || !this.props.PanelWidth() ? (null) : (
- <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
- style={{
- position: this.props.Document.isInkMask ? "absolute" : undefined,
- 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()}%`),
- }}>
- <DocumentViewInternal {...this.props}
- DocumentView={this.selfView}
- viewPath={this.docViewPathFunc}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- NativeWidth={this.NativeWidth}
- NativeHeight={this.NativeHeight}
- isSelected={this.isSelected}
- select={this.select}
- ContentScaling={this.ContentScale}
- ScreenToLocalTransform={this.screenToLocalTransform}
- focus={this.props.focus || emptyFunction}
- bringToFront={emptyFunction}
- ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} />
- </div>)}
- </div>);
+ return (
+ <div className="contentFittingDocumentView">
+ {!this.props.Document || !this.props.PanelWidth() ? null : (
+ <div
+ className="contentFittingDocumentView-previewDoc"
+ ref={this.ContentRef}
+ style={{
+ transition: this.props.dataTransition,
+ position: this.props.Document.isInkMask ? 'absolute' : undefined,
+ transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: isButton || isPresTreeElement ? '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()}%`),
+ }}>
+ <DocumentViewInternal
+ {...this.props}
+ DocumentView={this.selfView}
+ viewPath={this.docViewPathFunc}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ NativeWidth={this.NativeWidth}
+ NativeHeight={this.NativeHeight}
+ NativeDimScaling={this.NativeDimScaling}
+ isSelected={this.isSelected}
+ select={this.select}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ focus={this.props.focus || emptyFunction}
+ ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
+ />
+ </div>
+ )}
+ </div>
+ );
}
}
+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 toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
- if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout");
- else dv.switchViews(true, detailLayoutKeySuffix);
-}); \ No newline at end of file
+ if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
+});
+
+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);
+ 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;
+ wid += otherdoc[WidthSym]();
+ Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', alias);
+ }
+ });
+ return links;
+});
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 6c9d53d10..9714e1bd0 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -1,3 +1,8 @@
+@import "../global/globalCssVariables.scss";
+
.equationBox-cont {
transform-origin: top left;
+ > span {
+ width: 100%;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index c170f9867..0bd30bce9 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -12,11 +12,12 @@ import { LightboxView } from '../LightboxView';
import './EquationBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
-
@observer
export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(EquationBox, fieldKey); }
- public static SelectOnLoad: string = "";
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(EquationBox, fieldKey);
+ }
+ public static SelectOnLoad: string = '';
_ref: React.RefObject<EquationEditor> = React.createRef();
componentDidMount() {
if (EquationBox.SelectOnLoad === this.rootDoc[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
@@ -25,75 +26,92 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._ref.current!.mathField.focus();
this._ref.current!.mathField.select();
}
- reaction(() => StrCast(this.dataDoc.text),
+ reaction(
+ () => StrCast(this.dataDoc.text),
text => {
if (text && text !== this._ref.current!.mathField.latex()) {
this._ref.current!.mathField.latex(text);
}
- });
- reaction(() => this.props.isSelected(),
+ }
+ );
+ reaction(
+ () => this.props.isSelected(),
selected => {
if (this._ref.current) {
- if (selected) this._ref.current.element.current.children[0].addEventListener("keydown", this.keyPressed, true);
- else this._ref.current.element.current.children[0].removeEventListener("keydown", this.keyPressed);
+ if (selected) this._ref.current.element.current.children[0].addEventListener('keydown', this.keyPressed, true);
+ else this._ref.current.element.current.children[0].removeEventListener('keydown', this.keyPressed);
}
- }, { fireImmediately: true });
+ },
+ { fireImmediately: true }
+ );
}
plot: any;
@action
keyPressed = (e: KeyboardEvent) => {
- const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace("px", ""));
- const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace("px", ""));
- if (e.key === "Enter") {
+ const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', ''));
+ const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', ''));
+ if (e.key === 'Enter') {
const nextEq = Docs.Create.EquationDocument({
- title: "# math", text: StrCast(this.dataDoc.text), _width, _height: 25,
- x: NumCast(this.layoutDoc.x), y: NumCast(this.layoutDoc.y) + _height + 10
+ title: '# math',
+ text: StrCast(this.dataDoc.text),
+ _width,
+ _height: 25,
+ x: NumCast(this.layoutDoc.x),
+ y: NumCast(this.layoutDoc.y) + _height + 10,
});
EquationBox.SelectOnLoad = nextEq[Id];
this.props.addDocument?.(nextEq);
e.stopPropagation();
-
}
- if (e.key === "Tab") {
+ if (e.key === 'Tab') {
const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], {
x: NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym](),
y: NumCast(this.layoutDoc.y),
- _width: 400, _height: 300, backgroundColor: "white"
+ _width: 400,
+ _height: 300,
+ backgroundColor: 'white',
});
this.props.addDocument?.(graph);
e.stopPropagation();
}
- if (e.key === "Backspace" && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc);
- }
+ if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc);
+ };
onChange = (str: string) => {
this.dataDoc.text = str;
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", ""));
+ 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);
}
- }
+ };
render() {
TraceMobx();
- const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
- return (<div className="equationBox-cont"
- onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
- style={{
- transform: `scale(${scale})`,
- width: `${100 / scale}%`,
- height: `${100 / scale}%`,
- pointerEvents: !this.props.isSelected() ? "none" : undefined,
- }}
- 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" />
- </div>);
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 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);
+ }}
+ className="equationBox-cont"
+ onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
+ style={{
+ transform: `scale(${scale})`,
+ width: 'fit-content', // `${100 / scale}%`,
+ height: `${100 / scale}%`,
+ pointerEvents: !this.props.isSelected() ? 'none' : undefined,
+ }}
+ 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" />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 943b9f153..dd2c13391 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -1,16 +1,17 @@
-import React = require("react");
-import { computed } from "mobx";
-import { observer } from "mobx-react";
-import { DateField } from "../../../fields/DateField";
-import { Doc, Field, FieldResult } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { WebField } from "../../../fields/URLField";
-import { DocumentViewSharedProps } from "./DocumentView";
+import React = require('react');
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { DateField } from '../../../fields/DateField';
+import { Doc, Field, FieldResult, Opt } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ScriptField } from '../../../fields/ScriptField';
+import { WebField } from '../../../fields/URLField';
+import { DocumentViewSharedProps } from './DocumentView';
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
// However, that only happens because the properties are "defined" in the markup for the field view.
-// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
+// See the LayoutString method on each field view : ImageBox, FormattedTextBox, etc.
//
export interface FieldViewProps extends DocumentViewSharedProps {
// FieldView specific props that are not part of DocumentView props
@@ -21,11 +22,13 @@ export interface FieldViewProps extends DocumentViewSharedProps {
isContentActive: (outsideReaction?: boolean) => boolean | undefined;
isDocumentActive?: () => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
- scaling?: () => number;
- setHeight: (height: number) => void;
+ 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
+ onBrowseClick?: () => ScriptField | undefined;
+ onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => boolean | undefined;
// properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
- pointerEvents?: string;
+ pointerEvents?: () => Opt<string>;
fontSize?: number;
height?: number;
width?: number;
@@ -38,7 +41,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string) {
- return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+ return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
}
@computed
@@ -71,23 +74,22 @@ export class FieldView extends React.Component<FieldViewProps> {
//}
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>;
+ } 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)) {
+ } else if (!(field instanceof Promise)) {
return <p>{Field.toString(field)}</p>;
- }
- else {
- return <p> {"Waiting for server..."} </p>;
+ } else {
+ return <p> {'Waiting for server...'} </p>;
}
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index ba65acee0..dc3fc0396 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -1,52 +1,80 @@
-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, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-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 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");
+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) CurrentUserUtils.setupFilterDocs(targetDoc);
+ if (targetDoc && !targetDoc.currentFilter) 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 static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); }
public _filterSelected = false;
- public _filterMatch = "matched";
+ public _filterMatch = 'matched';
@computed static get currentContainingCollectionDoc() {
let docView: any = SelectionManager.Views()[0];
let doc = docView.Document;
- while (docView && doc && doc.type !== "collection") {
+ while (docView && doc && doc.type !== 'collection') {
doc = docView.props.ContainingCollectionDoc;
docView = docView.props.ContainingCollectionView;
}
@@ -58,7 +86,6 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
// * @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() {
@@ -70,57 +97,59 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
// // 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
- */
+ * @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] : CurrentUserUtils.ActiveDashboard;
+ 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 SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || 'data';
}
- return "data";
+ return 'data';
}
@computed static get targetDocChildren() {
- return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || CurrentUserUtils.ActiveDashboard.data);
+ return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || Doc.ActiveDashboard?.data);
}
@observable _loaded = false;
componentDidMount() {
- reaction(() => DocListCastAsync(this.layoutDoc.data),
- async (activeTabsAsync) => {
+ reaction(
+ () => DocListCastAsync(this.layoutDoc[this.fieldKey]),
+ async activeTabsAsync => {
const activeTabs = await activeTabsAsync;
activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction));
- runInAction(() => this._loaded = true);
- }, { fireImmediately: true });
+ runInAction(() => (this._loaded = true));
+ },
+ { fireImmediately: true }
+ );
}
- @observable _allDocs = [] as Doc[];
+
@computed get allDocs() {
// trace();
+ const allDocs = new Set<Doc>();
const targetDoc = FilterBox.targetDoc;
if (this._loaded && targetDoc) {
- const allDocs = new Set<Doc>();
- const activeTabs = FilterBox.targetDocChildren;
- SearchBox.foreachRecursiveDoc(activeTabs, (depth, doc) => allDocs.add(doc));
- setTimeout(action(() => this._allDocs = Array.from(allDocs)));
+ SearchBox.foreachRecursiveDoc(FilterBox.targetDocChildren, (depth, doc) => allDocs.add(doc));
}
- return this._allDocs;
+ return Array.from(allDocs);
}
@computed get _allFacets() {
// trace();
- const noviceReqFields = ["author", "tags", "text", "type"];
- const noviceLayoutFields: string[] = [];//["_curPage"];
+ 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.UserDoc().noviceMode).sort();
+ 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
*/
@@ -138,26 +167,26 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
gatherFieldValues(childDocs: Doc[], facetKey: string) {
const valueSet = new Set<string>();
let rtFields = 0;
- childDocs.forEach((d) => {
+ 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];
+ 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) => {
+ 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));
+ const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
+ DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
});
subDocs = newarray;
}
@@ -166,36 +195,39 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
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;
+ 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;
- 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);
+ 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);
+ 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);
@@ -204,49 +236,68 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader);
let nonNumbers = 0;
- let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
+ let minVal = Number.MAX_VALUE,
+ maxVal = -Number.MAX_VALUE;
facetValues.strings.map(val => {
const num = val ? Number(val) : Number.NaN;
if (Number.isNaN(num)) {
- nonNumbers++;
+ 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
+ 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 < .1) {
+ 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",
+ 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) * .1));
- const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * .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 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.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.layout = CollectionView.LayoutString('data');
+ newFacet.layoutKey = 'layout';
newFacet.type = DocumentType.COL;
newFacet.target = targetDoc;
newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
@@ -254,11 +305,11 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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 });
+ 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;
}
@@ -267,8 +318,8 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
@action
changeBool = (e: any) => {
- (FilterBox.targetDoc.currentFilter as Doc).filterBoolean = e.currentTarget.value;
- }
+ FilterBox.targetDoc && (DocCast(FilterBox.targetDoc.currentFilter).filterBoolean = e.currentTarget.value);
+ };
/**
* Changes whether to select matched or unmatched documents
@@ -276,7 +327,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
changeMatch = (e: any) => {
this._filterMatch = e.currentTarget.value;
- }
+ };
@action
changeSelected = () => {
@@ -287,151 +338,149 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._filterSelected = true;
// helper method to select specified docs
}
- }
+ };
FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) {
- switch (property.split(":")[0]) {
+ 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 (
+ <>
+ <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("")!;
+ suppressChildClick = () => ScriptField.MakeScript('')!;
/**
* Adds a filterDoc to the list of saved filters
*/
saveFilter = () => {
- Doc.AddDocToList(Doc.UserDoc(), "savedFilters", this.props.Document);
- }
+ Doc.AddDocToList(Doc.UserDoc(), 'savedFilters', this.props.Document);
+ };
/**
* Changes the title of the filterDoc
*/
onTitleValueChange = (val: string) => {
- this.props.Document.title = val || `FilterDoc for ${FilterBox.targetDoc.title}`;
+ 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: 50, border: "2px" }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}>
- {StrCast(doc.title)}
- </div>
- </>;
- }
- );
+ return (
+ <>
+ <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 50, 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)));
+ 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>
+ 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-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)}
- value={null}
- closeMenuOnSelect={true}
- />
- </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}
- 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}
- treeViewHideHeaderFields={false}
- dontRegisterView={true}
- styleProvider={this.FilterStyleProvider}
- layerProvider={this.props.layerProvider}
- 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">
+ <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>
@@ -442,57 +491,91 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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 style={{ display: 'flex' }}>
+ <div className="filterBox-saveWrapper">
+ <div className="filterBox-saveBookmark" onPointerDown={this.saveFilter}>
+ <div>SAVE</div>
+ </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 className="filterBox-saveWrapper">
+ <div className="filterBox-saveBookmark">
+ <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}>
+ <div>FILTERS</div>
+ </Flyout>
+ </div>
</div>
- </div>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark"
- onPointerDown={this.props.createNewFilterDoc}
- >
- <div>NEW</div>
+ <div className="filterBox-saveWrapper">
+ <div className="filterBox-saveBookmark" onPointerDown={this.props.createNewFilterDoc}>
+ <div>NEW</div>
+ </div>
</div>
</div>
</div>
</div>
- </div>;
+ );
}
}
ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
- const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []);
+ const docFilters = Cast(layoutDoc._docFilters, listSpec('string'), []);
for (const filter of docFilters) {
- const fields = filter.split(":"); // split into key:value:modifiers
+ 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)));
+ 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 > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => {
+ 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();
@@ -500,8 +583,8 @@ ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, fa
doc.facetHeader = facetHeader;
doc.facetValue = facetValue;
doc.treeViewHideHeaderFields = true;
- doc.treeViewChecked = ComputedField.MakeFunction("determineCheckedState(self.target, self.facetHeader, self.facetValue)");
+ doc.treeViewChecked = ComputedField.MakeFunction('determineCheckedState(self.target, self.facetHeader, self.facetValue)');
return doc;
});
return new List<Doc>(facetValueDocSet);
-}); \ No newline at end of file
+});
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 3ab0a3ff2..15d0f88f6 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -1,4 +1,4 @@
-import functionPlot from "function-plot";
+import functionPlot from 'function-plot';
import { action, computed, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -12,7 +12,6 @@ import { Docs } from '../../documents/Documents';
import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
-
const EquationSchema = createSchema({});
type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSchema]>;
@@ -20,74 +19,83 @@ const EquationDocument = makeInterface(EquationSchema, documentSchema);
@observer
export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FunctionPlotBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(FunctionPlotBox, fieldKey);
+ }
public static GraphCount = 0;
_plot: any;
- _plotId = "";
+ _plotId = '';
_plotEle: any;
constructor(props: any) {
super(props);
- this._plotId = "graph" + FunctionPlotBox.GraphCount++;
+ this._plotId = 'graph' + FunctionPlotBox.GraphCount++;
}
componentDidMount() {
this.props.setContentView?.(this);
- reaction(() => [DocListCast(this.dataDoc.data).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
- () => this.createGraph());
+ reaction(
+ () => [DocListCast(this.dataDoc[this.fieldKey]).lastElement()?.text, this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+ () => this.createGraph()
+ );
}
getAnchor = () => {
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));
return anchor;
- }
+ };
@action
scrollFocus = (doc: Doc, smooth: boolean) => {
- 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]))));
+ 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();
const height = this.props.PanelHeight();
- const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, "x^2").replace(/\\frac\{(.*)\}\{(.*)\}/, "($1/$2)");
+ const fn = StrCast(DocListCast(this.dataDoc.data).lastElement()?.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)');
try {
this._plot = functionPlot({
- target: "#" + this._plotEle.id,
+ 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.dataDoc.xRange, listSpec('number'), [-10, 10]) },
+ yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
grid: true,
data: [
{
fn,
// derivative: { fn: "2 * x", updateOnMouseMove: true }
- }
- ]
+ },
+ ],
});
} catch (e) {
console.log(e);
}
- }
+ };
@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()} />;
+ return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
}
render() {
TraceMobx();
- return (<div
- style={{
- pointerEvents: !this.isContentActive() ? "all" : undefined,
- width: this.props.PanelWidth(),
- height: this.props.PanelHeight()
- }}
- >
- {this.theGraph}
- <div style={{
- display: this.props.isSelected() ? "none" : undefined, position: "absolute", width: "100%", height: "100%",
- pointerEvents: "all"
- }} />
- </div>);
+ return (
+ <div
+ style={{
+ pointerEvents: !this.isContentActive() ? 'all' : undefined,
+ width: this.props.PanelWidth(),
+ height: this.props.PanelHeight(),
+ }}>
+ {this.theGraph}
+ <div
+ style={{
+ display: this.props.isSelected() ? 'none' : undefined,
+ position: 'absolute',
+ width: '100%',
+ height: '100%',
+ pointerEvents: 'all',
+ }}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 4238f6d29..cd2b23f02 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -103,7 +103,7 @@
display: flex;
height: 100%;
- .imageBox-fadeBlocker {
+ .imageBox-fadeBlocker, .imageBox-fadeBlocker-hover{
width: 100%;
height: 100%;
position: absolute;
@@ -133,12 +133,10 @@
transition: opacity 1s ease-in-out;
}
-.imageBox:hover {
- .imageBox-fadeBlocker {
- -webkit-transition: opacity 1s ease-in-out;
- -moz-transition: opacity 1s ease-in-out;
- -o-transition: opacity 1s ease-in-out;
- transition: opacity 1s ease-in-out;
- opacity: 0;
- }
+.imageBox-fadeBlocker-hover {
+ -webkit-transition: opacity 1s ease-in-out;
+ -moz-transition: opacity 1s ease-in-out;
+ -o-transition: opacity 1s ease-in-out;
+ transition: opacity 1s ease-in-out;
+ opacity: 0;
} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0a4168698..9590bcb15 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,7 @@
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
-import { observer } from "mobx-react";
+import { observer } from 'mobx-react';
import { extname } from 'path';
-import { DataSym, Doc, DocListCast, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -14,11 +14,12 @@ import { TraceMobx } from '../../../fields/util';
import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
+import { DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
-import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
-import { ContextMenu } from "../../views/ContextMenu";
+import { ContextMenu } from '../../views/ContextMenu';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -27,62 +28,64 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
-import "./ImageBox.scss";
-import React = require("react");
+import './ImageBox.scss';
+import React = require('react');
export const pageSchema = createSchema({
- googlePhotosUrl: "string",
- googlePhotosTags: "string"
+ googlePhotosUrl: 'string',
+ googlePhotosTags: 'string',
});
const uploadIcons = {
- idle: "downarrow.png",
- loading: "loading.gif",
- success: "greencheck.png",
- failure: "redx.png"
+ idle: 'downarrow.png',
+ loading: 'loading.gif',
+ success: 'greencheck.png',
+ failure: 'redx.png',
};
@observer
export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- protected _multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
- private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
+ protected _multiTouchDisposer?: import('../../util/InteractionUtils').InteractionUtils.MultiTouchEventDisposer | undefined;
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(ImageBox, fieldKey);
+ }
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
- @observable _curSuffix = "";
+ private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ @observable _curSuffix = '';
@observable _uploadIcon = uploadIcons.idle;
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
- }
- setViewSpec = (anchor: Doc, preview: boolean) => {
-
- } // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
-
+ };
+ setViewSpec = (anchor: Doc, preview: boolean) => {}; // 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 = () => {
- const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations);
+ const anchor = this._getAnchor?.(this._savedAnnotations);
anchor && this.addDocument(anchor);
return anchor ?? this.rootDoc;
- }
+ };
componentDidMount() {
this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop....
- this._disposers.sizer = reaction(() => (
- {
+ 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],
- selected: this.props.isSelected()
+ scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
+ selected: this.props.isSelected(),
}),
- ({ forceFull, scrSize, selected }) => this._curSuffix = forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o",
- { fireImmediately: true, delay: 1000 });
- this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
+ ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'),
+ { fireImmediately: true, delay: 1000 }
+ );
+ this._disposers.path = reaction(
+ () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
({ nativeSize, width }) => {
- if (!this.layoutDoc._height) {
- this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth;
+ if (true || !this.layoutDoc._height) {
+ this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
}
},
- { fireImmediately: true });
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -94,10 +97,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
if (de.metaKey) {
- de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
- Doc.AddDocToList(this.dataDoc, this.fieldKey + "-alternates", drop);
- e.stopPropagation();
- }));
+ de.complete.docDragData.droppedDocuments.forEach(
+ action((drop: Doc) => {
+ Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
+ e.stopPropagation();
+ })
+ );
} else if (de.altKey || !this.dataDoc[this.fieldKey]) {
const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
const targetField = Doc.LayoutFieldKey(layoutDoc);
@@ -110,69 +115,111 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
}
- }
+ };
@undoBatch
- resolution = () => this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes
+ resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
@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 + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360;
+ this.dataDoc[this.fieldKey + '-nativeWidth'] = nh;
+ this.dataDoc[this.fieldKey + '-nativeHeight'] = nw;
this.layoutDoc._width = h;
this.layoutDoc._height = w;
});
+ crop = (region: Doc | undefined, addCrop?: boolean) => {
+ if (!region) return;
+ const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).lockedPosition = true;
+ Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
+ Doc.GetProto(region).isPushpin = true;
+ this.addDocument(region);
+ const anchx = NumCast(cropping.x);
+ const anchy = NumCast(cropping.y);
+ const anchw = NumCast(cropping._width);
+ const anchh = NumCast(cropping._height);
+ 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;
+ const croppingProto = Doc.GetProto(cropping);
+ croppingProto.annotationOn = undefined;
+ croppingProto.isPrototype = 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 = 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;
+ if (addCrop) {
+ DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ }
+ this.props.bringToFront(cropping);
+ return cropping;
+ };
+
specificContextMenu = (e: React.MouseEvent): void => {
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: "Copy path", event: () => Utils.CopyText(this.choosePath(field.url)), icon: "expand-arrows-alt" });
- if (!Doc.UserDoc().noviceMode) {
- funcs.push({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
-
- const existingAnalyze = ContextMenu.Instance?.findByDescription("Analyzers...");
- const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
- modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
- modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
+ 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: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
+ if (!Doc.noviceMode) {
+ funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
+
+ const existingAnalyze = ContextMenu.Instance?.findByDescription('Analyzers...');
+ const modes: ContextMenuProps[] = existingAnalyze && 'subitems' in existingAnalyze ? existingAnalyze.subitems : [];
+ modes.push({ description: 'Generate Tags', event: this.generateMetadata, icon: 'tag' });
+ modes.push({ description: 'Find Faces', event: this.extractFaces, icon: 'camera' });
//modes.push({ description: "Recommend", event: this.extractText, icon: "brain" });
- !existingAnalyze && ContextMenu.Instance?.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
+ !existingAnalyze && ContextMenu.Instance?.addItem({ description: 'Analyzers...', subitems: modes, icon: 'hand-point-right' });
}
- ContextMenu.Instance?.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ ContextMenu.Instance?.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
- }
+ };
extractFaces = () => {
const converter = (results: any) => {
return results.map((face: CognitiveServices.Image.Face) => Doc.Get.FromJson({ data: face, title: `Face: ${face.faceId}` })!);
};
- this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter);
- }
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-faces'], this.url, Service.Face, converter);
+ };
generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
const converter = (results: any) => {
- const tagDoc = new Doc;
+ const tagDoc = new Doc();
const tagsList = new List();
results.tags.map((tag: Tag) => {
tagsList.push(tag.name);
- const sanitized = tag.name.replace(" ", "_");
+ const sanitized = tag.name.replace(' ', '_');
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
- this.dataDoc[this.fieldKey + "-generatedTags"] = tagsList;
- tagDoc.title = "Generated Tags Doc";
+ this.dataDoc[this.fieldKey + '-generatedTags'] = tagsList;
+ tagDoc.title = 'Generated Tags Doc';
tagDoc.confidence = threshold;
return tagDoc;
};
- this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
- }
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + '-generatedTagsDoc'], this.url, Service.ComputerVision, converter);
+ };
@computed private get url() {
const data = Cast(this.dataDoc[this.fieldKey], ImageField);
@@ -181,9 +228,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
choosePath(url: URL) {
const lower = url.href.toLowerCase();
- if (url.protocol === "data") return url.href;
+ if (url.protocol === 'data') return url.href;
if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here
const ext = extname(url.href);
return url.href.replace(ext, this._curSuffix + ext);
@@ -191,40 +238,36 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
considerGooglePhotosLink = () => {
const remoteUrl = this.dataDoc.googlePhotosUrl;
- return !remoteUrl ? (null) : (<img draggable={false}
- style={{ transformOrigin: "bottom right" }}
- id={"google-photos"}
- src={"/assets/google_photos.png"}
- onClick={() => window.open(remoteUrl)}
- />);
- }
+ return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />;
+ };
considerGooglePhotosTags = () => {
const tags = this.dataDoc.googlePhotosTags;
- return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
- }
+ return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
+ };
@computed
private get considerDownloadIcon() {
const data = this.dataDoc[this.fieldKey];
if (!(data instanceof ImageField)) {
- return (null);
+ return null;
}
const primary = data.url.href;
if (primary.includes(window.location.origin)) {
- return (null);
+ return null;
}
return (
<img
- id={"upload-icon"} draggable={false}
- style={{ transformOrigin: "bottom right" }}
+ id={'upload-icon'}
+ draggable={false}
+ style={{ transformOrigin: 'bottom right' }}
src={`/assets/${this._uploadIcon}`}
onClick={async () => {
const { dataDoc } = this;
const { success, failure, idle, loading } = uploadIcons;
- runInAction(() => this._uploadIcon = loading);
- const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
- dataDoc[this.props.fieldKey + "-originalUrl"] = primary;
+ runInAction(() => (this._uploadIcon = loading));
+ const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [primary] });
+ dataDoc[this.props.fieldKey + '-originalUrl'] = primary;
let succeeded = true;
let data: ImageField | undefined;
try {
@@ -232,13 +275,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
} catch {
succeeded = false;
}
- runInAction(() => this._uploadIcon = succeeded ? success : failure);
- setTimeout(action(() => {
- this._uploadIcon = idle;
- if (data) {
- dataDoc[this.fieldKey] = data;
- }
- }), 2000);
+ runInAction(() => (this._uploadIcon = succeeded ? success : failure));
+ setTimeout(
+ action(() => {
+ this._uploadIcon = idle;
+ if (data) {
+ dataDoc[this.fieldKey] = data;
+ }
+ }),
+ 2000
+ );
}}
/>
);
@@ -246,18 +292,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get nativeSize() {
TraceMobx();
- const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 500);
- const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1);
- const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + "-nativeOrientation"], 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'], 1));
+ const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
@computed get paths() {
const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
- const alts = DocListCast(this.dataDoc[this.fieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
- const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+ const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+ const altpaths = alts
+ .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
+ .filter(url => url)
+ .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
const 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('http://www.cs.brown.edu/~bcz/noImage.png')];
}
@computed get content() {
@@ -266,51 +315,38 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const srcpath = this.paths[0];
const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
- const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]);
+ const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
- let transformOrigin = "center center";
+ let transformOrigin = 'center center';
let transform = `translate(0%, 0%) rotate(${rotation}deg) scale(${aspect})`;
if (rotation === 90 || rotation === -270) {
- transformOrigin = "top left";
+ transformOrigin = 'top left';
transform = `translate(100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
} else if (rotation === 180) {
transform = `rotate(${rotation}deg) scale(${aspect})`;
} else if (rotation === 270 || rotation === -90) {
- transformOrigin = "right top";
+ transformOrigin = 'right top';
transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
}
- 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 }} >
- <img key="paths" ref={this._imgRef}
- src={srcpath}
- style={{ transform, transformOrigin }}
- draggable={false}
- width={nativeWidth} />
- {fadepath === srcpath ? (null) : <div className="imageBox-fadeBlocker">
- <img className="imageBox-fadeaway" key={"fadeaway"} ref={this._imgRef}
- src={fadepath}
- style={{ transform, transformOrigin }} draggable={false}
- width={nativeWidth} />
- </div>}
+ 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 }}>
+ <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+ {fadepath === srcpath ? null : (
+ <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}>
+ <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+ </div>
+ )}
+ </div>
+ {this.considerDownloadIcon}
+ {this.considerGooglePhotosLink()}
+ <FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} />
</div>
- {this.considerDownloadIcon}
- {this.considerGooglePhotosLink()}
- <FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
- </div>;
- }
-
- // adjust y position to center image in panel aspect is bigger than image aspect.
- // bcz :note, this is broken for rotated images
- get ycenter() {
- const { nativeWidth, nativeHeight } = this.nativeSize;
- const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]);
- const aspect = (rotation % 180) ? nativeWidth / nativeHeight : nativeHeight / nativeWidth;
- return this.props.PanelHeight() / this.props.PanelWidth() > aspect ?
- (this.props.PanelHeight() - this.props.PanelWidth() * aspect) / 2 : 0;
+ );
}
- screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter);
+ screenToLocalTransform = this.props.ScreenToLocalTransform;
contentFunc = () => [this.content];
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -322,63 +358,79 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
}
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ 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)) {
+ 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 = () => {
+ this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this._marqueeing = undefined;
this.props.select(false);
- }
+ };
+ savedAnnotations = () => this._savedAnnotations;
render() {
TraceMobx();
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
- const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / (this.props.scaling?.() || 1)}px` : borderRad;
- return (<div className="imageBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
- style={{
- width: this.props.PanelWidth() ? undefined : `100%`,
- height: this.props.PanelWidth() ? undefined : `100%`,
- pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
- borderRadius
- }} >
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- renderDepth={this.props.renderDepth + 1}
- fieldKey={this.annotationKey}
- CollectionView={undefined}
- isAnnotationOverlay={true}
- annotationLayerHostsContent={true}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={this.props.PanelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- select={emptyFunction}
- scaling={returnOne}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}>
- {this.contentFunc}
- </CollectionFreeFormView>
- {this.annotationLayer}
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator
- rootDoc={this.rootDoc}
- scrollTop={0}
- down={this._marqueeing}
- scaling={this.props.scaling}
- docView={this.props.docViewPath().slice(-1)[0]}
- addDocument={this.addDocument}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this._savedAnnotations}
- annotationLayer={this._annotationLayer.current}
- mainCont={this._mainCont.current}
- />}
- </div >);
+ const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this.props.NativeDimScaling?.() || 1)}px` : borderRad;
+ return (
+ <div
+ className="imageBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this._mainCont}
+ style={{
+ width: this.props.PanelWidth() ? undefined : `100%`,
+ height: this.props.PanelWidth() ? undefined : `100%`,
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ }}>
+ <CollectionFreeFormView
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ renderDepth={this.props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ isAnnotationOverlay={true}
+ annotationLayerHostsContent={true}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.props.PanelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ select={emptyFunction}
+ NativeDimScaling={returnOne}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ {this.annotationLayer}
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={this.props.NativeDimScaling}
+ docView={this.props.docViewPath().slice(-1)[0]}
+ addDocument={this.addDocument}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current}
+ anchorMenuCrop={this.crop}
+ />
+ )}
+ </div>
+ );
}
-
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index c44c8c828..4b1fbaf7d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -67,7 +67,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
const { script, type, onDelegate } = kvpScript;
//const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
- const target = forceOnDelegate || onDelegate ? doc : doc.proto || doc;
+ const target = forceOnDelegate || onDelegate || key.startsWith("_") ? doc : doc.proto || doc;
let field: Field;
if (type === "computed") {
field = new ComputedField(script);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 881cbf2bb..80def3025 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -60,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
docRangeFilters: returnEmptyFilter,
searchFilterDocs: returnEmptyDoclist,
styleProvider: DefaultStyleProvider,
- layerProvider: undefined,
docViewPath: returnEmptyDoclist,
ContainingCollectionView: undefined,
ContainingCollectionDoc: undefined,
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
index db43551d8..290152cd0 100644
--- a/src/client/views/nodes/LabelBigText.js
+++ b/src/client/views/nodes/LabelBigText.js
@@ -5,14 +5,14 @@ And from Jetroid/bigtext.js v1.0.0, September 2016
Usage:
BigText("#myElement",{
- rotateText: {Number}, (null)
- fontSizeFactor: {Number}, (0.8)
- maximumFontSize: {Number}, (null)
- limitingDimension: {String}, ("both")
- horizontalAlign: {String}, ("center")
- verticalAlign: {String}, ("center")
- textAlign: {String}, ("center")
- whiteSpace: {String}, ("nowrap")
+ rotateText: {Number}, (null)
+ fontSizeFactor: {Number}, (0.8)
+ maximumFontSize: {Number}, (null)
+ limitingDimension: {String}, ("both")
+ horizontalAlign: {String}, ("center")
+ verticalAlign: {String}, ("center")
+ textAlign: {String}, ("center")
+ whiteSpace: {String}, ("nowrap")
});
@@ -28,6 +28,8 @@ fontSizeFactor: This option is used to give some vertical spacing for letters th
maximumFontSize: maximum font size to use.
+minimumFontSize: minimum font size to use. if font is calculated smaller than this, text will be rendered at this size and wrapped
+
limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
@@ -98,7 +100,8 @@ export default function BigText(element, options) {
horizontalAlign: "center",
verticalAlign: "center",
textAlign: "center",
- whiteSpace: "nowrap"
+ whiteSpace: "nowrap",
+ singleLine: true
};
//Merge provided options and default options
@@ -109,20 +112,29 @@ export default function BigText(element, options) {
//Get variables which we will reference frequently
var style = element.style;
- var computedStyle = document.defaultView.getComputedStyle(element);
var parent = element.parentNode;
var parentStyle = parent.style;
var parentComputedStyle = document.defaultView.getComputedStyle(parent);
//hides the element to prevent "flashing"
style.visibility = "hidden";
-
//Set some properties
style.display = "inline-block";
style.clear = "both";
style.float = "left";
- style.fontSize = (1000 * options.fontSizeFactor) + "px";
- style.lineHeight = "1000px";
+ var fontSize = options.maximumFontSize;
+ if (options.singleLine) {
+ style.fontSize = (fontSize * options.fontSizeFactor) + "px";
+ style.lineHeight = fontSize + "px";
+ } else {
+ for (; fontSize > options.minimumFontSize; fontSize = fontSize - Math.min(fontSize / 2, Math.max(0, fontSize - 48) + 2)) {
+ style.fontSize = (fontSize * options.fontSizeFactor) + "px";
+ style.lineHeight = "1";
+ if (element.offsetHeight <= +parentComputedStyle.height.replace("px", "")) {
+ break;
+ }
+ }
+ }
style.whiteSpace = options.whiteSpace;
style.textAlign = options.textAlign;
style.position = "relative";
@@ -130,6 +142,7 @@ export default function BigText(element, options) {
style.margin = 0;
style.left = "50%";
style.top = "50%";
+ var computedStyle = document.defaultView.getComputedStyle(element);
//Get properties of parent to allow easier referencing later.
var parentPadding = {
@@ -154,6 +167,7 @@ export default function BigText(element, options) {
width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
height: element.offsetHeight,
};
+ if (!box.width || !box.height) return element;
if (options.rotateText !== null) {
@@ -172,22 +186,37 @@ export default function BigText(element, options) {
box.height = element.offsetWidth * sine + element.offsetHeight * cosine;
}
- var widthFactor = (parentInnerWidth - parentPadding.left - parentPadding.right) / box.width;
- var heightFactor = (parentInnerHeight - parentPadding.top - parentPadding.bottom) / box.height;
+ var parentWidth = (parentInnerWidth - parentPadding.left - parentPadding.right);
+ var parentHeight = (parentInnerHeight - parentPadding.top - parentPadding.bottom);
+ var widthFactor = parentWidth / box.width;
+ var heightFactor = parentHeight / box.height;
var lineHeight;
if (options.limitingDimension.toLowerCase() === "width") {
- lineHeight = Math.floor(widthFactor * 1000);
- parentStyle.height = lineHeight + "px";
+ lineHeight = Math.floor(widthFactor * fontSize);
} else if (options.limitingDimension.toLowerCase() === "height") {
- lineHeight = Math.floor(heightFactor * 1000);
+ lineHeight = Math.floor(heightFactor * fontSize);
} else if (widthFactor < heightFactor)
- lineHeight = Math.floor(widthFactor * 1000);
+ lineHeight = Math.floor(widthFactor * fontSize);
else if (widthFactor >= heightFactor)
- lineHeight = Math.floor(heightFactor * 1000);
+ lineHeight = Math.floor(heightFactor * fontSize);
var fontSize = lineHeight * options.fontSizeFactor;
- if (options.maximumFontSize !== null && fontSize > options.maximumFontSize) {
+ if (fontSize < options.minimumFontSize) {
+ parentStyle.display = "flex";
+ parentStyle.alignItems = "center";
+ style.textAlign = "center";
+ style.visibility = "";
+ style.fontSize = options.minimumFontSize + "px";
+ style.lineHeight = "";
+ style.overflow = "hidden";
+ style.textOverflow = "ellipsis";
+ style.top = "";
+ style.left = "";
+ style.margin = "";
+ return element;
+ }
+ if (options.maximumFontSize && fontSize > options.maximumFontSize) {
fontSize = options.maximumFontSize;
lineHeight = fontSize / options.fontSizeFactor;
}
@@ -197,11 +226,11 @@ export default function BigText(element, options) {
style.marginBottom = "0px";
style.marginRight = "0px";
- if (options.limitingDimension.toLowerCase() === "height") {
- //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
- //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
- parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
- }
+ // if (options.limitingDimension.toLowerCase() === "height") {
+ // //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
+ // //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
+ // parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
+ // }
//Calculate the inner width and height
var innerDimensions = _calculateInnerDimensions(computedStyle);
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 6a0d651d2..42e158584 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -4,7 +4,7 @@
border-radius: inherit;
display: flex;
flex-direction: column;
- position: absolute;
+ position: relative;
}
.labelBox-mainButton {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 4e0461650..b58a9affb 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
@@ -23,18 +23,23 @@ export interface LabelBoxProps {
@observer
export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps)>() {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); }
- public static LayoutStringWithTitle(fieldType: { name: string }, fieldStr: string, label: string) {
- return `<${fieldType.name} fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+ public static LayoutStringWithTitle(fieldStr: string, label?: string) {
+ return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
}
private dropDisposer?: DragManager.DragDropDisposer;
-
+ private _timeout: any;
componentDidMount() {
this.props.setContentView?.(this);
}
+ componentWillUnMount() {
+ this._timeout && clearTimeout(this._timeout);
+ }
getTitle() {
- return this.props.label ? this.props.label : this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) :
- 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) => {
@@ -47,14 +52,14 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; }
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({
+ !Doc.noviceMode && funcs.push({
description: "Clear Script Params", event: () => {
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
params?.map(p => this.paramsDoc[p] = undefined);
}, icon: "trash"
});
- ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
+ funcs.length && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
}
@undoBatch
@@ -73,8 +78,38 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
@observable _mouseOver = false;
@computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; }
+ fitTextToBox = (r: any): any => {
+ const singleLine = BoolCast(this.rootDoc._singleLine, true);
+ const params = {
+ rotateText: null,
+ fontSizeFactor: 1,
+ minimumFontSize: NumCast(this.rootDoc._minFontSize, 8),
+ maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000),
+ limitingDimension: "both",
+ horizontalAlign: "center",
+ verticalAlign: "center",
+ textAlign: "center",
+ singleLine,
+ whiteSpace: singleLine ? "nowrap" : "pre-wrap"
+ };
+ this._timeout = undefined;
+ if (!r) return params;
+ if (!r.offsetHeight || !r.offsetWidth) return this._timeout = setTimeout(() => this.fitTextToBox(r));
+ const parent = r.parentNode;
+ const parentStyle = parent.style;
+ parentStyle.display = "";
+ parentStyle.alignItems = "";
+ r.setAttribute("style", "");
+ r.style.width = singleLine ? "" : "100%";
+
+ r.style.textOverflow = "ellipsis";
+ r.style.overflow = "hidden";
+ BigText(r, params);
+ return params;
+ }
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
+ const boxParams = this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
@@ -87,28 +122,21 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
<div className="labelBox-mainButton" style={{
backgroundColor: this.hoverColor,
- fontSize: StrCast(this.layoutDoc._fontSize) || Math.min(18, this.props.PanelHeight() / 2),
+ fontSize: StrCast(this.layoutDoc._fontSize),
fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
+ paddingLeft: NumCast(this.rootDoc._xPadding),
+ paddingRight: NumCast(this.rootDoc._xPadding),
+ paddingTop: NumCast(this.rootDoc._yPadding),
+ paddingBottom: NumCast(this.rootDoc._yPadding),
width: this.props.PanelWidth(),
height: this.props.PanelHeight(),
- whiteSpace: this.layoutDoc._singleLine ? "pre" : "pre-wrap"
+ whiteSpace: boxParams.singleLine ? "pre" : "pre-wrap"
}} >
- <span ref={r => {
- if (r) {
- BigText(r, {
- rotateText: null,
- fontSizeFactor: 1,
- maximumFontSize: null,
- limitingDimension: "both",
- horizontalAlign: "center",
- verticalAlign: "center",
- textAlign: "center",
- whiteSpace: "nowrap"
- });
- }
- }}>{label.startsWith("#") ? (null) : label}</span>
+ <span style={{ width: boxParams.singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}>
+ {label.startsWith("#") ? (null) : label.replace(/([^a-zA-Z])/g, "$1\u200b")}
+ </span>
</div>
<div className="labelBox-fieldKeyParams" >
{!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 437d29f39..85a8622ec 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,30 +1,31 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from "../../../fields/util";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../fields/Doc';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { TraceMobx } from '../../../fields/util';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
-import { DragManager } from "../../util/DragManager";
-import { LinkManager } from "../../util/LinkManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { LinkEditor } from "../linking/LinkEditor";
-import { StyleProp } from "../StyleProvider";
-import { FieldView, FieldViewProps } from "./FieldView";
-import "./LinkAnchorBox.scss";
-import { LinkDocPreview } from "./LinkDocPreview";
-import React = require("react");
-const higflyout = require("@hig/flyout");
+import { DragManager } from '../../util/DragManager';
+import { LinkFollower } from '../../util/LinkFollower';
+import { SelectionManager } from '../../util/SelectionManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { LinkEditor } from '../linking/LinkEditor';
+import { StyleProp } from '../StyleProvider';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LinkAnchorBox.scss';
+import { LinkDocPreview } from './LinkDocPreview';
+import React = require('react');
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkAnchorBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(LinkAnchorBox, fieldKey);
+ }
_doubleTap = false;
_lastTap: number = 0;
_ref = React.createRef<HTMLDivElement>();
@@ -38,7 +39,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
- }
+ };
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
if (!this._isOpen && cdiv) {
@@ -47,20 +48,20 @@ 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 = 'alias';
+ dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'isLinkButton'];
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[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
+ this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
}
}
return false;
});
@action
onClick = (e: React.MouseEvent) => {
- if ((e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton)) {
+ if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) {
this.props.select(false);
}
if (!this._doubleTap && !e.ctrlKey && e.button < 2) {
@@ -68,10 +69,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._editing = true;
anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false);
if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) {
- this._timeout = setTimeout(action(() => {
- LinkManager.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
- this._editing = false;
- }), 300 - (Date.now() - this._lastTap));
+ this._timeout = setTimeout(
+ action(() => {
+ LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
+ this._editing = false;
+ }),
+ 300 - (Date.now() - this._lastTap)
+ );
e.stopPropagation();
}
} else {
@@ -81,18 +85,17 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.openLinkEditor(e);
e.stopPropagation();
}
- }
+ };
openLinkDocOnRight = (e: React.MouseEvent) => {
- this.props.addDocTab(this.rootDoc, "add:right");
- }
+ this.props.addDocTab(this.rootDoc, 'add:right');
+ };
openLinkTargetOnRight = (e: React.MouseEvent) => {
const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
alias._isLinkButton = undefined;
- alias._layerTags = undefined;
- alias.layoutKey = "layout";
- this.props.addDocTab(alias, "add:right");
- }
+ alias.layoutKey = 'layout';
+ this.props.addDocTab(alias, 'add:right');
+ };
@action
openLinkEditor = action((e: React.MouseEvent) => {
SelectionManager.DeselectAll();
@@ -101,56 +104,67 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Open Link Target on Right", event: () => this.openLinkTargetOnRight(e), icon: "eye" });
- funcs.push({ description: "Open Link on Right", event: () => this.openLinkDocOnRight(e), icon: "eye" });
- funcs.push({ description: "Open Link Editor", event: () => this.openLinkEditor(e), icon: "eye" });
- funcs.push({ description: "Toggle Always Show Link", event: () => this.props.Document.linkDisplay = !this.props.Document.linkDisplay, icon: "eye" });
+ funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' });
+ funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' });
+ funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' });
+ funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
- }
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ };
render() {
TraceMobx();
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 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 : .25;
+ 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 targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
const flyout = (
<div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}>
- <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => { })} />
- {!this._forceOpen ? (null) : <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}>
- <FontAwesomeIcon color="dimgray" icon={"times"} size={"sm"} />
- </div>}
+ <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} />
+ {!this._forceOpen ? null : (
+ <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}>
+ <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} />
+ </div>
+ )}
+ </div>
+ );
+ return (
+ <div
+ className={`linkAnchorBox-cont${small ? '-small' : ''}`}
+ onPointerLeave={LinkDocPreview.Clear}
+ onPointerEnter={e =>
+ LinkDocPreview.SetLinkInfo({
+ docProps: this.props,
+ linkSrc: linkSource,
+ linkDoc: this.rootDoc,
+ showHeader: true,
+ location: [e.clientX, e.clientY + 20],
+ })
+ }
+ onPointerDown={this.onPointerDown}
+ onClick={this.onClick}
+ title={targetTitle}
+ onContextMenu={this.specificContextMenu}
+ ref={this._ref}
+ style={{
+ background,
+ left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
+ top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
+ transform: `scale(${anchorScale})`,
+ }}>
+ {!this._editing && !this._forceOpen ? null : (
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}>
+ <span className="linkAnchorBox-button">
+ <FontAwesomeIcon icon={'eye'} size={'lg'} />
+ </span>
+ </Flyout>
+ )}
</div>
);
- return <div className={`linkAnchorBox-cont${small ? "-small" : ""}`}
- onPointerLeave={LinkDocPreview.Clear}
- onPointerEnter={e => LinkDocPreview.SetLinkInfo({
- docProps: this.props,
- linkSrc: linkSource,
- linkDoc: this.rootDoc,
- showHeader: true,
- location: [e.clientX, e.clientY + 20]
- })}
- onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu}
- ref={this._ref}
- style={{
- background,
- left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
- top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
- transform: `scale(${anchorScale})`
- }} >
- {!this._editing && !this._forceOpen ? (null) :
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => this._isOpen = true} onClose={action(() => this._isOpen = this._forceOpen = this._editing = false)}>
- <span className="linkAnchorBox-button" >
- <FontAwesomeIcon icon={"eye"} size={"lg"} />
- </span>
- </Flyout>}
- </div>;
}
}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index a9d33f161..91bd505c5 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -1,26 +1,24 @@
-import React = require("react");
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../../fields/Doc";
-import { LinkManager } from "../../util/LinkManager";
-import "./LinkDescriptionPopup.scss";
-import { TaskCompletionBox } from "./TaskCompletedBox";
-
+import React = require('react');
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../fields/Doc';
+import { LinkManager } from '../../util/LinkManager';
+import './LinkDescriptionPopup.scss';
+import { TaskCompletionBox } from './TaskCompletedBox';
@observer
export class LinkDescriptionPopup extends React.Component<{}> {
-
@observable public static descriptionPopup: boolean = false;
- @observable public static showDescriptions: string = "ON";
+ @observable public static showDescriptions: string = 'ON';
@observable public static popupX: number = 700;
@observable public static popupY: number = 350;
- @observable description: string = "";
+ @observable description: string = '';
@observable popupRef = React.createRef<HTMLDivElement>();
@action
descriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
this.description = e.currentTarget.value;
- }
+ };
@action
onDismiss = (add: boolean) => {
@@ -28,7 +26,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
if (add) {
LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description);
}
- }
+ };
@action
onClick = (e: PointerEvent) => {
@@ -36,34 +34,43 @@ export class LinkDescriptionPopup extends React.Component<{}> {
LinkDescriptionPopup.descriptionPopup = false;
TaskCompletionBox.taskCompleted = false;
}
- }
+ };
@action
componentDidMount() {
- document.addEventListener("pointerdown", this.onClick);
+ document.addEventListener('pointerdown', this.onClick, true);
}
componentWillUnmount() {
- document.removeEventListener("pointerdown", this.onClick);
+ document.removeEventListener('pointerdown', this.onClick, true);
}
render() {
- return <div className="linkDescriptionPopup" ref={this.popupRef}
- style={{
- left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700,
- top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350,
- }}>
- <input className="linkDescriptionPopup-input"
- onKeyPress={e => e.key === "Enter" && this.onDismiss(true)}
- placeholder={"(Optional) Enter link description..."}
- onChange={(e) => this.descriptionChanged(e)}>
- </input>
- <div className="linkDescriptionPopup-btn">
- <div className="linkDescriptionPopup-btn-dismiss"
- onPointerDown={e => this.onDismiss(false)}> Dismiss </div>
- <div className="linkDescriptionPopup-btn-add"
- onPointerDown={e => this.onDismiss(true)}> Add </div>
+ return (
+ <div
+ className="linkDescriptionPopup"
+ ref={this.popupRef}
+ style={{
+ left: LinkDescriptionPopup.popupX ? LinkDescriptionPopup.popupX : 700,
+ top: LinkDescriptionPopup.popupY ? LinkDescriptionPopup.popupY : 350,
+ }}>
+ <input
+ className="linkDescriptionPopup-input"
+ onKeyDown={e => e.stopPropagation()}
+ onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
+ placeholder={'(Optional) Enter link description...'}
+ onChange={e => this.descriptionChanged(e)}></input>
+ <div className="linkDescriptionPopup-btn">
+ <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}>
+ {' '}
+ Dismiss{' '}
+ </div>
+ <div className="linkDescriptionPopup-btn-add" onPointerDown={e => this.onDismiss(true)}>
+ {' '}
+ Add{' '}
+ </div>
+ </div>
</div>
- </div>;
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss
index 06ae466f0..c68e55f73 100644
--- a/src/client/views/nodes/LinkDocPreview.scss
+++ b/src/client/views/nodes/LinkDocPreview.scss
@@ -1,4 +1,4 @@
- .linkDocPreview {
+.linkDocPreview {
position: absolute;
pointer-events: all;
background-color: lightblue;
@@ -8,11 +8,15 @@
border-bottom: 8px solid white;
border-right: 8px solid white;
z-index: 2004;
+ cursor: pointer;
+
.linkDocPreview-inner {
background-color: white;
width: 100%;
height: 100%;
pointer-events: none;
+ display: flex;
+ flex-direction: column;
.linkDocPreview-info {
height: 37px;
@@ -21,6 +25,7 @@
.linkDocPreview-buttonBar {
float: right;
}
+
.linkDocPreview-title {
padding-right: 4px;
float: left;
@@ -59,13 +64,16 @@
cursor: pointer;
}
}
+ }
- .linkDocPreview-preview-wrapper {
- overflow: hidden;
- align-content: center;
- justify-content: center;
- background-color: rgb(160, 160, 160);
- }
+ .linkDocPreview-preview-wrapper {
+ overflow: hidden;
+ align-content: center;
+ justify-content: center;
+ pointer-events: all;
+ background-color: rgb(160, 160, 160);
+ overflow: auto;
+ cursor: grab;
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 2e29c0656..85b1f8d9e 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -1,20 +1,22 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
-import { observer } from "mobx-react";
-import wiki from "wikijs";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocCastAsync } from "../../../fields/Doc";
-import { NumCast, StrCast, Cast } from "../../../fields/Types";
-import { emptyFunction, emptyPath, returnEmptyDoclist, returnEmptyFilter, returnFalse, setupMoveUpEvents, Utils } from "../../../Utils";
+import { observer } from 'mobx-react';
+import wiki from 'wikijs';
+import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
import { DocServer } from '../../DocServer';
-import { Docs, DocUtils } from "../../documents/Documents";
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
+import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
-import { Transform } from "../../util/Transform";
+import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
-import { DocumentView, DocumentViewSharedProps } from "./DocumentView";
+import { DocumentView, DocumentViewSharedProps } from './DocumentView';
import './LinkDocPreview.scss';
-import React = require("react");
-import { DocumentType } from '../../documents/DocumentTypes';
+import React = require('react');
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -26,15 +28,20 @@ interface LinkDocPreviewProps {
}
@observer
export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
- @action public static Clear() { LinkDocPreview.LinkInfo = undefined; }
- @action public static SetLinkInfo(info?: LinkDocPreviewProps) { LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info); }
+ @action public static Clear() {
+ LinkDocPreview.LinkInfo = undefined;
+ }
+ @action public static SetLinkInfo(info?: LinkDocPreviewProps) {
+ LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info);
+ }
_infoRef = React.createRef<HTMLDivElement>();
+ _linkDocRef = React.createRef<HTMLDivElement>();
@observable public static LinkInfo: Opt<LinkDocPreviewProps>;
@observable _targetDoc: Opt<Doc>;
@observable _linkDoc: Opt<Doc>;
@observable _linkSrc: Opt<Doc>;
- @observable _toolTipText = "";
+ @observable _toolTipText = '';
@observable _hrefInd = 0;
@action init() {
@@ -46,94 +53,132 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
if (anchor1 && anchor2) {
linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1;
}
- if (linkTarget?.annotationOn) {
- linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => this._targetDoc = anno));
+ if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) {
+ // want to show annotation context document if annotation is not text
+ linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno)));
} else {
this._targetDoc = linkTarget;
}
- this._toolTipText = "";
+ this._toolTipText = '';
}
componentDidUpdate(props: any) {
if (props.linkSrc !== this.props.linkSrc || props.linkDoc !== this.props.linkDoc || props.hrefs !== this.props.hrefs) this.init();
}
componentDidMount() {
this.init();
- document.addEventListener("pointerdown", this.onPointerDown);
+ document.addEventListener('pointerdown', this.onPointerDown, true);
}
componentWillUnmount() {
LinkDocPreview.SetLinkInfo(undefined);
- document.removeEventListener("pointerdown", this.onPointerDown);
+ document.removeEventListener('pointerdown', this.onPointerDown, true);
}
onPointerDown = (e: PointerEvent) => {
- !this._infoRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview
- }
+ !this._linkDocRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview
+ };
@computed get href() {
if (this.props.hrefs?.length) {
const href = this.props.hrefs[this._hrefInd];
- if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview
- if (href.startsWith("https://en.wikipedia.org/wiki/")) {
- wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500))));
+ if (href.indexOf(Doc.localServerPath()) !== 0) {
+ // link to a web page URL -- try to show a preview
+ if (href.startsWith('https://en.wikipedia.org/wiki/')) {
+ wiki()
+ .page(href.replace('https://en.wikipedia.org/wiki/', ''))
+ .then(page => page.summary().then(action(summary => (this._toolTipText = summary.substring(0, 500)))));
} else {
- setTimeout(action(() => this._toolTipText = "url => " + href));
+ setTimeout(action(() => (this._toolTipText = 'url => ' + href)));
}
- } else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated
- const anchorDoc = href.replace(Doc.localServerPath(), "").split("?")[0];
- anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => {
- if (anchor instanceof Doc && DocListCast(anchor.links).length) {
- this._linkDoc = DocListCast(anchor.links)[0];
- this._linkSrc = anchor;
- const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- this._targetDoc = linkTarget?.type === DocumentType.MARKER && linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
- this._toolTipText = "";
- }
- }));
+ } else {
+ // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated
+ const anchorDoc = href.replace(Doc.localServerPath(), '').split('?')[0];
+ anchorDoc &&
+ DocServer.GetRefField(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 (automaticLink) {
+ // automatic links specify the target in the link info, not the source
+ const linkTarget = anchor;
+ this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
+ this._targetDoc = linkTarget;
+ } else {
+ this._linkSrc = anchor;
+ const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+ this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
+ }
+ this._toolTipText = '';
+ }
+ })
+ );
}
return href;
}
return undefined;
}
deleteLink = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc)));
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc))
+ );
+ };
nextHref = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1)));
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ action(() => {
+ const nextHrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1);
+ if (nextHrefInd !== this._hrefInd) {
+ this._linkDoc = undefined;
+ this._hrefInd = nextHrefInd;
+ }
+ }),
+ true
+ );
+ };
- followLink = (e: React.PointerEvent) => {
+ followLink = () => {
if (this._linkDoc && this._linkSrc) {
LinkDocPreview.Clear();
- LinkManager.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
+ LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
} else if (this.props.hrefs?.length) {
- this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _width: 200, _height: 400, useCors: true }), "add:right");
+ this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right');
}
- }
+ };
+
+ followLinkPointerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.followLink);
+
width = () => {
if (!this._targetDoc) return 225;
if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) {
- return Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]() / this._targetDoc[HeightSym]();
+ return (Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]()) / this._targetDoc[HeightSym]();
}
return Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225));
- }
+ };
height = () => {
if (!this._targetDoc) return 225;
if (this._targetDoc[WidthSym]() > this._targetDoc?.[HeightSym]()) {
- return Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]() / this._targetDoc[WidthSym]();
+ return (Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]()) / this._targetDoc[WidthSym]();
}
return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225));
- }
+ };
@computed get previewHeader() {
- return !this._linkDoc || !this._targetDoc || !this._linkSrc ? (null) :
- <div className="linkDocPreview-info" ref={this._infoRef}>
- <div className="linkDocPreview-title">
- {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + "..." : this._targetDoc.title}
+ return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : (
+ <div className="linkDocPreview-info">
+ <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
+ {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)}
<p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
</div>
- <div className="linkDocPreview-buttonBar" >
+ <div className="linkDocPreview-buttonBar">
<Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top">
- <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? "gray" : "green" }} onPointerDown={this.nextHref}>
+ <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}>
<FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" />
</div>
</Tooltip>
@@ -144,30 +189,54 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
</div>
</Tooltip>
</div>
- </div>;
+ </div>
+ );
}
@computed get docPreview() {
const href = this.href; // needs to be here to trigger lookup of web pages and docs on server
- return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? (null) :
+ return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
<div className="linkDocPreview-inner">
- {!this.props.showHeader ? (null) : this.previewHeader}
- <div className="linkDocPreview-preview-wrapper">
- {this._toolTipText ? this._toolTipText :
- <DocumentView ref={(r) => {
- const targetanchor = LinkManager.getOppositeAnchor(this._linkDoc!, this._linkSrc!);
- targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor);
- }}
+ {!this.props.showHeader ? null : this.previewHeader}
+ <div
+ className="linkDocPreview-preview-wrapper"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) => {
+ if (Math.abs(e.clientX - down[0]) + Math.abs(e.clientY - down[1]) > 100) {
+ DragManager.StartDocumentDrag([this._infoRef.current!], new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY);
+ LinkDocPreview.Clear();
+ return true;
+ }
+ return false;
+ },
+ emptyFunction,
+ this.followLink,
+ true
+ )
+ }
+ ref={this._infoRef}
+ style={{ maxHeight: this._toolTipText ? '100%' : undefined }}>
+ {this._toolTipText ? (
+ this._toolTipText
+ ) : (
+ <DocumentView
+ ref={r => {
+ const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+ targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor);
+ }}
Document={this._targetDoc!}
moveDocument={returnFalse}
rootSelected={returnFalse}
styleProvider={this.props.docProps?.styleProvider}
- layerProvider={this.props.docProps?.layerProvider}
docViewPath={returnEmptyDoclist}
ScreenToLocalTransform={Transform.Identity}
isDocumentActive={returnFalse}
- isContentActive={emptyFunction}
+ isContentActive={returnFalse}
addDocument={returnFalse}
+ showTitle={returnEmptyString}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -178,23 +247,33 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
ContainingCollectionDoc={undefined}
ContainingCollectionView={undefined}
renderDepth={-1}
+ suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
+ pointerEvents={returnNone}
focus={DocUtils.DefaultFocus}
whenChildContentsActiveChanged={returnFalse}
+ ignoreAutoHeight={true} // need to ignore autoHeight otherwise 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}
- />}
+ />
+ )}
</div>
- </div>;
+ </div>
+ );
}
render() {
const borders = 16; // 8px border on each side
- return <div className="linkDocPreview" onPointerDown={this.followLink}
- 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>;
+ return (
+ <div
+ className="linkDocPreview"
+ ref={this._linkDocRef}
+ onPointerDown={this.followLinkPointerDown}
+ 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>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index 854da5ed2..fb15520f6 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -35,7 +35,7 @@
.mapBox-wrapper {
width: 100%;
- .searchbox {
+ .mapBox-input {
box-sizing: border-box;
border: 1px solid transparent;
width: 240px;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index aa2130af5..6479e933e 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,18 +1,17 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
-import { action, computed, IReactionDisposer, observable, ObservableMap } from 'mobx';
-import { observer } from "mobx-react";
-import * as React from "react";
+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 { TraceMobx } from '../../../../fields/util';
import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
-import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
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';
@@ -20,19 +19,18 @@ import { MarqueeAnnotator } from '../../MarqueeAnnotator';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { Annotation } from '../../pdf/Annotation';
import { SidebarAnnos } from '../../SidebarAnnos';
-import { StyleProp } from '../../StyleProvider';
import { FieldView, FieldViewProps } from '../FieldView';
-import "./MapBox.scss";
+import './MapBox.scss';
import { MapBoxInfoWindow } from './MapBoxInfoWindow';
/**
* MapBox architecture:
* Main component: MapBox.tsx
* Supporting Components: SidebarAnnos, CollectionStackingView
- *
- * MapBox 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 MapBox 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,
+ *
+ * MapBox 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 MapBox 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
@@ -77,26 +75,30 @@ document.head.appendChild(script);
// },
// });
-
// 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
+ 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
+ types: ['establishment'], // type pf places, subject of change according to user need
} as google.maps.places.AutocompleteOptions;
@observer
export class MapBox 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(MapBox, fieldKey); }
- public get SidebarKey() { return this.fieldKey + "-sidebar"; }
+ showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(MapBox, 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); }
+ @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;
@@ -108,14 +110,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@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]); }
+ @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._showSidebar ? true : false; }
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ }
static _canAnnotate = true;
static _hadSelection: boolean = false;
@@ -129,109 +136,106 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@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()));
- }
+ 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
+ * @param controlDiv
+ * @param map
*/
private CenterControl = () => {
- const controlDiv = document.createElement("div");
- controlDiv.className = "mapBox-addMarker";
+ const controlDiv = document.createElement('div');
+ controlDiv.className = 'mapBox-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";
+ 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";
+ 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";
+ 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";
+ markerIcon.style.position = 'absolute';
+ markerIcon.innerHTML = 'Add';
controlUI.appendChild(markerIcon);
// Setup the click event listeners
- controlUI.addEventListener("click", () => {
+ 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)";
+ 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)";
+ 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
+ * @param map
*/
@action
private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
const marker = new google.maps.Marker({
position: position,
- map: map
+ 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
+ * fit map bounds to contain all markers
+ * @param map
*/
@action
private loadHandler = (map: google.maps.Map) => {
@@ -256,10 +260,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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.fitToBox && this.fitBounds(this._map);
+ this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
}
}, 250);
// listener to addmarker event
@@ -268,83 +271,83 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.placeMarker((e as any).latLng, map);
}
});
- }
+ };
@action
centered = () => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitToBox && this.fitBounds(this._map);
+ this.layoutDoc.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.fitToBox && this.fitBounds(this._map);
+ this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
}
this.dataDoc.mapZoom = this._map.getZoom();
- }
+ };
/**
* Load and render all map markers
- * @param marker
- * @param place
+ * @param marker
+ * @param place
*/
@action
private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
- place[Id] ? this.markerMap[place[Id]] = marker : null;
- }
+ 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
+ * @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
+ * @param doc
+ * @param sidebarKey
+ * @returns
*/
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- console.log("print all sidebar Docs");
+ console.log('print all sidebar Docs');
console.log(this.allSidebarDocs);
if (!this.layoutDoc._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";
+ doc.onClickBehavior = 'enterPortal';
if (existingMarker) {
- Doc.AddDocToList(existingMarker, "data", doc);
+ 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
- console.log("sidebaraddDocument");
+ console.log('sidebaraddDocument');
console.log(doc);
return this.addDocument(doc, sidebarKey); // add to sidebar list
- }
+ };
/**
* Removing documents from the sidebar
- * @param doc
- * @param sidebarKey
- * @returns
+ * @param doc
+ * @param sidebarKey
+ * @returns
*/
sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (this.layoutDoc._showSidebar) this.toggleSidebar();
@@ -354,31 +357,47 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
console.log(this.allSidebarDocs);
});
return this.removeDocument(doc, sidebarKey);
- }
+ };
/**
* Toggle sidebar onclick the tiny comment button on the top right corner
- * @param e
+ * @param e
*/
sidebarBtnDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e, down, delta) => {
- const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]);
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
- const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
- const ratio = (curNativeWidth + localDelta[0] / (this.props.scaling?.() || 1)) / nativeWidth;
- if (ratio >= 1) {
- this.layoutDoc.nativeWidth = nativeWidth * ratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0];
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
- }
- return false;
- }, emptyFunction, this.toggleSidebar);
- }
-
- sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
- @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
- @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
+ 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._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ } else {
+ this._showSidebar = false;
+ this.layoutDoc._width = mapWidth;
+ this.layoutDoc._sidebarWidthPercent = '0%';
+ }
+ return false;
+ }),
+ emptyFunction,
+ () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
+ );
+ };
+ sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ @computed get sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._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;
@@ -414,7 +433,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
// put temporary cutomized marker on searched location
- this.searchMarkers.forEach((marker) => {
+ this.searchMarkers.forEach(marker => {
marker.setMap(null);
});
this.searchMarkers = [];
@@ -426,100 +445,106 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
position: place.geometry.location,
})
);
- }
+ };
/**
* Handles toggle of sidebar on click the little comment button
*/
@computed get sidebarHandle() {
- TraceMobx();
- const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
- const color = !annotated ? Colors.WHITE : Colors.BLACK;
- const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props as any, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
- return (!annotated) ? (null) :
- <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
+ return (
+ <div
+ className="mapBox-overlayButton-sidebar"
+ key="sidebar"
+ title="Toggle Sidebar"
style={{
- left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`,
- backgroundColor: backgroundColor,
- color: color,
- opacity: annotated ? 1 : undefined
- }} >
- <FontAwesomeIcon icon={"comment-alt"} />
- </div>;
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._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._showSidebar = ((this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, "0%") === "0%" ? "50%" : "0%")) !== "0%";
- this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
- }
+ 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);
+ };
sidebarDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
- }
+ 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._sidebarWidthPercent = "" + 100 * Math.max(0, (1 - (e.clientX - bounds.left) / bounds.width)) + "%";
- this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== "0%";
+ this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
- }
+ };
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
+ 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].includes(CurrentUserUtils.SelectedTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ 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() {
- const pe = this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" :
- SnappingManager.GetIsDragging() ? undefined : "none";
- return <div className="mapBox-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={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />)}
- </div>;
+ return (
+ <div className="mapBox-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 = () => {
- const anchor =
- AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
- this.rootDoc;
+ const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? this.rootDoc;
return anchor;
- }
+ };
/**
* render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
- * @returns
+ * @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)}
- />
+ <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) => {
@@ -527,19 +552,19 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// if (SelectionManager.Views().lastElement()) {
// console.log(SelectionManager.Views().lastElement());
// }
- }
+ };
- panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ 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));
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;
render() {
- const renderAnnotations = (docFilters?: () => string[]) => (null);
+ 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
@@ -559,96 +584,84 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// docFilters={docFilters || this.props.docFilters}
// dontRenderDocuments={docFilters ? false : true}
// select={emptyFunction}
- // ContentScaling={returnOne}
// bringToFront={emptyFunction}
// whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
// removeDocument={this.removeDocument}
// moveDocument={this.moveDocument}
// addDocument={this.sidebarAddDocument}
- // childPointerEvents={true}
- // pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
- return <div className="mapBox" ref={this._ref}>
- {/*console.log(apiKey)*/}
- {/* <LoadScript
+ // 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 style={{ mixBlendMode: "multiply" }}>
- {renderAnnotations(this.transparentFilter)}
+ <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+ <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>
+ {!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}
+ mainCont={this._mainCont.current}
+ />
+ )}
</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 ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" />
- </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}
+ {/* </LoadScript > */}
+ <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- />)}
- {this.handleMapCenter(this._map)}
- </GoogleMap>
- {!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}
- mainCont={this._mainCont.current} />}
- </div>
- {/* </LoadScript > */}
- <div className="mapBox-sidebar"
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- <SidebarAnnos ref={this._sidebarRef}
- {...this.props}
- fieldKey={this.fieldKey}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- showSidebar={this.SidebarShown}
- nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- PanelWidth={this.sidebarWidth}
- sidebarAddDocument={this.sidebarAddDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.sidebarRemoveDocument}
- />
- </div>
- <div className="mapBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar"
- style={{
- display: !this.props.isContentActive() ? "none" : undefined,
- top: StrCast(this.rootDoc._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" />
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ {this.sidebarHandle}
</div>
- </div>;
+ );
}
}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 5e5f6cd74..00bedafbe 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -1,20 +1,18 @@
import { InfoWindow } from '@react-google-maps/api';
-import { action, computed } from 'mobx';
-import { observer } from "mobx-react";
-import * as React from "react";
+import { action } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
-import { CollectionViewType } from '../../collections/CollectionView';
import { ViewBoxAnnotatableProps } from '../../DocComponent';
import { FieldViewProps } from '../FieldView';
import { FormattedTextBox } from '../formattedText/FormattedTextBox';
-import "./MapBox.scss";
-
+import './MapBox.scss';
interface MapBoxInfoWindowProps {
place: Doc;
@@ -23,71 +21,74 @@ interface MapBoxInfoWindowProps {
isAnyChildContentActive: () => boolean;
}
@observer
-export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps>{
-
+export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & ViewBoxAnnotatableProps & FieldViewProps> {
@action
private handleInfoWindowClose = () => {
if (this.props.place.infoWindowOpen) {
this.props.place.infoWindowOpen = false;
}
this.props.place.infoWindowOpen = false;
- }
+ };
addNoteClick = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => {
- const newBox = Docs.Create.TextDocument("Note", { _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);
+ const newBox = Docs.Create.TextDocument('Note', { _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();
e.stopPropagation();
e.preventDefault();
});
- }
-
+ };
_stack: CollectionStackingView | CollectionNoteTakingView | null | undefined;
-
- // Collection stacking view for documents in the infowindow of a map marker
- @computed get renderChildDocs() {
- return;
- }
+ childFitWidth = (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() {
- return <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose} >
- <div className="mapbox-infowindow">
- <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
- <CollectionStackingView
- ref={r => this._stack = r}
- {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- Document={this.props.place}
- DataDoc={undefined}
- fieldKey="data"
- CollectionView={undefined}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- docFilters={returnEmptyFilter}
- setHeight={emptyFunction}
- isAnnotationOverlay={false}
- select={emptyFunction}
- scaling={returnOne}
- isContentActive={returnTrue}
- chromeHidden={true}
- rootSelected={returnFalse}
- childHideResizeHandles={returnTrue}
- childHideDecorationTitle={returnTrue}
- childFitWidth={doc => doc.type === DocumentType.RTF}
- // childDocumentsActive={returnFalse}
- removeDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean)}
- addDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean)}
- renderDepth={this.props.renderDepth + 1}
- viewType={CollectionViewType.Stacking}
- pointerEvents="all"
- />
+ return (
+ <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}>
+ <div className="mapbox-infowindow">
+ <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
+ <CollectionStackingView
+ ref={r => (this._stack = r)}
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ Document={this.props.place}
+ DataDoc={undefined}
+ fieldKey="data"
+ CollectionView={undefined}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ docFilters={returnEmptyFilter}
+ setHeight={emptyFunction}
+ isAnnotationOverlay={false}
+ select={emptyFunction}
+ NativeDimScaling={returnOne}
+ isContentActive={returnTrue}
+ chromeHidden={true}
+ rootSelected={returnFalse}
+ childHideResizeHandles={returnTrue}
+ childHideDecorationTitle={returnTrue}
+ childFitWidth={this.childFitWidth}
+ // childDocumentsActive={returnFalse}
+ removeDocument={this.removeDoc}
+ addDocument={this.addDoc}
+ renderDepth={this.props.renderDepth + 1}
+ viewType={CollectionViewType.Stacking}
+ pointerEvents={returnAll}
+ />
+ </div>
+ <hr />
+ <div
+ onPointerDown={this.addNoteClick}
+ onClick={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ }}>
+ Add Note
+ </div>
</div>
- <hr />
- <div onPointerDown={this.addNoteClick} onClick={e => { e.stopPropagation(); e.preventDefault(); }} >
- Add Note
- </div>
- </div>
- </InfoWindow>;
+ </InfoWindow>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 9807cee7c..345407c2f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,33 +1,40 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
-import { observer } from "mobx-react";
-import * as Pdfjs from "pdfjs-dist";
-import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, DocListCast, Opt, WidthSym } from "../../../fields/Doc";
-import { Cast, NumCast, StrCast, ImageCast } from '../../../fields/Types';
-import { PdfField } from "../../../fields/URLField";
+import { observer } from 'mobx-react';
+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 { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
import { KeyCodes } from '../../util/KeyCodes';
-import { undoBatch } from '../../util/UndoManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { Colors } from '../global/globalEnums';
-import { AnchorMenu } from '../pdf/AnchorMenu';
-import { PDFViewer } from "../pdf/PDFViewer";
+import { CreateImage } from '../nodes/WebBoxRenderer';
+import { PDFViewer } from '../pdf/PDFViewer';
import { SidebarAnnos } from '../SidebarAnnos';
import { FieldView, FieldViewProps } from './FieldView';
-import "./PDFBox.scss";
-import React = require("react");
+import { ImageBox } from './ImageBox';
+import './PDFBox.scss';
+import { VideoBox } from './VideoBox';
+import React = require('react');
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PDFBox, fieldKey);
+ }
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
- private _searchString: string = "";
+ private _searchString: string = '';
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
private _searchRef = React.createRef<HTMLInputElement>();
@@ -38,8 +45,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
@observable private _pageControls = false;
- @computed get pdfUrl() { return Cast(this.dataDoc[this.props.fieldKey], PdfField); }
- @computed get pdfThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; }
+ @computed get pdfUrl() {
+ return Cast(this.dataDoc[this.props.fieldKey], PdfField);
+ }
+ @computed get pdfThumb() {
+ return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url;
+ }
constructor(props: any) {
super(props);
@@ -47,49 +58,173 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
!this.Document._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));
+ 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)));
}
}
- componentWillUnmount() { this._selectReactionDisposer?.(); }
+ replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
+ if (oldDiv.childNodes) {
+ for (let i = 0; i < oldDiv.childNodes.length; i++) {
+ this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement);
+ }
+ }
+
+ if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') {
+ newDiv.style.display = 'none';
+ }
+ if (newDiv && newDiv.style) newDiv.style.overflow = 'hidden';
+ if (oldDiv instanceof HTMLCanvasElement) {
+ const canvas = oldDiv;
+ const img = document.createElement('img'); // create a Image Element
+ img.src = canvas.toDataURL(); //image sourcez
+ img.style.width = canvas.style.width;
+ img.style.height = canvas.style.height;
+ const newCan = newDiv as HTMLCanvasElement;
+ const parEle = newCan.parentElement as HTMLElement;
+ parEle.removeChild(newCan);
+ parEle.appendChild(img);
+ }
+ };
+
+ crop = (region: Doc | undefined, addCrop?: boolean) => {
+ if (!region) return;
+ const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).lockedPosition = true;
+ Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
+ Doc.GetProto(region).isPushpin = true;
+ this.addDocument(region);
+
+ const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
+ const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
+ newDiv.style.width = this.layoutDoc[WidthSym]().toString();
+ newDiv.style.height = this.layoutDoc[HeightSym]().toString();
+ this.replaceCanvases(docViewContent, newDiv);
+ const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
+
+ const anchx = NumCast(cropping.x);
+ const anchy = NumCast(cropping.y);
+ const anchw = cropping[WidthSym]() * (this.props.NativeDimScaling?.() || 1);
+ const anchh = cropping[HeightSym]() * (this.props.NativeDimScaling?.() || 1);
+ const viewScale = 1;
+ 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;
+ cropping._height = anchh;
+ cropping.isLinkButton = undefined;
+ const croppingProto = Doc.GetProto(cropping);
+ croppingProto.annotationOn = undefined;
+ croppingProto.isPrototype = 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;
+ if (addCrop) {
+ DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ }
+ this.props.bringToFront(cropping);
+
+ CreateImage(
+ '',
+ document.styleSheets,
+ 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']),
+ 4
+ )
+ .then((data_url: any) => {
+ VideoBox.convertDataUri(data_url, region[Id]).then(returnedfilename =>
+ setTimeout(
+ action(() => {
+ croppingProto.data = new ImageField(returnedfilename);
+ }),
+ 500
+ )
+ );
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
+
+ return cropping;
+ };
+
+ updateIcon = () => {
+ // currently we render pdf icons as text labels
+ const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
+ const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime();
+ this._pdfViewer?._mainCont.current &&
+ CollectionFreeFormView.UpdateIcon(
+ filename,
+ docViewContent,
+ this.layoutDoc[WidthSym](),
+ this.layoutDoc[HeightSym](),
+ this.props.PanelWidth(),
+ this.props.PanelHeight(),
+ NumCast(this.layoutDoc._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;
+ }, 500);
+ }
+ );
+ };
+
+ componentWillUnmount() {
+ this._selectReactionDisposer?.();
+ }
componentDidMount() {
this.props.setContentView?.(this);
- this._selectReactionDisposer = reaction(() => this.props.isSelected(),
+ this._selectReactionDisposer = reaction(
+ () => this.props.isSelected(),
() => {
- document.removeEventListener("keydown", this.onKeyDown);
- this.props.isSelected(true) && document.addEventListener("keydown", this.onKeyDown);
- }, { fireImmediately: true });
+ document.removeEventListener('keydown', this.onKeyDown);
+ this.props.isSelected(true) && document.addEventListener('keydown', this.onKeyDown);
+ },
+ { fireImmediately: true }
+ );
}
scrollFocus = (doc: Doc, smooth: boolean) => {
- if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ let didToggle = false;
+ if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
this.toggleSidebar(!smooth);
+ didToggle = true;
}
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
this._initialScrollTarget = doc;
- return this._pdfViewer?.scrollFocus(doc, smooth);
- }
+ return this._pdfViewer?.scrollFocus(doc, smooth) ?? (didToggle ? 1 : undefined);
+ };
getAnchor = () => {
const anchor =
- AnchorMenu.Instance?.GetAnchor() ??
+ this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ??
Docs.Create.TextanchorDocument({
- title: StrCast(this.rootDoc.title + "@" + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
+ title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
y: NumCast(this.layoutDoc._scrollTop),
- unrendered: true
+ unrendered: true,
});
this.addDocument(anchor);
return anchor;
- }
+ };
@action
loaded = (nw: number, nh: number, np: number) => {
- 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.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));
- }
+ };
public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
if (!this._searching && !clear) {
@@ -104,16 +239,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
});
public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
- public backPage = () => { this.Document._curPage = (NumCast(this.Document._curPage) || 1) - 1; return true; };
- public forwardPage = () => { this.Document._curPage = (NumCast(this.Document._curPage) || 1) + 1; return true; };
- public gotoPage = (p: number) => this.Document._curPage = p;
+ public backPage = () => {
+ this.Document._curPage = Math.max(1, (NumCast(this.Document._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);
+ return true;
+ };
+ public gotoPage = (p: number) => (this.Document._curPage = p);
@undoBatch
onKeyDown = action((e: KeyboardEvent) => {
let processed = false;
switch (e.key) {
- case "PageDown": processed = this.forwardPage(); break;
- case "PageUp": processed = this.backPage(); break;
+ case 'PageDown':
+ processed = this.forwardPage();
+ break;
+ case 'PageUp':
+ processed = this.backPage();
+ break;
}
if (processed) {
e.stopImmediatePropagation();
@@ -127,180 +272,232 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.scrollFocus(this._initialScrollTarget, false);
this._initialScrollTarget = undefined;
}
- }
- searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
+ };
+ 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();
return this.addDocument(doc, sidebarKey);
- }
- sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf
- setupMoveUpEvents(this, e, (e, down, delta) => {
- const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]);
- 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.scaling?.() || 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;
+ };
+ sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => {
+ // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf
+ const batch = UndoManager.StartBatch('sidebar');
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) => {
+ const localDelta = this.props
+ .ScreenToLocalTransform()
+ .scale(this.props.NativeDimScaling?.() || 1)
+ .transformDirection(delta[0], delta[1]);
+ 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;
+ }
+ return false;
+ },
+ (e, movement, isClick) => !isClick && batch.end(),
+ () => {
+ this.toggleSidebar();
+ batch.end();
}
- return false;
- }, emptyFunction, () => this.toggleSidebar());
- }
+ );
+ };
@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);
if (preview) {
this._previewNativeWidth = nativeWidth * sideratio;
- this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * sideratio / curNativeWidth;
+ this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth;
this._showSidebar = true;
- }
- else {
+ } else {
this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth;
+ this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
});
settingsPanel() {
- const pageBtns = <>
- <button className="pdfBox-backBtn" key="back" title="Page Back"
- onPointerDown={e => e.stopPropagation()} onClick={this.backPage} >
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
- </button>
- <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward"
- onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage} >
- <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
- </button>
- </>;
- const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`;
+ const pageBtns = (
+ <>
+ <button className="pdfBox-backBtn" key="back" title="Page Back" onPointerDown={e => e.stopPropagation()} onClick={this.backPage}>
+ <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-left'} size="sm" />
+ </button>
+ <button className="pdfBox-fwdBtn" key="fwd" title="Page Forward" onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}>
+ <FontAwesomeIcon style={{ color: 'white' }} icon={'arrow-right'} size="sm" />
+ </button>
+ </>
+ );
+ const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`;
const curPage = NumCast(this.Document._curPage) || 1;
- return !this.props.isContentActive() ? (null) :
- <div className="pdfBox-ui" onKeyDown={e => [KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true}
- onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}>
- <div className="pdfBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
+ return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : (
+ <div
+ className="pdfBox-ui"
+ onKeyDown={e => ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)}
+ onPointerDown={e => e.stopPropagation()}
+ style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}>
+ <div className="pdfBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
<button className="pdfBox-overlayButton" title={searchTitle} />
- <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged}
- onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} />
+ <input
+ className="pdfBox-searchBar"
+ placeholder="Search"
+ ref={this._searchRef}
+ onChange={this.searchStringChanged}
+ onKeyDown={e => {
+ e.stopPropagation();
+ e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey);
+ }}
+ />
<button className="pdfBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
<FontAwesomeIcon icon="search" size="sm" />
</button>
- <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation} >
- <FontAwesomeIcon icon={"arrow-up"} size="lg" />
+ <button className="pdfBox-prevIcon" title="Previous Annotation" onClick={this.prevAnnotation}>
+ <FontAwesomeIcon icon={'arrow-up'} size="lg" />
</button>
- <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} >
- <FontAwesomeIcon icon={"arrow-down"} size="lg" />
+ <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation}>
+ <FontAwesomeIcon icon={'arrow-down'} size="lg" />
</button>
</div>
- <button className="pdfBox-overlayButton" title={searchTitle}
- onClick={action(() => { this._searching = !this._searching; this.search("", true, true); })} >
- <div className="pdfBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} />
- <div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" />
+ <button
+ className="pdfBox-overlayButton"
+ title={searchTitle}
+ onClick={action(() => {
+ this._searching = !this._searching;
+ this.search('', true, true);
+ })}>
+ <div className="pdfBox-overlayButton-arrow" onPointerDown={e => e.stopPropagation()} />
+ <div className="pdfBox-overlayButton-iconCont" onPointerDown={e => e.stopPropagation()}>
+ <FontAwesomeIcon icon={this._searching ? 'times' : 'search'} size="lg" />
</div>
</button>
<div className="pdfBox-pageNums">
- <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: "all" }}
- onChange={e => this.Document._curPage = Number(e.currentTarget.value)}
- onClick={action(() => this._pageControls = !this._pageControls)} />
- {this._pageControls ? pageBtns : (null)}
- </div>
- <div className="pdfBox-sidebarBtn" key="sidebar" title="Toggle Sidebar"
- style={{
- display: !this.props.isContentActive() ? "none" : undefined,
- top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5,
- backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK
- }}
- onPointerDown={e => this.sidebarBtnDown(e, true)} >
- <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
+ <input
+ value={curPage}
+ style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }}
+ onChange={e => (this.Document._curPage = Number(e.currentTarget.value))}
+ onKeyDown={e => e.stopPropagation()}
+ onClick={action(() => (this._pageControls = !this._pageControls))}
+ />
+ {this._pageControls ? pageBtns : null}
</div>
- </div>;
+ {this.sidebarHandle}
+ </div>
+ );
}
- sidebarWidth = () => !this.SidebarShown ? 0 :
- this._previewWidth ? PDFBox.openSidebarWidth :
- (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)
+ sidebarWidth = () =>
+ !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
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: '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" });
- }
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ };
@computed get renderTitleBox() {
- const classname = "pdfBox" + (this.props.isContentActive() ? "-interactive" : "");
- return <div className={classname} >
- <div className="pdfBox-title-outer">
- <strong className="pdfBox-title" >{this.props.Document.title}</strong>
+ const classname = 'pdfBox' + (this.props.isContentActive() ? '-interactive' : '');
+ return (
+ <div className={classname}>
+ <div className="pdfBox-title-outer">
+ <strong className="pdfBox-title">{StrCast(this.props.Document.title)}</strong>
+ </div>
</div>
- </div>;
+ );
}
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@observable _showSidebar = false;
- @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ }
+ @computed get sidebarHandle() {
+ return (
+ <div
+ className="pdfBox-sidebarBtn"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}
+ onPointerDown={e => this.sidebarBtnDown(e, true)}>
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+ </div>
+ );
+ }
- contentScaling = () => 1;
isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected();
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
- const scale = previewScale * (this.props.scaling?.() || 1);
- return <div 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
- }}>
- <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
- <div style={{
- width: `calc(${100 / scale}% - ${(this.sidebarWidth() + PDFBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`,
- height: `${100 / scale}%`,
- transform: `scale(${scale})`,
- position: "absolute",
- transformOrigin: "top left",
- top: 0
- }}>
- <PDFViewer {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- pdf={this._pdf!}
- url={this.pdfUrl!.url.pathname}
- isContentActive={this.isPdfContentActive}
- anchorMenuClick={this.anchorMenuClick}
- loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
- setPdfViewer={this.setPdfViewer}
- addDocument={this.addDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- startupLive={true}
- ContentScaling={returnOne}
- />
+ const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
+ return (
+ <div
+ 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,
+ }}>
+ <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
+ <div
+ style={{
+ width: `calc(${100 / scale}% - ${(this.sidebarWidth() / scale) * (this._previewWidth ? scale : 1)}px)`,
+ height: `${100 / scale}%`,
+ transform: `scale(${scale})`,
+ position: 'absolute',
+ transformOrigin: 'top left',
+ top: 0,
+ }}>
+ <PDFViewer
+ {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ pdf={this._pdf!}
+ url={this.pdfUrl!.url.pathname}
+ isContentActive={this.isPdfContentActive}
+ anchorMenuClick={this.anchorMenuClick}
+ loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
+ setPdfViewer={this.setPdfViewer}
+ addDocument={this.addDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ crop={this.crop}
+ />
+ </div>
+ <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+ <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>
+ {this.settingsPanel()}
</div>
- <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}
- />
- {this.settingsPanel()}
- </div>;
+ );
}
static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
@@ -316,12 +513,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const href = this.pdfUrl?.url.href;
if (href) {
- if (PDFBox.pdfcache.get(href)) setTimeout(action(() => this._pdf = PDFBox.pdfcache.get(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)));
+ PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
}
}
return this.renderTitleBox;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.scss b/src/client/views/nodes/RecordingBox/ProgressBar.scss
new file mode 100644
index 000000000..28ad25ffa
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -0,0 +1,123 @@
+
+.progressbar {
+ touch-action: none;
+ vertical-align: middle;
+ text-align: center;
+
+ align-items: center;
+ cursor: default;
+
+
+ position: absolute;
+ display: flex;
+ justify-content: flex-start;
+ bottom: 2px;
+ width: 99%;
+ height: 30px;
+ background-color: gray;
+
+ &.done {
+ top: 0;
+ width: 0px;
+ height: 5px;
+ background-color: red;
+ z-index: 2;
+ }
+
+ &.mark {
+ top: 0;
+ background-color: transparent;
+ border-right: 2px solid white;
+ z-index: 3;
+ pointer-events: none;
+ }
+}
+
+.progressbar-disabled {
+ cursor: not-allowed;
+}
+
+.progressbar-dragging {
+ cursor: grabbing;
+}
+
+// citation: https://codepen.io/_Master_/pen/PRdjmQ
+@keyframes blinker {
+ from {opacity: 1.0;}
+ to {opacity: 0.0;}
+}
+.blink {
+ text-decoration: blink;
+ animation-name: blinker;
+ animation-duration: 0.6s;
+ animation-iteration-count:infinite;
+ animation-timing-function:ease-in-out;
+ animation-direction: alternate;
+}
+
+.segment {
+ border: 3px solid black;
+ background-color: red;
+ margin: 1px;
+ padding: 0;
+ cursor: pointer;
+ transition-duration: .5s;
+ user-select: none;
+
+ vertical-align: middle;
+ text-align: center;
+}
+
+.segment-expanding {
+border-color: red;
+ background-color: white;
+ transition-duration: 0s;
+ opacity: .75;
+ pointer-events: none;
+}
+
+.segment-expanding:hover {
+ background-color: inherit;
+ cursor: not-allowed;
+}
+
+.segment-disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ transition-duration: 0s;
+ /* Hide the text. */
+ text-indent: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+}
+
+.segment-hide {
+ background-color: inherit;
+ text-align: center;
+ vertical-align: middle;
+ user-select: none;
+}
+
+.segment:first-child {
+ margin-left: 2px;
+}
+.segment:last-child {
+ margin-right: 2px;
+}
+
+.segment:hover {
+ background-color: white;
+}
+
+.segment:hover, .segment-selected {
+ margin: 0px;
+ border: 4px solid red;
+ border-radius: 2px;
+}
+
+.segment-selected {
+ border: 4px solid #202020;
+ background-color: red;
+ opacity: .75;
+ cursor: grabbing;
+}
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
new file mode 100644
index 000000000..1bb2b7c84
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -0,0 +1,301 @@
+import * as React from 'react';
+import { useEffect, useState, useCallback, useRef } from "react"
+import "./ProgressBar.scss"
+import { MediaSegment } from './RecordingView';
+
+interface ProgressBarProps {
+ videos: MediaSegment[],
+ setVideos: React.Dispatch<React.SetStateAction<MediaSegment[]>>,
+ orderVideos: boolean,
+ progress: number,
+ recording: boolean,
+ doUndo: boolean,
+ setCanUndo?: React.Dispatch<React.SetStateAction<boolean>>,
+}
+
+interface SegmentBox {
+ endTime: number,
+ startTime: number,
+ order: number,
+}
+interface CurrentHover {
+ index: number,
+ minX: number,
+ maxX: number
+}
+
+export function ProgressBar(props: ProgressBarProps) {
+ const progressBarRef = useRef<HTMLDivElement | null>(null)
+
+ // the actual list of JSX elements rendered as segments
+ const [segments, setSegments] = useState<JSX.Element[]>([]);
+ // array for the order of video segments
+ const [ordered, setOrdered] = useState<SegmentBox[]>([]);
+
+ const [undoStack, setUndoStack] = useState<SegmentBox[]>([]);
+
+ // -1 if no segment is currently being dragged around; else, it is the id of that segment over
+ // NOTE: the id of a segment is its index in the ordered array
+ const [dragged, setDragged] = useState<number>(-1);
+
+ // length of the time removed from the video, in seconds*100
+ const [totalRemovedTime, setTotalRemovedTime] = useState<number>(0);
+
+ // this holds the index of the videoc segment to be removed
+ const [removed, setRemoved] = useState<number>(-1);
+
+ // update the canUndo props based on undo stack
+ useEffect(() => props.setCanUndo?.(undoStack.length > 0), [undoStack.length]);
+
+ // useEffect for undo - brings back the most recently deleted segment
+ useEffect(() => handleUndo(), [props.doUndo])
+ const handleUndo = () => {
+ // get the last element from the undo if it exists
+ if (undoStack.length === 0) return;
+ // get and remove the last element from the undo stack
+ const last = undoStack.lastElement();
+ setUndoStack(prevUndo => prevUndo.slice(0, -1));
+
+ // update the removed time and place element back into ordered
+ setTotalRemovedTime(prevRemoved => prevRemoved - (last.endTime - last.startTime));
+ setOrdered(prevOrdered => [...prevOrdered, last]);
+ }
+
+ // useEffect for recording changes - changes style to disabled and adds the "expanding-segment"
+ useEffect(() => {
+ // get segments segment's html using it's id -> make them appeared disabled (or enabled)
+ segments.forEach((seg) => document.getElementById(seg.props.id)?.classList.toggle('segment-disabled', props.recording));
+ progressBarRef.current?.classList.toggle('progressbar-disabled', props.recording);
+
+ if (props.recording)
+ setSegments(prevSegments => [...prevSegments, <div key='segment-expanding' id='segment-expanding' className='segment segment-expanding blink' style={{ width: 'fit-content' }}>{props.videos.length + 1}</div>]);
+ }, [props.recording])
+
+
+ // useEffect that updates the segmentsJSX, which is rendered
+ // only updated when ordered is updated or if the user is dragging around a segment
+ useEffect(() => {
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ const segmentsJSX = ordered.map((seg, i) =>
+ <div key={`segment-${i}`} id={`segment-${i}`} className={dragged === i ? 'segment-hide' : 'segment'} style={{ width: `${((seg.endTime - seg.startTime) / totalTime) * 100}%` }}>{seg.order + 1}</div>);
+
+ setSegments(segmentsJSX)
+ }, [dragged, ordered]);
+
+ // useEffect for dragged - update the cursor to be grabbing while grabbing
+ useEffect(() => {
+ progressBarRef.current?.classList.toggle('progressbar-dragging', dragged !== -1);
+ }, [dragged]);
+
+ // to imporve performance, only want to update the CSS width, not re-render the whole JSXList
+ useEffect(() => {
+ if (!props.recording) return
+ const totalTime = props.progress * 1000 - totalRemovedTime;
+ let remainingTime = totalTime;
+ segments.forEach((seg, i) => {
+ // for the last segment, we need to set that directly
+ if (i === segments.length - 1) return;
+ // update remaining time
+ remainingTime -= (ordered[i].endTime - ordered[i].startTime);
+
+ // update the width for this segment
+ const htmlId = seg.props.id;
+ const segmentHtml = document.getElementById(htmlId);
+ if (segmentHtml) segmentHtml.style.width = `${((ordered[i].endTime - ordered[i].startTime) / totalTime) * 100}%`;
+ });
+
+ // update the width of the expanding segment using the remaining time
+ const segExapandHtml = document.getElementById('segment-expanding');
+ if (segExapandHtml)
+ segExapandHtml.style.width = ordered.length === 0 ? '100%' : `${(remainingTime / totalTime) * 100}%`;
+ }, [props.progress]);
+
+ // useEffect for props.videos - update the ordered array when a new video is added
+ useEffect(() => {
+ // this useEffect fired when the videos are being rearragned to the order
+ // in this case, do nothing.
+ if (props.orderVideos) return;
+
+ const order = props.videos.length - 1;
+ // in this case, a new video is added -> push it onto ordered
+ if (order >= ordered.length) {
+ const { endTime, startTime } = props.videos.lastElement();
+ setOrdered(prevOrdered => {
+ return [...prevOrdered, { endTime, startTime, order }];
+ });
+ }
+
+ // in this case, a video is removed
+ else if (order < ordered.length) {
+ console.warn('warning: video removed from parent');
+ }
+ }, [props.videos]);
+
+ // useEffect for props.orderVideos - matched the order array with the videos array before the export
+ useEffect(() => props.setVideos(vids => ordered.map((seg) => vids[seg.order])), [props.orderVideos]);
+
+ // useEffect for removed - handles logic for removing a segment
+ useEffect(() => {
+ if (removed === -1) return;
+ // update total removed time
+ setTotalRemovedTime(prevRemoved => prevRemoved + (ordered[removed].endTime - ordered[removed].startTime));
+
+ // put the element on the undo stack
+ setUndoStack(prevUndo => [...prevUndo, ordered[removed]]);
+ // remove the segment from the array
+ setOrdered(prevOrdered => prevOrdered.filter((seg, i) => i !== removed));
+ // reset to default/nullish state
+ setRemoved(-1);
+ }, [removed]);
+
+ // returns the new currentHover based on the new index
+ const updateCurrentHover = (segId: number): CurrentHover | null => {
+ // get the segId of the segment that will become the new bounding area
+ const rect = progressBarRef.current?.children[segId].getBoundingClientRect()
+ if (rect == null) return null
+ return {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+ }
+
+ // pointerdown event for the progress bar
+ const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ // don't move the videobox element
+ e.stopPropagation();
+
+ // if recording, do nothing
+ if (props.recording) return;
+
+ // get the segment the user clicked on to be dragged
+ const clickedSegment = e.target as HTMLDivElement & EventTarget
+
+ // get the profess bar ro add event listeners
+ // don't do anything if null
+ const progressBar = progressBarRef.current
+ if (progressBar == null || clickedSegment.id === progressBar.id) return
+
+ // if holding shift key, let's remove that segment
+ if (e.shiftKey) {
+ const segId = parseInt(clickedSegment.id.split('-')[1]);
+ setRemoved(segId);
+ return
+ }
+
+ // if holding ctrl key and click, let's undo that segment #hiddenfeature lol
+ if (e.ctrlKey) {
+ handleUndo();
+ return;
+ }
+
+ // if we're here, the user is dragging a segment around
+ // let the progress bar capture all the pointer events until the user releases (pointerUp)
+ const ptrId = e.pointerId;
+ progressBar.setPointerCapture(ptrId)
+
+ const rect = clickedSegment.getBoundingClientRect()
+ // id for segment is like 'segment-1' or 'segment-10',
+ // so this works to get the id
+ const segId = parseInt(clickedSegment.id.split('-')[1])
+ // set the selected segment to be the one dragged
+ setDragged(segId)
+
+ // this is the logic for storing the lower X bound and upper X bound to know
+ // whether a swap is needed between two segments
+ let currentHover: CurrentHover = {
+ index: segId,
+ minX: rect.x,
+ maxX: rect.x + rect.width,
+ }
+
+ // create the floating segment that tracks the cursor
+ const detchedSegment = document.createElement("div")
+ initDeatchSegment(detchedSegment, rect);
+
+ const updateSegmentOrder = (event: PointerEvent): void => {
+ event.stopPropagation();
+ event.preventDefault();
+
+ // this fixes a bug where pointerup doesn't fire while cursor is upped while being dragged
+ if (!progressBar.hasPointerCapture(ptrId)) {
+ placeSegmentandCleanup();
+ return;
+ }
+
+ followCursor(event, detchedSegment);
+
+ const curX = event.clientX;
+ // handle the left bound
+ if (curX < currentHover.minX && currentHover.index > 0) {
+ swapSegments(currentHover.index, currentHover.index - 1)
+ currentHover = updateCurrentHover(currentHover.index - 1) ?? currentHover
+ }
+ // handle the right bound
+ else if (curX > currentHover.maxX && currentHover.index < segments.length - 1) {
+ swapSegments(currentHover.index, currentHover.index + 1)
+ currentHover = updateCurrentHover(currentHover.index + 1) ?? currentHover
+ }
+ }
+
+ // handles when the user is done dragging the segment (pointerUp)
+ const placeSegmentandCleanup = (event?: PointerEvent): void => {
+ event?.stopPropagation();
+ event?.preventDefault();
+ // if they put the segment outside of the bounds, remove it
+ if (event && (event.clientX < 0 || event.clientX > document.body.clientWidth || event.clientY < 0 || event.clientY > document.body.clientHeight))
+ setRemoved(currentHover.index);
+
+ // remove the update event listener for pointermove
+ progressBar.removeEventListener('pointermove', updateSegmentOrder);
+ // remove the floating segment from the DOM
+ detchedSegment.remove();
+ // dragged is -1 is equiv to nothing being dragged, so the normal state
+ // so this will place the segment in it's location and update the segment bar
+ setDragged(-1);
+ }
+
+ // event listeners that allow the user to drag and release the floating segment
+ progressBar.addEventListener('pointermove', updateSegmentOrder);
+ progressBar.addEventListener('pointerup', placeSegmentandCleanup, { once: true });
+ }
+
+ const swapSegments = (oldIndex: number, newIndex: number) => {
+ if (newIndex == null) return;
+ setOrdered(prevOrdered => {
+ const temp = { ...prevOrdered[oldIndex] }
+ prevOrdered[oldIndex] = prevOrdered[newIndex]
+ prevOrdered[newIndex] = temp
+ return prevOrdered
+ });
+ // update visually where the segment is hovering over
+ setDragged(newIndex);
+ }
+
+ // functions for the floating segment that tracks the cursor while grabbing it
+ const initDeatchSegment = (dot: HTMLDivElement, rect: DOMRect) => {
+ dot.classList.add("segment-selected");
+ dot.style.transitionDuration = '0s';
+ dot.style.position = 'absolute';
+ dot.style.zIndex = '999';
+ dot.style.width = `${rect.width}px`;
+ dot.style.height = `${rect.height}px`;
+ dot.style.left = `${rect.x}px`;
+ dot.style.top = `${rect.y}px`;
+ dot.draggable = false;
+ document.body.append(dot);
+ }
+ const followCursor = (event: PointerEvent, dot: HTMLDivElement): void => {
+ // event.stopPropagation()
+ const { width, height } = dot.getBoundingClientRect();
+ dot.style.left = `${event.clientX - width / 2}px`;
+ dot.style.top = `${event.clientY - height / 2}px`;
+ }
+
+
+ return (
+ <div className="progressbar" id="progressbar" onPointerDown={onPointerDown} ref={progressBarRef}>
+ {segments}
+ </div>
+ )
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
new file mode 100644
index 000000000..0ff7c4292
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -0,0 +1,58 @@
+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";
+
+
+@observer
+export class RecordingBox extends ViewBoxBaseComponent() {
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ 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>;
+ }
+}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
new file mode 100644
index 000000000..2e6f6bc26
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -0,0 +1,214 @@
+video {
+ // flex: 100%;
+ width: 100%;
+ // min-height: 400px;
+ //height: auto;
+ height: 100%;
+ //display: block;
+ object-fit: cover;
+ background-color: black;
+}
+
+button {
+ margin: 0 .5rem
+}
+
+.recording-container {
+ height: 100%;
+ width: 100%;
+ // display: flex;
+ pointer-events: all;
+ background-color: black;
+}
+
+.video-wrapper {
+ // max-width: 600px;
+ // max-width: 700px;
+ // position: relative;
+ display: flex;
+ justify-content: center;
+ // overflow: hidden;
+ border-radius: 10px;
+ margin: 0;
+}
+
+.video-wrapper:hover .controls {
+ bottom: 34.5px;
+ transform: translateY(0%);
+ opacity: 100%;
+}
+
+.controls {
+ display: flex;
+ align-items: center;
+ justify-content: space-evenly;
+ position: absolute;
+ // padding: 14px;
+ //width: 100%;
+ max-width: 500px;
+ // max-height: 20%;
+ flex-wrap: wrap;
+ background: rgba(255, 255, 255, 0.25);
+ box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(4px);
+ border-radius: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ // transform: translateY(150%);
+ transition: all 0.3s ease-in-out;
+ // opacity: 0%;
+ bottom: 34.5px;
+ height: 60px;
+ right: 2px;
+ // bottom: -150px;
+}
+
+.controls:active {
+ bottom: 40px;
+ // bottom: -150px;
+}
+
+.actions button {
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+}
+
+.actions button i {
+ background-color: none;
+ color: white;
+ font-size: 30px;
+}
+
+
+.velocity {
+ appearance: none;
+ background: none;
+ color: white;
+ outline: none;
+ border: none;
+ text-align: center;
+ font-size: 16px;
+}
+
+.mute-btn {
+ background: none;
+ border: none;
+ outline: none;
+ cursor: pointer;
+}
+
+.mute-btn i {
+ background-color: none;
+ color: white;
+ font-size: 20px;
+}
+
+.recording-sign {
+ height: 20px;
+ width: auto;
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ top: 10px;
+ right: 15px;
+ align-items: center;
+ justify-content: center;
+
+ .timer {
+ font-size: 15px;
+ color: white;
+ margin: 0;
+ }
+
+ .dot {
+ height: 15px;
+ width: 15px;
+ margin: 5px;
+ background-color: red;
+ border-radius: 50%;
+ display: inline-block;
+ }
+}
+
+.controls-inner-container {
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ position: relative;
+}
+
+.record-button-wrapper {
+ width: 35px;
+ height: 35px;
+ font-size: 0;
+ background-color: grey;
+ border: 0px;
+ border-radius: 35px;
+ margin: 10px;
+ display: flex;
+ justify-content: center;
+
+ .record-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 50%;
+ height: 80%;
+ width: 80%;
+ align-self: center;
+ margin: 0;
+
+ &:hover {
+ height: 85%;
+ width: 85%;
+ }
+ }
+
+ .stop-button {
+ background-color: red;
+ border: 0px;
+ border-radius: 10%;
+ height: 70%;
+ width: 70%;
+ align-self: center;
+ margin: 0;
+
+
+ // &:hover {
+ // width: 40px;
+ // height: 40px
+ // }
+ }
+
+}
+
+.options-wrapper {
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ align-content: center;
+ position: relative;
+ top: 0;
+ bottom: 0;
+
+ &.video-edit-wrapper {
+
+ // right: 50% - 15;
+
+ .track-screen {
+ font-weight: 200;
+ }
+
+ }
+
+ &.track-screen-wrapper {
+
+ // right: 50% - 30;
+
+ .track-screen {
+ font-weight: 200;
+ color: aqua;
+ }
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
new file mode 100644
index 000000000..ec5917b9e
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -0,0 +1,271 @@
+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 { FaCheckCircle } from 'react-icons/fa';
+import { IconContext } from "react-icons";
+import { Networking } from '../../../Network';
+import { Upload } from '../../../../server/SharedMediaTypes';
+import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Presentation, TrackMovements } from '../../../util/TrackMovements';
+
+export interface MediaSegment {
+ videoChunks: any[],
+ endTime: number,
+ startTime: number,
+ presentation?: Presentation,
+}
+
+interface IRecordingViewProps {
+ 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
+ const [playing, setPlaying] = useState(false);
+ const [progress, setProgress] = useState(0);
+
+ // acts as a "refresh state" to tell progressBar when to undo
+ const [doUndo, setDoUndo] = useState(false);
+ // whether an undo can occur or not
+ const [canUndo, setCanUndo] = useState(false);
+
+ const [videos, setVideos] = useState<MediaSegment[]>([]);
+ const [orderVideos, setOrderVideos] = useState<boolean>(false);
+ const videoRecorder = useRef<MediaRecorder | null>(null);
+ const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+ 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
+ }
+ };
+
+ useEffect(() => {
+ if (finished) {
+ // make the total presentation that'll match the concatted video
+ let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+
+ // this async function uses the server to create the concatted video and then sets the result to it's accessPaths
+ (async () => {
+ 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)
+
+ // 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");
+ })();
+ }
+ }, [videos]);
+
+ // this will call upon the progress bar to edit videos to be in the correct order
+ useEffect(() => {
+ finished && setOrderVideos(true);
+ }, [finished]);
+
+ // check if the browser supports media devices on first load
+ useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+
+ useEffect(() => {
+ let interval: any = null;
+ if (recording) {
+ interval = setInterval(() => {
+ setRecordingTimer(unit => unit + 1);
+ }, 10);
+ } else if (!recording && recordingTimer !== 0) {
+ clearInterval(interval);
+ }
+ return () => clearInterval(interval);
+ }, [recording]);
+
+ useEffect(() => {
+ 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!.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
+ if (!videoRecorder.current) videoRecorder.current = new MediaRecorder(await startShowingStream());
+
+ // temporary chunks of video
+ let videoChunks: any = [];
+
+ videoRecorder.current.ondataavailable = (event: any) => {
+ if (event.data.size > 0) videoChunks.push(event.data);
+ };
+
+ videoRecorder.current.onstart = (event: any) => {
+ setRecording(true);
+ // start the recording api when the video recorder starts
+ trackScreen && TrackMovements.Instance.start();
+ };
+
+ videoRecorder.current.onstop = () => {
+ // if we have a last portion
+ if (videoChunks.length > 1) {
+ // append the current portion to the video pieces
+ const nextVideo = {
+ videoChunks,
+ endTime: recordingTimerRef.current,
+ 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]);
+ }
+
+ // 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();
+
+ // end the streams (audio/video) to remove recording icon
+ const stream = videoElementRef.current!.srcObject;
+ stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
+
+ // finish/clear the recoringApi
+ TrackMovements.Instance.finish();
+
+ // 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();
+ }
+
+ 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);
+ }
+
+ const undoPrevious = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ setDoUndo(prev => !prev);
+ }
+
+ const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+
+ const millisecondToMinuteSecond = (milliseconds: number) => {
+ const toTwoDigit = (digit: number) => {
+ 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 (
+ <div className="recording-container">
+ <div className="video-wrapper">
+ <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>
+
+ </div>
+
+ <ProgressBar
+ videos={videos}
+ setVideos={setVideos}
+ orderVideos={orderVideos}
+ progress={progress}
+ recording={recording}
+ doUndo={doUndo}
+ setCanUndo={setCanUndo}
+ />
+ </div>
+ </div>)
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/index.ts b/src/client/views/nodes/RecordingBox/index.ts
new file mode 100644
index 000000000..ff21eaed6
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/index.ts
@@ -0,0 +1,2 @@
+export * from './RecordingView'
+export * from './RecordingBox' \ No newline at end of file
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index dbb567d3a..76a24d831 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -1,32 +1,31 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
// import { Canvas } from '@react-three/fiber';
-import { computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
+import { computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
// import { BufferAttribute, Camera, Vector2, Vector3 } from 'three';
-import { DateField } from "../../../fields/DateField";
-import { Doc, HeightSym, WidthSym } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-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 { DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Networking } from "../../Network";
-import { CaptureManager } from "../../util/CaptureManager";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
-import { ContextMenu } from "../ContextMenu";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
+import { DateField } from '../../../fields/DateField';
+import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+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 { DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
+import { CaptureManager } from '../../util/CaptureManager';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
+import { ContextMenu } from '../ContextMenu';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
-import { FormattedTextBox } from "./formattedText/FormattedTextBox";
-import "./ScreenshotBox.scss";
-import { VideoBox } from "./VideoBox";
+import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import './ScreenshotBox.scss';
+import { VideoBox } from './VideoBox';
declare class MediaRecorder {
- constructor(e: any, options?: any); // whatever MediaRecorder has
+ constructor(e: any, options?: any); // whatever MediaRecorder has
}
// interface VideoTileProps {
@@ -107,12 +106,16 @@ declare class MediaRecorder {
@observer
export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(ScreenshotBox, fieldKey);
+ }
private _audioRec: any;
private _videoRec: any;
@observable private _videoRef: HTMLVideoElement | null = null;
@observable _screenCapture = false;
- @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+ @computed get recordingStart() {
+ return Cast(this.dataDoc[this.props.fieldKey + '-recordingStart'], DateField)?.date.getTime();
+ }
constructor(props: any) {
super(props);
@@ -126,11 +129,9 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
}
}
getAnchor = () => {
- 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)
- || this.rootDoc;
- }
+ 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) || this.rootDoc;
+ };
videoLoad = () => {
const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
@@ -141,7 +142,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
}
- }
+ };
componentDidMount() {
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
@@ -159,33 +160,37 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
}
componentWillUnmount() {
const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
}
specificContextMenu = (e: React.MouseEvent): void => {
- const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
- ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
- }
+ const subitems = [{ description: 'Screen Capture', event: this.toggleRecording, icon: 'expand-arrows-alt' as any }];
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems, icon: 'video' });
+ };
@computed get content() {
- if (this.rootDoc.videoWall) return (null);
- return <video className={"videoBox-content"} key="video"
- ref={r => {
- this._videoRef = r;
- setTimeout(() => {
- if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) {
- this.toggleRecording();
- }
- }, 100);
- }}
- autoPlay={this._screenCapture}
- style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
- onCanPlay={this.videoLoad}
- controls={true}
- onClick={e => e.preventDefault()}>
- <source type="video/mp4" />
- Not supported.
- </video>;
+ if (this.rootDoc.videoWall) return null;
+ return (
+ <video
+ className={'videoBox-content'}
+ key="video"
+ ref={r => {
+ this._videoRef = r;
+ setTimeout(() => {
+ if (this.rootDoc.mediaState === 'pendingRecording' && this._videoRef) {
+ this.toggleRecording();
+ }
+ }, 100);
+ }}
+ autoPlay={this._screenCapture}
+ style={{ width: this._screenCapture ? '100%' : undefined, height: this._screenCapture ? '100%' : undefined }}
+ onCanPlay={this.videoLoad}
+ controls={true}
+ onClick={e => e.preventDefault()}>
+ <source type="video/mp4" />
+ Not supported.
+ </video>
+ );
}
// _numScreens = 5;
@@ -209,7 +214,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
// {screens}
// </ Canvas>;
// }
- return (null);
+ return null;
}
toggleRecording = async () => {
if (!this._screenCapture) {
@@ -219,31 +224,33 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this._audioRec.onstop = async (e: any) => {
const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
if (!(result instanceof Error)) {
- this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
+ this.dataDoc[this.props.fieldKey + '-audio'] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
const vid_chunks: any = [];
- this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ this._videoRec.onstart = () => (this.dataDoc[this.props.fieldKey + '-recordingStart'] = new DateField(new Date()));
this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
this._videoRec.onstop = async (e: any) => {
+ 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;
- if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+ 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.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
- } else alert("video conversion failed");
+ } else alert('video conversion failed');
};
this._audioRec.start();
this._videoRec.start();
runInAction(() => {
this._screenCapture = true;
- this.dataDoc.mediaState = "recording";
+ this.dataDoc.mediaState = 'recording';
});
DocUtils.ActiveRecordings.push(this);
} else {
@@ -251,81 +258,86 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this._videoRec?.stop();
runInAction(() => {
this._screenCapture = false;
- this.dataDoc.mediaState = "paused";
+ this.dataDoc.mediaState = 'paused';
});
const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
CaptureManager.Instance.open(this.rootDoc);
}
- }
+ };
setupDictation = () => {
- if (this.dataDoc[this.fieldKey + "-dictation"]) return;
- const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
- NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
- NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+ 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;
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;
- }
+ 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();
- return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
- <div className="videoBox-viewer" >
- <div style={{ position: "relative", height: this.videoPanelHeight() }}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- PanelHeight={this.videoPanelHeight}
- PanelWidth={this.props.PanelWidth}
- focus={this.props.focus}
- isSelected={this.props.isSelected}
- isAnnotationOverlay={true}
- select={emptyFunction}
- isContentActive={returnFalse}
- scaling={returnOne}
- 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}
- </CollectionFreeFormView></div>
- <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
- {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) :
- <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- Document={this.dataDoc[this.fieldKey + "-dictation"]}
- fieldKey={"text"}
- PanelHeight={this.formattedPanelHeight}
+ return (
+ <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: '100%', height: '100%' }}>
+ <div className="videoBox-viewer">
+ <div style={{ position: 'relative', height: this.videoPanelHeight() }}>
+ <CollectionFreeFormView
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ PanelHeight={this.videoPanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
isAnnotationOverlay={true}
select={emptyFunction}
- isContentActive={emptyFunction}
- scaling={returnOne}
- xPadding={25}
- yPadding={10}
+ isContentActive={returnFalse}
+ NativeDimScaling={returnOne}
whenChildContentsActiveChanged={emptyFunction}
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- </FormattedTextBox>}
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ <div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
+ {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
+ <FormattedTextBox
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ Document={this.dataDoc[this.fieldKey + '-dictation']}
+ fieldKey={'text'}
+ PanelHeight={this.formattedPanelHeight}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ isContentActive={emptyFunction}
+ NativeDimScaling={returnOne}
+ xPadding={25}
+ yPadding={10}
+ whenChildContentsActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
+ )}
+ </div>
</div>
+ {!this.props.isSelected() ? null : (
+ <div className="screenshotBox-uiButtons">
+ <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}>
+ <FontAwesomeIcon icon="file" size="lg" />
+ </div>
+ </div>
+ )}
</div>
- {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
- <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
- <FontAwesomeIcon icon="file" size="lg" />
- </div>
- </div>}
- </div >;
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 366c3fc2f..1c9b0bc0e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -1,39 +1,39 @@
-import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
-import "@webscopeio/react-textarea-autocomplete/style.css";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { ScriptField } from "../../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../fields/Types";
-import { TraceMobx } from "../../../fields/util";
-import { returnEmptyString } from "../../../Utils";
-import { DragManager } from "../../util/DragManager";
-import { InteractionUtils } from "../../util/InteractionUtils";
-import { CompileScript, ScriptParam } from "../../util/Scripting";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { ScriptManager } from "../../util/ScriptManager";
-import { ContextMenu } from "../ContextMenu";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
-import { EditableView } from "../EditableView";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { OverlayView } from "../OverlayView";
-import { DocumentIconContainer } from "./DocumentIcon";
-import "./ScriptingBox.scss";
-const _global = (window /* browser */ || global /* node */) as any;
+let ReactTextareaAutocomplete = require('@webscopeio/react-textarea-autocomplete').default;
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+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 { DragManager } from '../../util/DragManager';
+import { InteractionUtils } from '../../util/InteractionUtils';
+import { CompileScript, ScriptParam } from '../../util/Scripting';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ScriptManager } from '../../util/ScriptManager';
+import { ContextMenu } from '../ContextMenu';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { EditableView } from '../EditableView';
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import { OverlayView } from '../OverlayView';
+import { DocumentIconContainer } from './DocumentIcon';
+import './ScriptingBox.scss';
+const _global = (window /* browser */ || global) /* node */ as any;
@observer
export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
-
private dropDisposer?: DragManager.DragDropDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer | undefined;
- public static LayoutString(fieldStr: string) { return FieldView.LayoutString(ScriptingBox, fieldStr); }
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(ScriptingBox, fieldStr);
+ }
private _overlayDisposer?: () => void;
private _caretPos = 0;
- @observable private _errorMessage: string = "";
+ @observable private _errorMessage: string = '';
@observable private _applied: boolean = false;
@observable private _function: boolean = false;
@observable private _spaced: boolean = false;
@@ -42,12 +42,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
@observable private _scriptingDescriptions: any = ScriptingGlobals.getDescriptions();
@observable private _scriptingParams: any = ScriptingGlobals.getParameters();
- @observable private _currWord: string = "";
+ @observable private _currWord: string = '';
@observable private _suggestions: string[] = [];
@observable private _suggestionBoxX: number = 0;
@observable private _suggestionBoxY: number = 0;
- @observable private _lastChar: string = "";
+ @observable private _lastChar: string = '';
@observable private _suggestionRef: any = React.createRef();
@observable private _scriptTextRef: any = React.createRef();
@@ -55,51 +55,80 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
@observable private _selection: any = 0;
@observable private _paramSuggestion: boolean = false;
- @observable private _scriptSuggestedParams: any = "";
- @observable private _scriptParamsText: any = "";
+ @observable private _scriptSuggestedParams: any = '';
+ @observable private _scriptParamsText: any = '';
constructor(props: any) {
super(props);
+ if (!this.compileParams.length) {
+ const params = ScriptCast(this.rootDoc[this.props.fieldKey])?.script.options.params as { [key: string]: any };
+ if (params) {
+ this.compileParams = Array.from(Object.keys(params))
+ .filter(p => !p.startsWith('_'))
+ .map(key => key + ':' + params[key]);
+ }
+ }
}
// vars included in fields that store parameters types and names and the script itself
- @computed({ keepAlive: true }) get paramsNames() { return this.compileParams.map(p => p.split(":")[0].trim()); }
- @computed({ keepAlive: true }) get paramsTypes() { return this.compileParams.map(p => p.split(":")[1].trim()); }
- @computed({ keepAlive: true }) get rawScript() { return StrCast(this.dataDoc[this.props.fieldKey + "-rawScript"], ""); }
- @computed({ keepAlive: true }) get functionName() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionName"], ""); }
- @computed({ keepAlive: true }) get functionDescription() { return StrCast(this.dataDoc[this.props.fieldKey + "-functionDescription"], ""); }
- @computed({ keepAlive: true }) get compileParams() { return Cast(this.dataDoc[this.props.fieldKey + "-params"], listSpec("string"), []); }
+ @computed({ keepAlive: true }) get paramsNames() {
+ return this.compileParams.map(p => p.split(':')[0].trim());
+ }
+ @computed({ keepAlive: true }) get paramsTypes() {
+ return this.compileParams.map(p => p.split(':')[1].trim());
+ }
+ @computed({ keepAlive: true }) get rawScript() {
+ return ScriptCast(this.rootDoc[this.fieldKey])?.script.originalScript ?? '';
+ }
+ @computed({ keepAlive: true }) get functionName() {
+ return StrCast(this.rootDoc[this.props.fieldKey + '-functionName'], '');
+ }
+ @computed({ keepAlive: true }) get functionDescription() {
+ return StrCast(this.rootDoc[this.props.fieldKey + '-functionDescription'], '');
+ }
+ @computed({ keepAlive: true }) get compileParams() {
+ return Cast(this.rootDoc[this.props.fieldKey + '-params'], listSpec('string'), []);
+ }
- set rawScript(value) { this.dataDoc[this.props.fieldKey + "-rawScript"] = value; }
- set functionName(value) { this.dataDoc[this.props.fieldKey + "-functionName"] = value; }
- set functionDescription(value) { this.dataDoc[this.props.fieldKey + "-functionDescription"] = value; }
+ set rawScript(value) {
+ Doc.SetInPlace(this.rootDoc, this.props.fieldKey, new ScriptField(undefined, undefined, value), true);
+ }
+ set functionName(value) {
+ Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionName', value, true);
+ }
+ set functionDescription(value) {
+ Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-functionDescription', value, true);
+ }
- set compileParams(value) { this.dataDoc[this.props.fieldKey + "-params"] = new List<string>(value); }
+ set compileParams(value) {
+ Doc.SetInPlace(this.rootDoc, this.props.fieldKey + '-params', new List<string>(value), true);
+ }
getValue(result: any, descrip: boolean) {
- if (typeof result === "object") {
+ if (typeof result === 'object') {
const text = descrip ? result[1] : result[2];
- return text !== undefined ? text : "";
+ return text !== undefined ? text : '';
} else {
- return "";
+ return '';
}
}
@action
componentDidMount() {
- this.rawScript = ScriptCast(this.dataDoc[this.props.fieldKey])?.script?.originalScript ?? this.rawScript;
-
- const observer = new _global.ResizeObserver(action((entries: any) => {
- const area = document.querySelector('textarea');
- if (area) {
- for (const { } of entries) {
- const getCaretCoordinates = require('textarea-caret');
- const caret = getCaretCoordinates(area, this._selection);
- this.resetSuggestionPos(caret);
+ this.rawText = this.rawScript;
+ const observer = new _global.ResizeObserver(
+ action((entries: any) => {
+ const area = document.querySelector('textarea');
+ if (area) {
+ for (const {} of entries) {
+ const getCaretCoordinates = require('textarea-caret');
+ const caret = getCaretCoordinates(area, this._selection);
+ this.resetSuggestionPos(caret);
+ }
}
- }
- }));
- observer.observe(document.getElementsByClassName("scriptingBox")[0]);
+ })
+ );
+ observer.observe(document.getElementsByClassName('scriptingBox')[0]);
}
@action
@@ -110,8 +139,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const top = caret.top;
const x = this.dataDoc.x;
let left = caret.left;
- if ((left + suggestionWidth) > (x + scriptWidth)) {
- const diff = (left + suggestionWidth) - (x + scriptWidth);
+ if (left + suggestionWidth > x + scriptWidth) {
+ const diff = left + suggestionWidth - (x + scriptWidth);
left = left - diff;
}
@@ -123,52 +152,53 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
this._overlayDisposer?.();
}
- protected createDashEventsTarget = (ele: HTMLDivElement, dropFunc: (e: Event, de: DragManager.DropEvent) => void) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement, dropFunc: (e: Event, de: DragManager.DropEvent) => void) => {
+ //used for stacking and masonry view
if (ele) {
this.dropDisposer?.();
this.dropDisposer = DragManager.MakeDropTarget(ele, dropFunc, this.layoutDoc);
}
- }
+ };
// only included in buttons, transforms scripting UI to a button
@action
onFinish = () => {
- this.rootDoc.layoutKey = "layout";
- }
+ this.rootDoc.layoutKey = 'layout';
+ };
// displays error message
@action
onError = (error: any) => {
- this._errorMessage = error?.message ? error.message : error?.map((entry: any) => entry.messageText).join(" ") || "";
- }
+ this._errorMessage = error?.message ? error.message : error?.map((entry: any) => entry.messageText).join(' ') || '';
+ };
// checks if the script compiles using CompileScript method and inputting params
@action
onCompile = () => {
const params: ScriptParam = {};
- this.compileParams.forEach(p => params[p.split(":")[0].trim()] = p.split(":")[1].trim());
+ this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim()));
- const result = CompileScript(this.rawScript, {
+ const result = CompileScript(this.rawText, {
editable: true,
transformer: DocumentIconContainer.getTransformer(),
params,
- typecheck: false
+ typecheck: false,
});
- this.dataDoc[this.fieldKey] = result.compiled ? new ScriptField(result) : undefined;
+ Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true);
this.onError(result.compiled ? undefined : result.errors);
return result.compiled;
- }
+ };
// checks if the script compiles and then runs the script
@action
onRun = () => {
if (this.onCompile()) {
const bindings: { [name: string]: any } = {};
- this.paramsNames.forEach(key => bindings[key] = this.dataDoc[key]);
+ this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key]));
// binds vars so user doesnt have to refer to everything as self.<var>
- ScriptCast(this.dataDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError);
+ ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError);
}
- }
+ };
// checks if the script compiles and switches to applied UI
@action
@@ -176,39 +206,39 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
if (this.onCompile()) {
this._applied = true;
}
- }
+ };
@action
onEdit = () => {
- this._errorMessage = "";
+ this._errorMessage = '';
this._applied = false;
this._function = false;
- }
+ };
@action
onSave = () => {
if (this.onCompile()) {
this._function = true;
} else {
- this._errorMessage = "Can not save script, does not compile";
+ this._errorMessage = 'Can not save script, does not compile';
}
- }
+ };
@action
onCreate = () => {
- this._errorMessage = "";
+ this._errorMessage = '';
if (this.functionName.length === 0) {
- this._errorMessage = "Must enter a function name";
+ this._errorMessage = 'Must enter a function name';
return false;
}
- if (this.functionName.indexOf(" ") > 0) {
- this._errorMessage = "Name can not include spaces";
+ if (this.functionName.indexOf(' ') > 0) {
+ this._errorMessage = 'Name can not include spaces';
return false;
}
- if (this.functionName.indexOf(".") > 0) {
+ if (this.functionName.indexOf('.') > 0) {
this._errorMessage = "Name can not include '.'";
return false;
}
@@ -223,173 +253,184 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
this._scriptKeys = ScriptingGlobals.getGlobals();
this._scriptingDescriptions = ScriptingGlobals.getDescriptions();
this._scriptingParams = ScriptingGlobals.getParameters();
- }
+ };
// overlays document numbers (ex. d32) over all documents when clicked on
onFocus = () => {
this._overlayDisposer?.();
this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- }
+ };
// sets field of the corresponding field key (param name) to be dropped document
@action
onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => {
- this.dataDoc[fieldKey] = de.complete.docDragData?.droppedDocuments[0];
+ Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true);
e.stopPropagation();
- }
+ };
- // deletes a param from all areas in which it is stored
+ // deletes a param from all areas in which it is stored
@action
onDelete = (num: number) => {
- this.dataDoc[this.paramsNames[num]] = undefined;
+ Doc.SetInPlace(this.rootDoc, this.paramsNames[num], undefined, true);
this.compileParams.splice(num, 1);
return true;
- }
+ };
// sets field of the param name to the selected value in drop down box
@action
viewChanged = (e: React.ChangeEvent, name: string) => {
//@ts-ignore
const val = e.target.selectedOptions[0].value;
- this.dataDoc[name] = val[0] === "S" ? val.substring(1) : val[0] === "N" ? parseInt(val.substring(1)) : val.substring(1) === "true";
- }
+ Doc.SetInPlace(this.rootDoc, name, val[0] === 'S' ? val.substring(1) : val[0] === 'N' ? parseInt(val.substring(1)) : val.substring(1) === 'true', true);
+ };
// creates a copy of the script document
onCopy = () => {
const copy = Doc.MakeCopy(this.rootDoc, true);
copy.x = NumCast(this.dataDoc.x) + NumCast(this.dataDoc._width);
this.props.addDocument?.(copy);
- }
+ };
// adds option to create a copy to the context menu
specificContextMenu = (): void => {
- const existingOptions = ContextMenu.Instance.findByDescription("Options...");
- const options = existingOptions && "subitems" in existingOptions ? existingOptions.subitems : [];
- options.push({ description: "Create a Copy", event: this.onCopy, icon: "copy" });
- !existingOptions && ContextMenu.Instance.addItem({ description: "Options...", subitems: options, icon: "hand-point-right" });
- }
+ const existingOptions = ContextMenu.Instance.findByDescription('Options...');
+ const options = existingOptions && 'subitems' in existingOptions ? existingOptions.subitems : [];
+ options.push({ description: 'Create a Copy', event: this.onCopy, icon: 'copy' });
+ !existingOptions && ContextMenu.Instance.addItem({ description: 'Options...', subitems: options, icon: 'hand-point-right' });
+ };
renderFunctionInputs() {
- const descriptionInput =
- <textarea
- className="scriptingBox-textarea-inputs"
- onChange={e => this.functionDescription = e.target.value}
- placeholder="enter description here"
- value={this.functionDescription}
- />;
- const nameInput =
- <textarea
- className="scriptingBox-textarea-inputs"
- onChange={e => this.functionName = e.target.value}
- placeholder="enter name here"
- value={this.functionName}
- />;
-
- return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} >
- <div className="scriptingBox-wrapper" style={{ maxWidth: "100%" }}>
- <div className="container" style={{ maxWidth: "100%" }}>
- <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function name: </div>
- <div style={{ maxWidth: "100%" }}> {nameInput}</div>
- <div className="descriptor" style={{ textAlign: "center", display: "inline-block", maxWidth: "100%" }}> Enter a function description: </div>
- <div style={{ maxWidth: "100%" }}>{descriptionInput}</div>
+ const descriptionInput = <textarea className="scriptingBox-textarea-inputs" onChange={e => (this.functionDescription = e.target.value)} placeholder="enter description here" value={this.functionDescription} />;
+ const nameInput = <textarea className="scriptingBox-textarea-inputs" onChange={e => (this.functionName = e.target.value)} placeholder="enter name here" value={this.functionName} />;
+
+ return (
+ <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()}>
+ <div className="scriptingBox-wrapper" style={{ maxWidth: '100%' }}>
+ <div className="container" style={{ maxWidth: '100%' }}>
+ <div className="descriptor" style={{ textAlign: 'center', display: 'inline-block', maxWidth: '100%' }}>
+ {' '}
+ Enter a function name:{' '}
+ </div>
+ <div style={{ maxWidth: '100%' }}> {nameInput}</div>
+ <div className="descriptor" style={{ textAlign: 'center', display: 'inline-block', maxWidth: '100%' }}>
+ {' '}
+ Enter a function description:{' '}
+ </div>
+ <div style={{ maxWidth: '100%' }}>{descriptionInput}</div>
+ </div>
</div>
+ {this.renderErrorMessage()}
</div>
- {this.renderErrorMessage()}
- </div>;
+ );
}
renderErrorMessage() {
- return !this._errorMessage ? (null) : <div className="scriptingBox-errorMessage"> {this._errorMessage} </div>;
+ return !this._errorMessage ? null : <div className="scriptingBox-errorMessage"> {this._errorMessage} </div>;
}
// rendering when a doc's value can be set in applied UI
renderDoc(parameter: string) {
- return <div className="scriptingBox-paramInputs" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()}
- ref={ele => ele && this.createDashEventsTarget(ele, (e, de) => this.onDrop(e, de, parameter))} >
- <EditableView display={"block"} maxHeight={72} height={35} fontSize={14}
- contents={this.dataDoc[parameter]?.title ?? "undefined"}
- GetValue={() => this.dataDoc[parameter]?.title ?? "undefined"}
- SetValue={action((value: string) => {
- const script = CompileScript(value, {
- addReturn: true,
- typecheck: false,
- transformer: DocumentIconContainer.getTransformer()
- });
- const results = script.compiled && script.run();
- if (results && results.success) {
- this._errorMessage = "";
- this.dataDoc[parameter] = results.result;
- return true;
- }
- this._errorMessage = "invalid document";
- return false;
- })}
- />
- </div>;
+ return (
+ <div className="scriptingBox-paramInputs" onFocus={this.onFocus} onBlur={() => this._overlayDisposer?.()} ref={ele => ele && this.createDashEventsTarget(ele, (e, de) => this.onDrop(e, de, parameter))}>
+ <EditableView
+ display={'block'}
+ maxHeight={72}
+ height={35}
+ fontSize={14}
+ contents={StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')}
+ GetValue={() => StrCast(DocCast(this.rootDoc[parameter])?.title, 'undefined')}
+ SetValue={action((value: string) => {
+ const script = CompileScript(value, {
+ addReturn: true,
+ typecheck: false,
+ transformer: DocumentIconContainer.getTransformer(),
+ });
+ const results = script.compiled && script.run();
+ if (results && results.success) {
+ this._errorMessage = '';
+ Doc.SetInPlace(this.rootDoc, parameter, results.result, true);
+ return true;
+ }
+ this._errorMessage = 'invalid document';
+ return false;
+ })}
+ />
+ </div>
+ );
}
// rendering when a string's value can be set in applied UI
renderBasicType(parameter: string, isNum: boolean) {
- const strVal = (isNum ? NumCast(this.dataDoc[parameter]).toString() : StrCast(this.dataDoc[parameter]));
- return <div className="scriptingBox-paramInputs" style={{ overflowY: "hidden" }}>
- <EditableView display={"block"} maxHeight={72} height={35} fontSize={14}
- contents={strVal ?? "undefined"}
- GetValue={() => strVal ?? "undefined"}
- SetValue={action((value: string) => {
- const setValue = isNum ? parseInt(value) : value;
- if (setValue !== undefined && setValue !== " ") {
- this._errorMessage = "";
- this.dataDoc[parameter] = setValue;
- return true;
- }
- this._errorMessage = "invalid input";
- return false;
- })}
- />
- </div>;
+ const strVal = isNum ? NumCast(this.rootDoc[parameter]).toString() : StrCast(this.rootDoc[parameter]);
+ return (
+ <div className="scriptingBox-paramInputs" style={{ overflowY: 'hidden' }}>
+ <EditableView
+ display={'block'}
+ maxHeight={72}
+ height={35}
+ fontSize={14}
+ contents={strVal ?? 'undefined'}
+ GetValue={() => strVal ?? 'undefined'}
+ SetValue={action((value: string) => {
+ const setValue = isNum ? parseInt(value) : value;
+ if (setValue !== undefined && setValue !== ' ') {
+ this._errorMessage = '';
+ Doc.SetInPlace(this.rootDoc, parameter, setValue, true);
+ return true;
+ }
+ this._errorMessage = 'invalid input';
+ return false;
+ })}
+ />
+ </div>
+ );
}
// rendering when an enum's value can be set in applied UI (drop down box)
renderEnum(parameter: string, types: (string | boolean | number)[]) {
- return <div className="scriptingBox-paramInputs">
- <div className="scriptingBox-viewBase">
- <div className="commandEntry-outerDiv">
- <select className="scriptingBox-viewPicker"
- onPointerDown={e => e.stopPropagation()}
- onChange={e => this.viewChanged(e, parameter)}
- value={typeof (this.dataDoc[parameter]) === "string" ? "S" + StrCast(this.dataDoc[parameter]) :
- typeof (this.dataDoc[parameter]) === "number" ? "N" + NumCast(this.dataDoc[parameter]) :
- "B" + BoolCast(this.dataDoc[parameter])}>
- {types.map(type =>
- <option className="scriptingBox-viewOption" value={(typeof (type) === "string" ? "S" : typeof (type) === "number" ? "N" : "B") + type}> {type.toString()} </option>
- )}
- </select>
+ return (
+ <div className="scriptingBox-paramInputs">
+ <div className="scriptingBox-viewBase">
+ <div className="commandEntry-outerDiv">
+ <select
+ className="scriptingBox-viewPicker"
+ onPointerDown={e => e.stopPropagation()}
+ onChange={e => this.viewChanged(e, parameter)}
+ value={typeof this.rootDoc[parameter] === 'string' ? 'S' + StrCast(this.rootDoc[parameter]) : typeof this.rootDoc[parameter] === 'number' ? 'N' + NumCast(this.rootDoc[parameter]) : 'B' + BoolCast(this.rootDoc[parameter])}>
+ {types.map(type => (
+ <option className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}>
+ {' '}
+ {type.toString()}{' '}
+ </option>
+ ))}
+ </select>
+ </div>
</div>
</div>
- </div>;
+ );
}
// setting a parameter (checking type and name before it is added)
compileParam(value: string, whichParam?: number) {
- if (value.includes(":")) {
- const ptype = value.split(":")[1].trim();
- const pname = value.split(":")[0].trim();
- if (ptype === "Doc" || ptype === "string" || ptype === "number" || ptype === "boolean" || ptype.split("|")[1]) {
+ if (value.includes(':')) {
+ const ptype = value.split(':')[1].trim();
+ const pname = value.split(':')[0].trim();
+ if (ptype === 'Doc' || ptype === 'string' || ptype === 'number' || ptype === 'boolean' || ptype.split('|')[1]) {
if ((whichParam !== undefined && pname === this.paramsNames[whichParam]) || !this.paramsNames.includes(pname)) {
- this._errorMessage = "";
+ this._errorMessage = '';
if (whichParam !== undefined) {
this.compileParams[whichParam] = value;
} else {
- this.compileParams = [...value.split(";").filter(s => s), ...this.compileParams];
+ this.compileParams = [...value.split(';').filter(s => s), ...this.compileParams];
}
return true;
}
- this._errorMessage = "this name has already been used";
+ this._errorMessage = 'this name has already been used';
} else {
- this._errorMessage = "this type is not supported";
+ this._errorMessage = 'this type is not supported';
}
} else {
- this._errorMessage = "must set type of parameter";
+ this._errorMessage = 'must set type of parameter';
}
return false;
}
@@ -403,47 +444,46 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
this._suggestions.push(StrCast(element));
}
});
- return (this._suggestions);
+ return this._suggestions;
}
@action
handleFunc(pos: number) {
- const scriptString = this.rawScript.slice(0, pos - 2);
- this._currWord = scriptString.split(" ")[scriptString.split(" ").length - 1];
+ const scriptString = this.rawText.slice(0, pos - 2);
+ this._currWord = scriptString.split(' ')[scriptString.split(' ').length - 1];
this._suggestions = [StrCast(this._scriptingParams[this._currWord])];
- return (this._suggestions);
+ return this._suggestions;
}
-
getDescription(value: string) {
const descrip = this._scriptingDescriptions[value];
- return descrip?.length > 0 ? descrip : "";
+ return descrip?.length > 0 ? descrip : '';
}
getParams(value: string) {
const params = this._scriptingParams[value];
- return params?.length > 0 ? params : "";
+ return params?.length > 0 ? params : '';
}
returnParam(item: string) {
- const params = item.split(",");
- let value = "";
+ const params = item.split(',');
+ let value = '';
let first = true;
- params.forEach((element) => {
+ params.forEach(element => {
if (first) {
- value = element.split(":")[0].trim();
+ value = element.split(':')[0].trim();
first = false;
} else {
- value = value + ", " + element.split(":")[0].trim();
+ value = value + ', ' + element.split(':')[0].trim();
}
});
return value;
}
getSuggestedParams(pos: number) {
- const firstScript = this.rawScript.slice(0, pos);
- const indexP = firstScript.lastIndexOf(".");
- const indexS = firstScript.lastIndexOf(" ");
+ const firstScript = this.rawText.slice(0, pos);
+ const indexP = firstScript.lastIndexOf('.');
+ const indexS = firstScript.lastIndexOf(' ');
const func = firstScript.slice((indexP > indexS ? indexP : indexS) + 1, firstScript.length + 1);
return this._scriptingParams[func];
}
@@ -452,63 +492,64 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
suggestionPos = () => {
const getCaretCoordinates = require('textarea-caret');
const This = this;
- document.querySelector('textarea')?.addEventListener("input", function () {
+ document.querySelector('textarea')?.addEventListener('input', function () {
const caret = getCaretCoordinates(this, this.selectionEnd);
This._selection = this;
This.resetSuggestionPos(caret);
});
- }
+ };
@action
keyHandler(e: any, pos: number) {
- if (this._lastChar === "Enter") {
- this.rawScript = this.rawScript + " ";
+ e.stopPropagation();
+ if (this._lastChar === 'Enter') {
+ this.rawText = this.rawText + ' ';
}
- if (e.key === "(") {
+ if (e.key === '(') {
this.suggestionPos();
this._scriptParamsText = this.getSuggestedParams(pos);
this._scriptSuggestedParams = this.getSuggestedParams(pos);
if (this._scriptParamsText !== undefined && this._scriptParamsText.length > 0) {
- if (this.rawScript[pos - 2] !== "(") {
+ if (this.rawText[pos - 2] !== '(') {
this._paramSuggestion = true;
}
}
- } else if (e.key === ")") {
+ } else if (e.key === ')') {
this._paramSuggestion = false;
} else {
- if (e.key === "Backspace") {
- if (this._lastChar === "(") {
+ if (e.key === 'Backspace') {
+ if (this._lastChar === '(') {
this._paramSuggestion = false;
- } else if (this._lastChar === ")") {
- if (this.rawScript.slice(0, this.rawScript.length - 1).split("(").length - 1 > this.rawScript.slice(0, this.rawScript.length - 1).split(")").length - 1) {
+ } else if (this._lastChar === ')') {
+ if (this.rawText.slice(0, this.rawText.length - 1).split('(').length - 1 > this.rawText.slice(0, this.rawText.length - 1).split(')').length - 1) {
if (this._scriptParamsText.length > 0) {
this._paramSuggestion = true;
}
}
}
- } else if (this.rawScript.split("(").length - 1 <= this.rawScript.split(")").length - 1) {
+ } else if (this.rawText.split('(').length - 1 <= this.rawText.split(')').length - 1) {
this._paramSuggestion = false;
}
}
- this._lastChar = e.key === "Backspace" ? this.rawScript[this.rawScript.length - 2] : e.key;
+ this._lastChar = e.key === 'Backspace' ? this.rawText[this.rawText.length - 2] : e.key;
if (this._paramSuggestion) {
- const parameters = this._scriptParamsText.split(",");
- const index = this.rawScript.lastIndexOf("(");
- const enteredParams = this.rawScript.slice(index, this.rawScript.length);
- const splitEntered = enteredParams.split(",");
+ const parameters = this._scriptParamsText.split(',');
+ const index = this.rawText.lastIndexOf('(');
+ const enteredParams = this.rawText.slice(index, this.rawText.length);
+ const splitEntered = enteredParams.split(',');
const numEntered = splitEntered.length;
parameters.forEach((element: string, i: number) => {
if (i !== parameters.length - 1) {
- parameters[i] = element + ",";
+ parameters[i] = element + ',';
}
});
- let first = "";
- let last = "";
+ let first = '';
+ let last = '';
parameters.forEach((element: string, i: number) => {
if (i < numEntered - 1) {
@@ -518,7 +559,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}
});
- this._scriptSuggestedParams = <div> {first} <b>{parameters[numEntered - 1]}</b> {last} </div>;
+ this._scriptSuggestedParams = (
+ <div>
+ {' '}
+ {first} <b>{parameters[numEntered - 1]}</b> {last}{' '}
+ </div>
+ );
}
}
@@ -526,61 +572,73 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
handlePosChange(number: any) {
this._caretPos = number;
if (this._caretPos === 0) {
- this.rawScript = " " + this.rawScript;
+ this.rawText = ' ' + this.rawText;
} else if (this._spaced) {
this._spaced = false;
- if (this.rawScript[this._caretPos - 1] === " ") {
- this.rawScript = this.rawScript.slice(0, this._caretPos - 1) +
- this.rawScript.slice(this._caretPos, this.rawScript.length);
+ if (this.rawText[this._caretPos - 1] === ' ') {
+ this.rawText = this.rawText.slice(0, this._caretPos - 1) + this.rawText.slice(this._caretPos, this.rawText.length);
}
}
}
+ @observable rawText: string = '';
@computed({ keepAlive: true }) get renderScriptingBox() {
TraceMobx();
- return <div style={{ width: this.compileParams.length > 0 ? "70%" : "100%" }} ref={this._scriptTextRef}>
- <ReactTextareaAutocomplete className="ScriptingBox-textarea-script"
- minChar={1}
- placeholder="write your script here"
- onFocus={this.onFocus}
- onBlur={() => this._overlayDisposer?.()}
- onChange={e => this.rawScript = e.target.value}
- value={this.rawScript}
- movePopupAsYouType={true}
- loadingComponent={() => <span>Loading</span>}
-
- trigger={{
- " ": {
- dataProvider: (token: any) => this.handleToken(token),
- component: ({ entity: value }) => this.renderFuncListElement(value),
- output: (item: any, trigger) => {
- this._spaced = true;
- return trigger + item.trim();
+ return (
+ <div style={{ width: this.compileParams.length > 0 ? '70%' : '100%' }} ref={this._scriptTextRef}>
+ <ReactTextareaAutocomplete
+ className="ScriptingBox-textarea-script"
+ minChar={1}
+ placeholder="write your script here"
+ onFocus={this.onFocus}
+ onBlur={() => this._overlayDisposer?.()}
+ onChange={action((e: any) => (this.rawText = e.target.value))}
+ value={this.rawText}
+ movePopupAsYouType={true}
+ loadingComponent={() => <span>Loading</span>}
+ trigger={{
+ ' ': {
+ dataProvider: (token: any) => this.handleToken(token),
+ component: (blob: any) => {
+ console.log('Blob', blob);
+ return this.renderFuncListElement(blob.entity);
+ },
+ output: (item: any, trigger: any) => {
+ this._spaced = true;
+ return trigger + item.trim();
+ },
},
- },
- ".": {
- dataProvider: (token: any) => this.handleToken(token),
- component: ({ entity: value }) => this.renderFuncListElement(value),
- output: (item: any, trigger) => {
- this._spaced = true;
- return trigger + item.trim();
+ '.': {
+ dataProvider: (token: any) => this.handleToken(token),
+ component: (blob: any) => {
+ console.log('Blob', blob);
+ return this.renderFuncListElement(blob.entity);
+ },
+ output: (item: any, trigger: any) => {
+ this._spaced = true;
+ return trigger + item.trim();
+ },
},
- }
- }}
- onKeyDown={(e) => this.keyHandler(e, this._caretPos)}
- onCaretPositionChange={(number: any) => this.handlePosChange(number)}
- />
- </div>;
+ }}
+ onKeyDown={(e: React.KeyboardEvent) => this.keyHandler(e, this._caretPos)}
+ onCaretPositionChange={(number: any) => this.handlePosChange(number)}
+ />
+ </div>
+ );
}
renderFuncListElement(value: string | object) {
- return (typeof value !== "string") ? (null) : <div>
- <div style={{ fontSize: "14px" }}>
- {value}
+ return typeof value !== 'string' ? null : (
+ <div>
+ <div style={{ fontSize: '14px' }}>{value}</div>
+ <div key="desc" style={{ fontSize: '10px' }}>
+ {this.getDescription(value)}
+ </div>
+ <div key="params" style={{ fontSize: '10px' }}>
+ {this.getParams(value)}
+ </div>
</div>
- <div key="desc" style={{ fontSize: "10px" }}>{this.getDescription(value)}</div>
- <div key="params" style={{ fontSize: "10px" }}>{this.getParams(value)}</div>
- </div>;
+ );
}
// inputs for scripting div (script box, params box, and params column)
@@ -589,102 +647,184 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// should there be a border? style={{ borderStyle: "groove", borderBlockWidth: "1px" }}
// params box on bottom
- const parameterInput = <div className="scriptingBox-params">
- <EditableView display={"block"} maxHeight={72} height={35} fontSize={22}
- contents={""}
- GetValue={returnEmptyString}
- SetValue={value => value && value !== " " ? this.compileParam(value) : false}
- placeholder={"enter parameters here"}
- />
- </div>;
+ const parameterInput = (
+ <div className="scriptingBox-params">
+ <EditableView
+ display={'block'}
+ maxHeight={72}
+ height={35}
+ fontSize={22}
+ contents={''}
+ GetValue={returnEmptyString}
+ SetValue={value => (value && value !== ' ' ? this.compileParam(value) : false)}
+ placeholder={'enter parameters here'}
+ />
+ </div>
+ );
// params column on right side (list)
- const definedParameters = !this.compileParams.length ? (null) :
- <div className="scriptingBox-plist" style={{ width: "30%" }}>
- {this.compileParams.map((parameter, i) =>
- <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} >
- <EditableView display={"block"} maxHeight={72} height={35} fontSize={12} background-color={"beige"}
+ const definedParameters = !this.compileParams.length ? null : (
+ <div className="scriptingBox-plist" style={{ width: '30%' }}>
+ {this.compileParams.map((parameter, i) => (
+ <div className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <EditableView
+ display={'block'}
+ maxHeight={72}
+ height={35}
+ fontSize={12}
+ background-color={'beige'}
contents={parameter}
GetValue={() => parameter}
- SetValue={value => value && value !== " " ? this.compileParam(value, i) : this.onDelete(i)}
+ SetValue={value => (value && value !== ' ' ? this.compileParam(value, i) : this.onDelete(i))}
/>
</div>
- )}
- </div>;
+ ))}
+ </div>
+ );
- return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()} >
- <div className="scriptingBox-wrapper">
- {this.renderScriptingBox}
- {definedParameters}
+ return (
+ <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected() && e.stopPropagation()}>
+ <div className="scriptingBox-wrapper">
+ {this.renderScriptingBox}
+ {definedParameters}
+ </div>
+ {parameterInput}
+ {this.renderErrorMessage()}
</div>
- {parameterInput}
- {this.renderErrorMessage()}
- </div>;
+ );
}
// toolbar (with compile and apply buttons) for scripting UI
renderScriptingTools() {
- const buttonStyle = "scriptingBox-button" + (StrCast(this.rootDoc.layoutKey).startsWith("layout_on") ? "-finish" : "");
- return <div className="scriptingBox-toolbar">
- <button className={buttonStyle} onPointerDown={e => { this.onCompile(); e.stopPropagation(); }}>Compile</button>
- <button className={buttonStyle} onPointerDown={e => { this.onApply(); e.stopPropagation(); }}>Apply</button>
- <button className={buttonStyle} onPointerDown={e => { this.onSave(); e.stopPropagation(); }}>Save</button>
-
- {!StrCast(this.rootDoc.layoutKey).startsWith("layout_on") ? (null) : // onClick, onChecked, etc need a Finish button to return to their default layout
- <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>}
- </div>;
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ return (
+ <div className="scriptingBox-toolbar">
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onCompile();
+ e.stopPropagation();
+ }}>
+ Compile
+ </button>
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onApply();
+ e.stopPropagation();
+ }}>
+ Apply
+ </button>
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onSave();
+ e.stopPropagation();
+ }}>
+ Save
+ </button>
+
+ {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onFinish();
+ e.stopPropagation();
+ }}>
+ Finish
+ </button>
+ )}
+ </div>
+ );
}
// inputs UI for params which allows you to set values for each displayed in a list
renderParamsInputs() {
- return <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()} >
- {!this.compileParams.length || !this.paramsNames ? (null) :
- <div className="scriptingBox-plist">
- {this.paramsNames.map((parameter: string, i: number) =>
- <div className="scriptingBox-pborder" onKeyPress={e => e.key === "Enter" && this._overlayDisposer?.()} >
- <div className="scriptingBox-wrapper" style={{ maxHeight: "40px" }}>
- <div className="scriptingBox-paramNames" > {`${parameter}:${this.paramsTypes[i]} = `} </div>
- {this.paramsTypes[i] === "boolean" ? this.renderEnum(parameter, [true, false]) : (null)}
- {this.paramsTypes[i] === "string" ? this.renderBasicType(parameter, false) : (null)}
- {this.paramsTypes[i] === "number" ? this.renderBasicType(parameter, true) : (null)}
- {this.paramsTypes[i] === "Doc" ? this.renderDoc(parameter) : (null)}
- {this.paramsTypes[i]?.split("|")[1] ? this.renderEnum(parameter, this.paramsTypes[i].split("|").map(s => !isNaN(parseInt(s.trim())) ? parseInt(s.trim()) : s.trim())) : (null)}
+ return (
+ <div className="scriptingBox-inputDiv" onPointerDown={e => this.props.isSelected(true) && e.stopPropagation()}>
+ {!this.compileParams.length || !this.paramsNames ? null : (
+ <div className="scriptingBox-plist">
+ {this.paramsNames.map((parameter: string, i: number) => (
+ <div className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}>
+ <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div>
+ {this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null}
+ {this.paramsTypes[i] === 'string' ? this.renderBasicType(parameter, false) : null}
+ {this.paramsTypes[i] === 'number' ? this.renderBasicType(parameter, true) : null}
+ {this.paramsTypes[i] === 'Doc' ? this.renderDoc(parameter) : null}
+ {this.paramsTypes[i]?.split('|')[1]
+ ? this.renderEnum(
+ parameter,
+ this.paramsTypes[i].split('|').map(s => (!isNaN(parseInt(s.trim())) ? parseInt(s.trim()) : s.trim()))
+ )
+ : null}
+ </div>
</div>
- </div>)}
- </div>}
- {this.renderErrorMessage()}
- </div>;
+ ))}
+ </div>
+ )}
+ {this.renderErrorMessage()}
+ </div>
+ );
}
// 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" : "");
- return <div className="scriptingBox-toolbar">
- <button className={buttonStyle} onPointerDown={e => { this.onEdit(); e.stopPropagation(); }}>Edit</button>
- <button className={buttonStyle} onPointerDown={e => { func(); e.stopPropagation(); }}>{toolSet}</button>
- {!StrCast(this.rootDoc.layoutKey).startsWith("layout_on") ? (null) :
- <button className={buttonStyle} onPointerDown={e => { this.onFinish(); e.stopPropagation(); }}>Finish</button>}
- </div>;
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ return (
+ <div className="scriptingBox-toolbar">
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onEdit();
+ e.stopPropagation();
+ }}>
+ Edit
+ </button>
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ func();
+ e.stopPropagation();
+ }}>
+ {toolSet}
+ </button>
+ {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : (
+ <button
+ className={buttonStyle}
+ onPointerDown={e => {
+ this.onFinish();
+ e.stopPropagation();
+ }}>
+ Finish
+ </button>
+ )}
+ </div>
+ );
}
// renders script UI if _applied = false and params UI if _applied = true
render() {
+ console.log(ReactTextareaAutocomplete);
TraceMobx();
return (
- <div className={`scriptingBox`} onContextMenu={this.specificContextMenu}
- onPointerUp={!this._function ? this.suggestionPos : undefined}>
- <div className="scriptingBox-outerDiv"
- onWheel={e => this.props.isSelected(true) && e.stopPropagation()}>
- {this._paramSuggestion ? <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: "inline" }}> {this._scriptSuggestedParams} </div> : null}
+ <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}>
+ <div className="scriptingBox-outerDiv" onWheel={e => this.props.isSelected(true) && e.stopPropagation()}>
+ {this._paramSuggestion ? (
+ <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}>
+ {' '}
+ {this._scriptSuggestedParams}{' '}
+ </div>
+ ) : null}
{!this._applied && !this._function ? this.renderScriptingInputs : null}
{this._applied && !this._function ? this.renderParamsInputs() : null}
{!this._applied && this._function ? this.renderFunctionInputs() : null}
{!this._applied && !this._function ? this.renderScriptingTools() : null}
- {this._applied && !this._function ? this.renderTools("Run", () => this.onRun()) : null}
- {!this._applied && this._function ? this.renderTools("Create Function", () => this.onCreate()) : null}
+ {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null}
+ {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null}
</div>
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index f0d7bd2f3..aa51714da 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,6 @@
-.mini-viewer{
+@import "../global/globalCssVariables.scss";
+
+.mini-viewer {
cursor: grab;
position: absolute;
right: 10;
@@ -13,23 +15,24 @@
width: 100%;
height: 100%;
position: relative;
+
.videoBox-viewer {
- display:flex;
+ display: flex;
flex-direction: column;
height: 100%;
border-radius: inherit;
- opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
+ opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
+ background: $dark-gray;
}
+
.inkingCanvas-paths-markers {
- opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
- }
- .collectionStackedTimeline {
- background: beige;
+ opacity: 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
}
+
.videoBox-stackPanel {
z-index: -1;
width: 100%;
- position: relative;
+ position: relative;
}
.videoBox-annotationLayer {
@@ -43,26 +46,33 @@
}
}
-.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
-.videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
+.videoBox-content-YouTube,
+.videoBox-content-YouTube-fullScreen,
+.videoBox-content,
+.videoBox-content-interactive,
+.videoBox-cont-fullScreen {
width: 100%;
z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
position: absolute;
+
video {
width: auto;
- height: 100%;
- display: flex;
+ height: 100%;
+ display: flex;
margin: auto;
}
}
-.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
+.videoBox-content,
+.videoBox-content-interactive,
+.videoBox-content-fullScreen {
width: 100%;
- height: 100%;
+ height: 100%;
left: 0px;
}
-.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen {
+.videoBox-content-YouTube,
+.videoBox-content-YouTube-fullScreen {
height: 100%;
}
@@ -70,28 +80,127 @@
// pointer-events: all;
// }
+.videoBox-ui-wrapper {
+ width: 0;
+ height: 0;
+}
+
.videoBox-ui {
position: absolute;
flex-direction: row;
- right: 5px;
- top: 5px;
- display: none;
- background-color: rgba(0, 0, 0, 0.6);
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ background-color: $dark-gray;
+ color: white;
+ border-radius: 100px;
+ height: 40px;
+ padding: 0 10px 0 7px;
+ transition: opacity 0.3s;
+ z-index: 100001;
+
+ .timecode-controls {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ margin: 0 2px;
+ flex-grow: 2;
+ font-size: 14px;
+
+ .timeline-slider {
+ margin: 5px;
+ flex-grow: 2;
+ }
+ }
+
+ .toolbar-slider.volume, .toolbar-slider.zoom {
+ width: 50px;
+ }
+
+ .videobox-button {
+ margin: 2px;
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+ border-radius: 50%;
+ background: $dark-gray;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: $black;
+ }
+
+ svg {
+ width: 15px;
+ height: 15px;
+ }
+ }
}
-.videoBox-time, .videoBox-snapshot, .videoBox-timelineButton, .videoBox-play, .videoBox-full {
- color : white;
+
+.videoBox-time,
+.videoBox-snapshot,
+.videoBox-timelineButton,
+.videoBox-play,
+.videoBox-full {
+ color: white;
position: relative;
transform-origin: left top;
- pointer-events:all;
+ pointer-events: all;
padding-right: 5px;
cursor: pointer;
+
&:hover {
- background-color: gray;
+ background-color: $medium-gray;
}
}
-.videoBox:hover {
+.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive {
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+
.videoBox-ui {
- display: flex;
+ left: 50%;
+ top: 90%;
+ transform: translate(-50%, -50%);
+ width: 80%;
+ transition: top 0s, width 0s, opacity 0.3s, visibility 0.3s;
}
+}
+
+video::-webkit-media-controls {
+ display: none !important;
+}
+
+input[type="range"] {
+ -webkit-appearance: none;
+ background: none;
+}
+
+input[type="range"]:focus {
+ outline: none;
+}
+
+input[type="range"]::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 10px;
+ cursor: pointer;
+ box-shadow: 0;
+ background: $light-gray;
+ border-radius: 10px;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+ box-shadow: 0;
+ border: 0;
+ height: 12px;
+ width: 12px;
+ border-radius: 10px;
+ background: $medium-blue;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -1px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 33fdc4935..a3d501153 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,114 +1,249 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from "mobx";
-import { observer } from "mobx-react";
-import { basename } from "path";
+import React = require('react');
+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 } from "../../../fields/Doc";
-import { InkTool } from "../../../fields/InkField";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, VideoField } from "../../../fields/URLField";
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
-import { Docs, DocUtils } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Networking } from "../../Network";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { DocumentManager } from "../../util/DocumentManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { SnappingManager } from "../../util/SnappingManager";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
-import { DocumentDecorations } from "../DocumentDecorations";
-import { MarqueeAnnotator } from "../MarqueeAnnotator";
-import { AnchorMenu } from "../pdf/AnchorMenu";
-import { StyleProp } from "../StyleProvider";
+import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { InkTool } from '../../../fields/InkField';
+import { List } from '../../../fields/List';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
+import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, 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 { ReplayMovements } from '../../util/ReplayMovements';
+import { SelectionManager } from '../../util/SelectionManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { undoBatch } from '../../util/UndoManager';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { DocumentDecorations } from '../DocumentDecorations';
+import { MarqueeAnnotator } from '../MarqueeAnnotator';
+import { AnchorMenu } from '../pdf/AnchorMenu';
+import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
-import { LinkDocPreview } from "./LinkDocPreview";
-import "./VideoBox.scss";
+import { RecordingBox } from './RecordingBox';
+import './VideoBox.scss';
+const path = require('path');
+
+/**
+ * VideoBox
+ * Main component: VideoBox.tsx
+ * Supporting Components: CollectionStackedTimeline
+ *
+ * VideoBox is a node that supports the playback of video files in Dash.
+ * When a video file or YouTube video is importeed into Dash, it is immediately rendered as a VideoBox document.
+ * CollectionStackedTimline handles AudioBox and VideoBox shared behavior, but VideoBox handles playing, pausing, etc because it contains <video> element
+ * User can trim video: nondestructive, just sets new bounds for playback and rendering timeline
+ * Like images, users can zoom and pan and it has an overlay layer allowing for annotations on top of the video at different times
+ */
@observer
export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+ 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 Instance: VideoBox;
- static heightPercent = 60; // height of timeline in percent of height of videoBox.
+ static heightPercent = 80; // height of video relative to videoBox when timeline is open
+ static numThumbnails = 20;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _youtubePlayer: YT.Player | undefined = undefined;
- private _videoRef: HTMLVideoElement | null = null;
+ private _videoRef: HTMLVideoElement | null = null; // <video> ref
+ private _contentRef: HTMLDivElement | null = null; // ref to div that wraps video and controls for full screen
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
- private _stackedTimeline = React.createRef<CollectionStackedTimeline>();
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _audioPlayer: HTMLAudioElement | null = null;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _playRegionTimer: any = null;
- private _playRegionDuration = 0;
- @observable static _nativeControls: boolean;
- @observable _marqueeing: number[] | undefined;
+ private _playRegionTimer: any = null; // timeout for playback
+ private _controlsFadeTimer: any = null; // timeout for controls fade
+
+ @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+ @observable static _nativeControls: boolean; // default html controls
+ @observable _marqueeing: number[] | undefined; // coords for marquee selection
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@observable _screenCapture = false;
- @observable _clicking = false;
+ @observable _clicking = false; // used for transition between showing/hiding timeline
@observable _forceCreateYouTubeIFrame = false;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
@observable _playing = false;
- @computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); }
- @computed get duration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ @observable _finished: boolean = false; // has playback reached end of clip
+ @observable _volume: number = 1;
+ @observable _muted: boolean = false;
+ @observable _controlsTransform?: { X: number; Y: number };
+ @observable _controlsVisible: boolean = true;
+ @observable _scrubbing: boolean = false;
- private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
- public get player(): HTMLVideoElement | null { return this._videoRef; }
+ @computed get links() {
+ return DocListCast(this.dataDoc.links);
+ }
+ @computed get heightPercent() {
+ return NumCast(this.layoutDoc._timelineHeightPercent, 100);
+ } // current percent of video relative to VideoBox height
+ // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ @observable rawDuration: number = 0;
- constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
- super(props);
- VideoBox.Instance = this;
+ @computed get youtubeVideoId() {
+ const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+ return field && field.url.href.indexOf('youtube') !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split('/')) : '';
}
- getAnchor = () => {
- const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
- const marquee = AnchorMenu.Instance.GetAnchor?.();
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+ // returns the path of the audio file
+ @computed get audiopath() {
+ const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+ const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
+ return field?.url.href ?? vfield?.url.href ?? '';
}
- videoLoad = () => {
- const aspect = this.player!.videoWidth / this.player!.videoHeight;
- Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
- Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
- if (Number.isFinite(this.player!.duration)) {
- this.dataDoc[this.fieldKey + "-duration"] = this.player!.duration;
+ // returns the presentation data if it exists, null otherwise
+ @computed get presentation() {
+ const data = this.dataDoc[this.fieldKey + '-presentation'];
+ return data ? JSON.parse(data) : null;
+ }
+
+ @computed private get timeline() {
+ return this._stackedTimeline;
+ }
+ private get transition() {
+ return this._clicking ? 'left 0.5s, width 0.5s, height 0.5s' : '';
+ } // css transition for hiding/showing timeline
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
+ }
+
+ componentDidMount() {
+ this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
+ if (this.youtubeVideoId) {
+ const youtubeaspect = 400 / 315;
+ const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+ const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+ if (!nativeWidth || !nativeHeight) {
+ if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+ Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+ }
+ }
+ this.player && this.setPlayheadTime(0);
+ document.addEventListener('keydown', this.keyEvents, true);
+
+ if (this.presentation) {
+ ReplayMovements.Instance.setVideoBox(this);
+ }
+ }
+
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this.Pause();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ document.removeEventListener('keydown', this.keyEvents, true);
+
+ if (this.presentation) {
+ ReplayMovements.Instance.removeVideoBox();
}
}
+ // handles key events, when timeline scrubs fade controls
+ @action
+ keyEvents = (e: KeyboardEvent) => {
+ if (
+ // need to include range inputs because after dragging time slider it becomes target element
+ !(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
+ this.props.isSelected(true)
+ ) {
+ switch (e.key) {
+ case 'ArrowLeft':
+ case 'ArrowRight':
+ clearTimeout(this._controlsFadeTimer);
+ this._scrubbing = true;
+ this._controlsFadeTimer = setTimeout(
+ action(() => (this._scrubbing = false)),
+ 500
+ );
+ e.stopPropagation();
+ break;
+ }
+ }
+ };
+
+ // plays video
@action public Play = (update: boolean = true) => {
+ if (this._playRegionTimer) return;
+
this._playing = true;
- try {
- this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
- update && this.player?.play();
- update && this._audioPlayer?.play();
- update && this._youtubePlayer?.playVideo();
- this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
- } catch (e) {
- console.log("Video Play Exception:", e);
+ const eleTime = this.player?.currentTime || 0;
+ if (this.timeline) {
+ let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+ if (this._finished) {
+ // restarts video if reached end on previous play
+ this._finished = false;
+ start = this.timeline.trimStart;
+ }
+
+ try {
+ this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
+ update && this.player && this.playFrom(start, undefined, true);
+ update && this._audioPlayer?.play();
+ update && this._youtubePlayer?.playVideo();
+ this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+ } catch (e) {
+ console.log('Video Play Exception:', e);
+ }
}
this.updateTimecode();
- }
+ };
+ // goes to time
@action public Seek(time: number) {
try {
this._youtubePlayer?.seekTo(Math.round(time), true);
} catch (e) {
- console.log("Video Seek Exception:", e);
+ console.log('Video Seek Exception:', e);
}
this.player && (this.player.currentTime = time);
this._audioPlayer && (this._audioPlayer.currentTime = time);
+ // TODO: revisit this and clean it
+ if ((this.player?.currentTime || -1) < this.rawDuration) {
+ this._finished = false;
+ }
}
+ // pauses video
@action public Pause = (update: boolean = true) => {
this._playing = false;
+ this.removeCurrentlyPlaying();
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
@@ -116,47 +251,93 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
} catch (e) {
- console.log("Video Pause Exception:", e);
+ console.log('Video Pause Exception:', e);
}
this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
this._playTimer = undefined;
- this.props.renderDepth !== -1 && this.updateTimecode();
- }
+ this.updateTimecode();
+ if (!this._finished) {
+ clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
+ }
+ this._playRegionTimer = undefined;
+ };
+ // toggles video full screen
@action public FullScreen = () => {
- this._fullScreen = true;
- this.player && this.player.requestFullscreen();
+ if (document.fullscreenElement === this._contentRef) {
+ this._fullScreen = false;
+ this.player && this._contentRef && document.exitFullscreen();
+ } else {
+ this._fullScreen = true;
+ this.player && this._contentRef && this._contentRef.requestFullscreen();
+ }
try {
- this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
+ this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
} catch (e) {
- console.log("Video FullScreen Exception:", e);
+ console.log('Video FullScreen Exception:', e);
}
- }
+ };
- @action public Snapshot(downX?: number, downY?: number) {
+ // fades out controls in fullscreen after mouse stops moving
+ @action controlsFade = (e: PointerEvent) => {
+ e.stopPropagation();
+ if (!this._scrubbing) {
+ clearTimeout(this._controlsFadeTimer);
+ this._controlsVisible = true;
+ this._controlsFadeTimer = setTimeout(
+ action(() => (this._controlsVisible = false)),
+ 3000
+ );
+ }
+ };
+
+ // drag controls around window in fulls screen
+ @action controlsDrag = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const eleStyle = getComputedStyle(e.target as Element);
+ this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) };
+
+ setupMoveUpEvents(
+ e.target,
+ e,
+ action((e, down, delta) => {
+ if (this._controlsTransform) {
+ this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth));
+ this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight));
+ }
+ return false;
+ }),
+ emptyFunction,
+ emptyFunction
+ );
+ };
+
+ // creates and links snapshot photo of current video frame
+ @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
const width = NumCast(this.layoutDoc._width);
const canvas = document.createElement('canvas');
canvas.width = 640;
- canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
- const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ canvas.height = (640 * Doc.NativeHeight(this.layoutDoc)) / (Doc.NativeWidth(this.layoutDoc) || 1);
+ const ctx = canvas.getContext('2d'); //draw image to canvas. scale to target dimensions
if (ctx) {
- // ctx.rect(0, 0, canvas.width, canvas.height);
- // ctx.fillStyle = "blue";
- // ctx.fill();
this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
}
if (!this._videoRef) {
const b = Docs.Create.LabelDocument({
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
- _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y, 1),
+ _width: 150,
+ _height: 50,
+ title: (this.layoutDoc._currentTimecode || 0).toString(),
+ _isLinkButton: true,
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
- Networking.PostToServer("/youtubeScreenshot", {
+ DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
- timecode: this.layoutDoc._currentTimecode
+ timecode: this.layoutDoc._currentTimecode,
}).then(response => {
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
@@ -168,469 +349,805 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
//convert to desired file format
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 retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
+ const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
- returnedFilename && this.createRealSummaryLink(returnedFilename, downX, downY));
+ VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(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.Snapshot(undefined, undefined, makeIcon);
+ };
- private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
- const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+ // creates link for snapshot
+ createRealSummaryLink = (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, {
- _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
- _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
+ _nativeWidth: Doc.NativeWidth(this.layoutDoc),
+ _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+ x: NumCast(this.layoutDoc.x) + width,
+ y: NumCast(this.layoutDoc.y),
+ _isLinkButton: true,
+ _width: 150,
+ _height: (height / width) * 150,
+ title: '--snapshot' + NumCast(this.layoutDoc._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() }, "video snapshot");
+ const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, '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));
- }
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ };
+
+ getAnchor = () => {
+ const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
+ const marquee = AnchorMenu.Instance.GetAnchor?.();
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+ };
+ // sets video info on load
+ videoLoad = action(() => {
+ const aspect = this.player!.videoWidth / this.player!.videoHeight;
+ Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
+ Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
+ if (Number.isFinite(this.player!.duration)) {
+ this.rawDuration = this.player!.duration;
+ } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
+ });
+
+ // updates video time
@action
updateTimecode = () => {
this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
try {
this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
- console.log("Video Timecode Exception:", e);
+ console.log('Video Timecode Exception:', e);
}
- }
+ };
- componentDidMount() {
- 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._disposers.triggerVideo = reaction(
- () => !LinkDocPreview.LinkInfo && this.props.renderDepth !== -1 ? NumCast(this.Document._triggerVideo, null) : undefined,
- time => time !== undefined && setTimeout(() => {
- this.player && this.Play();
- setTimeout(() => this.Document._triggerVideo = undefined, 10);
- }, this.player ? 0 : 250), // wait for mainCont and try again to play
- { fireImmediately: true }
- );
- this._disposers.triggerStop = reaction(
- () => this.props.renderDepth !== -1 && !LinkDocPreview.LinkInfo ? NumCast(this.Document._triggerVideoStop, null) : undefined,
- stop => stop !== undefined && setTimeout(() => {
- this.player && this.Pause();
- setTimeout(() => this.Document._triggerVideoStop = undefined, 10);
- }, this.player ? 0 : 250), // wait for mainCont and try again to play
- { fireImmediately: true }
- );
- if (this.youtubeVideoId) {
- const youtubeaspect = 400 / 315;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+ // extracts video thumbnails and saves them as field of doc
+ getVideoThumbnails = () => {
+ if (this.dataDoc.thumbnails !== undefined) return;
+ this.dataDoc.thumbnails = new List<string>();
+ const thumbnailPromises: Promise<any>[] = [];
+ const video = document.createElement('video');
+
+ video.onloadedmetadata = () => (video.currentTime = 0);
+
+ video.onseeked = () => {
+ const canvas = document.createElement('canvas');
+ canvas.height = 100;
+ canvas.width = 100;
+ 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));
+ 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)));
}
- }
- }
+ };
- componentWillUnmount() {
- this.Pause();
- Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
- }
+ const field = Cast(this.dataDoc[this.fieldKey], VideoField);
+ field && (video.src = field.url.href);
+ };
+ // sets video element ref
@action
setVideoRef = (vref: HTMLVideoElement | null) => {
this._videoRef = vref;
if (vref) {
this._videoRef!.ontimeupdate = this.updateTimecode;
// @ts-ignore
- vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
+ // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
this._disposers.reactionDisposer?.();
- this._disposers.reactionDisposer = reaction(() => NumCast(this.layoutDoc._currentTimecode),
- time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
+ this._disposers.reactionDisposer = reaction(
+ () => NumCast(this.layoutDoc._currentTimecode),
+ time => !this._playing && (vref.currentTime = time),
+ { fireImmediately: true }
+ );
+
+ (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails();
}
- }
+ };
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false) {
- try {
- const posting = Utils.prepend("/uploadURI");
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix
- },
- json: true,
+ // set ref for div that wraps video and controls for fullscreen
+ @action
+ setContentRef = (cref: HTMLDivElement | null) => {
+ this._contentRef = cref;
+ if (cref) {
+ cref.onfullscreenchange = action(e => {
+ this._fullScreen = document.fullscreenElement === cref;
+ this._controlsVisible = true;
+ this._scrubbing = false;
+ clearTimeout(this._controlsFadeTimer);
+ if (this._fullScreen) {
+ document.addEventListener('pointermove', this.controlsFade);
+ } else {
+ document.removeEventListener('pointermove', this.controlsFade);
+ }
});
- return returnedUri;
-
- } catch (e) {
- console.log("VideoBox :" + e);
}
- }
+ };
+ // context menu
specificContextMenu = (e: React.MouseEvent): void => {
const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
if (field) {
const url = field.url.href;
const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
- subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
- this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
- description: "Screen Capture", event: (async () => {
- runInAction(() => this._screenCapture = !this._screenCapture);
- this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
- }), icon: "expand-arrows-alt"
+ subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
+ subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
+ this.rootDoc.type === DocumentType.SCREENSHOT &&
+ subitems.push({
+ description: 'Screen Capture',
+ event: async () => {
+ runInAction(() => (this._screenCapture = !this._screenCapture));
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ },
+ icon: 'expand-arrows-alt',
+ });
+ subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? '' : "Don't") + ' follow links when encountered', event: () => (this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks), icon: 'expand-arrows-alt' });
+ subitems.push({
+ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? '' : "Don't") + ' play when link is selected',
+ event: () => (this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks),
+ icon: 'expand-arrows-alt',
+ });
+ subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : 'Auto play') + ' anchors onClick', event: () => (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors), icon: 'expand-arrows-alt' });
+ // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: 'Copy path',
+ event: () => {
+ Utils.CopyText(url);
+ },
+ icon: 'expand-arrows-alt',
});
- subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
- subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
- subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
- subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
+ // if the videobox was turned from a recording box
+ if (this.dataDoc[this.fieldKey + '-recorded'] === true) {
+ subitems.push({
+ description: 'Recreate recording',
+ event: () => {
+ this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+ // delete assoicated video data
+ this.dataDoc[this.props.fieldKey] = '';
+ this.dataDoc[this.fieldKey + '-duration'] = '';
+ // delete assoicated presentation data
+ this.dataDoc[this.fieldKey + '-presentation'] = '';
+ },
+ icon: 'expand-arrows-alt',
+ });
+ }
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: subitems, icon: 'video' });
}
- }
+ };
- // returns the path of the audio file
- @computed get audiopath() {
- const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
- const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
- return field?.url.href ?? vfield?.url.href ?? "";
- }
// ref for updating time
- _audioPlayer: HTMLAudioElement | null = null;
- setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+ setAudioRef = (e: HTMLAudioElement | null) => (this._audioPlayer = e);
+
+ // renders the video and audio
@computed get content() {
const field = Cast(this.dataDoc[this.fieldKey], VideoField);
- const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
- return !field ? <div key="loading">Loading</div> :
- <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
- <div className={classname}>
- <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ const interactive = Doc.ActiveTool !== InkTool.None || !this.props.isSelected() ? '' : '-interactive';
+ const classname = 'videoBox-content' + (this._fullScreen ? '-fullScreen' : '') + interactive;
+ const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
+ return !field ? (
+ <div key="loading">Loading</div>
+ ) : (
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+ <div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
+ {this._fullScreen && (
+ <div
+ className="videoBox-ui"
+ onPointerDown={this.controlsDrag}
+ style={{
+ left: this._controlsTransform && this._controlsTransform.X,
+ top: this._controlsTransform && this._controlsTransform.Y,
+ visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden',
+ opacity: opacity,
+ }}>
+ {this.UIButtons}
+ </div>
+ )}
+ <video
+ key="video"
+ autoPlay={this._screenCapture}
+ ref={this.setVideoRef}
+ style={this._fullScreen ? this.fullScreenSize() : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
onSeeked={this.updateTimecode}
onPause={() => this.Pause()}
- onClick={e => e.preventDefault()}>
+ onClick={this._fullScreen ? () => (this.playing() ? this.Pause() : this.Play()) : e => e.preventDefault()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
</video>
- {!this.audiopath || this.audiopath === field.url.href ? (null) :
- <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+ {!this.audiopath || this.audiopath === field.url.href ? null : (
+ <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}>
<source src={this.audiopath} type="audio/mpeg" />
Not supported.
- </audio>}
+ </audio>
+ )}
</div>
- </div>;
- }
-
- @computed get youtubeVideoId() {
- const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
- return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
+ </div>
+ );
}
@action youtubeIframeLoaded = (e: any) => {
if (!this._youtubeContentCreated) {
this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
return;
- }
- else this._youtubeContentCreated = false;
+ } else this._youtubeContentCreated = false;
this.loadYouTube(e.target);
- }
- private loadYouTube = (iframe: any) => {
+ };
+
+ loadYouTube = (iframe: any) => {
let started = true;
- const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
- if (started && event.data === YT.PlayerState.PLAYING) {
- started = false;
- this._youtubePlayer?.unMute();
- //this.Pause();
- return;
- }
- if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
- });
+ const onYoutubePlayerStateChange = (event: any) =>
+ runInAction(() => {
+ if (started && event.data === YT.PlayerState.PLAYING) {
+ started = false;
+ this._youtubePlayer?.unMute();
+ //this.Pause();
+ return;
+ }
+ if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
+ });
const onYoutubePlayerReady = (event: any) => {
this._disposers.reactionDisposer?.();
this._disposers.youtubeReactionDisposer?.();
- this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
+ this._disposers.reactionDisposer = reaction(
+ () => this.layoutDoc._currentTimecode,
+ () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))
+ );
this._disposers.youtubeReactionDisposer = reaction(
- () => CurrentUserUtils.SelectedTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
- (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
+ () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+ interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'),
+ { fireImmediately: true }
+ );
};
- if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
+ if (typeof YT === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
else {
(YT as any)?.ready(() => {
this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
events: {
- 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
- 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
- }
+ onReady: this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
+ onStateChange: this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
+ },
});
});
}
- }
- private get uIButtons() {
- const curTime = NumCast(this.layoutDoc._currentTimecode);
- const nonNativeControls = [
- <Tooltip title={<div className="dash-tooltip">{"playback"}</div>} key="play" placement="bottom">
- <div className="videoBox-play" onPointerDown={this.onPlayDown} >
- <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
- </div>
- </Tooltip>,
- <Tooltip title={<div className="dash-tooltip">{"timecode"}</div>} key="time" placement="bottom">
- <div className="videoBox-time" onPointerDown={this.onResetDown} >
- <span>{formatTime(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.floor((curTime - Math.trunc(curTime)) * 100).toString().padStart(2, "0")}</span>
- </div>
- </Tooltip>,
- <Tooltip title={<div className="dash-tooltip">{"view full screen"}</div>} key="full" placement="bottom">
- <div className="videoBox-full" onPointerDown={this.FullScreen}>
- <FontAwesomeIcon icon="expand" size="lg" />
- </div>
- </Tooltip>];
- return <div className="videoBox-ui">
- {[...(VideoBox._nativeControls ? [] : nonNativeControls),
- <Tooltip title={<div className="dash-tooltip">{"snapshot current frame"}</div>} key="snap" placement="bottom">
- <div className="videoBox-snapshot" onPointerDown={this.onSnapshotDown} >
- <FontAwesomeIcon icon="camera" size="lg" />
- </div>
- </Tooltip>,
- <Tooltip title={<div className="dash-tooltip">{"show annotation timeline"}</div>} key="timeline" placement="bottom">
- <div className="videoBox-timelineButton" onPointerDown={this.onTimelineHdlDown}>
- <FontAwesomeIcon icon="eye" size="lg" />
- </div>
- </Tooltip>,]}
- </div>;
- }
+ };
- onPlayDown = () => this._playing ? this.Pause() : this.Play();
+ // for play button
+ onPlayDown = () => (this._playing ? this.Pause() : this.Play());
+ // for fullscreen button
onFullDown = (e: React.PointerEvent) => {
this.FullScreen();
e.stopPropagation();
e.preventDefault();
- }
+ };
+ // for snapshot button
onSnapshotDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e) => {
- this.Snapshot(e.clientX, e.clientY);
- return true;
- }, emptyFunction, () => this.Snapshot());
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ e => {
+ this.Snapshot(e.clientX, e.clientY);
+ return true;
+ },
+ emptyFunction,
+ () => this.Snapshot()
+ );
+ };
- onTimelineHdlDown = action((e: React.PointerEvent) => {
+ // for show/hide timeline button, transitions between show/hide
+ @action
+ onTimelineHdlDown = (e: React.PointerEvent) => {
this._clicking = true;
- setupMoveUpEvents(this, e,
- action((e: PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ action(encodeURIComponent => {
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));
+ // 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._timelineHeightPercent = 80;
}
return false;
- }), emptyFunction,
+ }),
+ emptyFunction,
() => {
this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
- setTimeout(action(() => this._clicking = false), 500);
- }, this.props.isContentActive(), this.props.isContentActive());
- });
-
- onResetDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e,
- (e: PointerEvent) => {
- this.Seek(Math.max(0, NumCast(this.layoutDoc._currentTimecode) + Math.sign(e.movementX) * 0.0333));
- e.stopImmediatePropagation();
- return false;
+ setTimeout(
+ action(() => (this._clicking = false)),
+ 500
+ );
},
- emptyFunction,
- (e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
- }
+ this.props.isContentActive(),
+ this.props.isContentActive()
+ );
+ };
+
+ // removes video from currently playing display
+ @action
+ removeCurrentlyPlaying = () => {
+ if (CollectionStackedTimeline.CurrentlyPlaying) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+ }
+ };
+
+ // adds video to currently playing display
+ @action
+ addCurrentlyPlaying = () => {
+ if (!CollectionStackedTimeline.CurrentlyPlaying) {
+ CollectionStackedTimeline.CurrentlyPlaying = [];
+ }
+ if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ }
+ };
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
- return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+ return (
+ <iframe
+ key={this._youtubeIframeId}
+ id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+ onPointerLeave={this.updateTimecode}
+ onLoad={this.youtubeIframeLoaded}
+ className={classname}
+ width={Doc.NativeWidth(this.layoutDoc) || 640}
+ height={Doc.NativeHeight(this.layoutDoc) || 390}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`}
+ />
+ );
}
+ // for annotating, adds doc with time info
@action.bound
addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
const curTime = NumCast(this.layoutDoc._currentTimecode);
- docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
+ docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
return this.addDocument(doc);
}
- // play back the video from time
+ // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
@action
- playFrom = (seekTimeInSeconds: number, endTime: number = this.duration) => {
+ playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
clearTimeout(this._playRegionTimer);
- this._playRegionDuration = endTime - seekTimeInSeconds;
+ this._playRegionTimer = undefined;
if (Number.isNaN(this.player?.duration)) {
setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
} else if (this.player) {
- if (seekTimeInSeconds < 0) {
- if (seekTimeInSeconds > -1) {
- setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
- } else {
- this.Pause();
- }
- } else if (seekTimeInSeconds <= this.player.duration) {
- this.player.currentTime = seekTimeInSeconds;
+ // trimBounds override requested playback bounds
+ const end = Math.min(this.timeline?.trimEnd ?? this.rawDuration, endTime ?? this.timeline?.trimEnd ?? this.rawDuration);
+ const start = Math.max(this.timeline?.trimStart ?? 0, seekTimeInSeconds);
+ const playRegionDuration = end - start;
+ // checks if times are within clip range
+ if (seekTimeInSeconds >= 0 && (this.timeline?.trimStart || 0) <= end && seekTimeInSeconds <= (this.timeline?.trimEnd || this.rawDuration)) {
+ this.player.currentTime = start;
this._audioPlayer && (this._audioPlayer.currentTime = seekTimeInSeconds);
this.player.play();
this._audioPlayer?.play();
- runInAction(() => this._playing = true);
- if (endTime !== this.duration) {
- this._playRegionTimer = setTimeout(() => this.Pause(), (this._playRegionDuration) * 1000); // use setTimeout to play a specific duration
- }
+ this._playing = true;
+ this.addCurrentlyPlaying();
+ this._playRegionTimer = setTimeout(() => {
+ // need to keep track of if end of clip is reached so on next play, clip restarts
+ if (fullPlay) this._finished = true;
+ // removes from currently playing if playback has reached end of range marker
+ else this.removeCurrentlyPlaying();
+ this.Pause();
+ }, playRegionDuration * 1000);
} else {
this.Pause();
}
}
+ };
+
+ // ends trim, hides trim controls and displays new clip
+ @undoBatch
+ finishTrim = action(() => {
+ this.Pause();
+ this.setPlayheadTime(Math.max(Math.min(this.timeline?.trimEnd || 0, this.player!.currentTime), this.timeline?.trimStart || 0));
+ this.timeline?.StopTrimming();
+ });
+
+ // displays trim controls to start trimming clip
+ startTrim = (scope: TrimScope) => {
+ this.Pause();
+ this.timeline?.StartTrimming(scope);
+ };
+
+ // for trim button, double click displays full clip, single displays curr trim bounds
+ onClipPointerDown = (e: React.PointerEvent) => {
+ // if timeline isn't shown, show first then trim
+ this.heightPercent >= 100 && this.onTimelineHdlDown(e);
+ this.timeline &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action((e: PointerEvent, doubleTap?: boolean) => {
+ if (doubleTap) {
+ this.startTrim(TrimScope.All);
+ } else if (this.timeline) {
+ this.Pause();
+ this.timeline.IsTrimming !== TrimScope.None ? this.finishTrim() : this.startTrim(TrimScope.Clip);
+ }
+ })
+ );
+ };
+
+ // for volume slider sets volume
+ @action
+ setVolume = (volume: number) => {
+ if (this.player) {
+ this._volume = volume;
+ this.player.volume = volume;
+ if (this._muted) {
+ this.toggleMute();
+ }
+ }
+ };
+
+ // toggles video mute
+ @action
+ toggleMute = () => {
+ if (this.player) {
+ this._muted = !this._muted;
+ this.player.muted = this._muted;
+ }
+ };
+
+ // stretches vertically or horizontally depending on video orientation so video fits full screen
+ fullScreenSize() {
+ if (this._videoRef && this._videoRef.videoHeight / this._videoRef.videoWidth > 1) {
+ return { height: '100%' };
+ } else {
+ return { width: '100%' };
+ }
}
+ // for zoom slider, sets timeline waveform zoom
+ zoom = (zoom: number) => {
+ this.timeline?.setZoom(zoom);
+ };
+
+ // plays link
playLink = (doc: Doc) => {
- const startTime = Math.max(0, (this._stackedTimeline.current?.anchorStart(doc) || 0));
- const endTime = this._stackedTimeline.current?.anchorEnd(doc);
+ const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0);
+ const endTime = this.timeline?.anchorEnd(doc);
if (startTime !== undefined) {
if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
else this.Seek(startTime);
}
- }
-
- playing = () => this._playing;
- timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
- setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
- timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
- @computed get renderTimeline() {
- return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
- <CollectionStackedTimeline ref={this._stackedTimeline} {...this.props}
- fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + "-dictation"}
- mediaPath={this.audiopath}
- renderDepth={this.props.renderDepth + 1}
- startTag={"_timecodeToShow" /* videoStart */}
- endTag={"_timecodeToHide" /* videoEnd */}
- bringToFront={emptyFunction}
- CollectionView={undefined}
- duration={this.duration}
- playFrom={this.playFrom}
- setTime={this.setAnchorTime}
- playing={this.playing}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={this.timelineScreenToLocal}
- Play={this.Play}
- Pause={this.Pause}
- playLink={this.playLink}
- PanelHeight={this.timelineHeight}
- trimming={false}
- trimStart={0}
- trimEnd={this.duration}
- trimDuration={this.duration}
- setStartTrim={emptyFunction}
- setEndTrim={emptyFunction}
- />
- </div>;
- }
-
- @computed get annotationLayer() {
- return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
- }
+ };
+ // starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].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
+ );
}
- }
+ };
- finishMarquee = action(() => {
+ // ends marquee selection
+ @action
+ finishMarquee = () => {
this._marqueeing = undefined;
this.props.select(true);
- });
+ };
+
+ timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
+
+ timelineScreenToLocal = () =>
+ this.props
+ .ScreenToLocalTransform()
+ .scale(this.scaling())
+ .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight());
+
+ setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time);
+
+ timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100;
+
+ playing = () => this._playing;
- @computed get fitWidth() { return this.props.docViewPath?.().slice(-1)[0].fitWidth; }
contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
- scaling = () => this.props.scaling?.() || 1;
- panelWidth = (): number => this.fitWidth ? this.props.PanelWidth() : (Doc.NativeAspect(this.rootDoc) || 1) * this.panelHeight();
- panelHeight = (): number => this.fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.heightPercent / 100 * this.props.PanelHeight();
+
+ 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);
+
screenToLocalTransform = () => {
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
- return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
- }
- marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
- marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+ return this.props
+ .ScreenToLocalTransform()
+ .translate(-offset, 0)
+ .scale(100 / this.heightPercent);
+ };
+
+ marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100;
+ marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];
+
timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+
+ // renders video controls
+ componentUI = (boundsLeft: number, boundsTop: number) => {
+ const bounds = this.props.docViewPath().lastElement().getBounds();
+ const left = bounds?.left || 0;
+ const right = bounds?.right || 0;
+ const top = bounds?.top || 0;
+ const height = (bounds?.bottom || 0) - top;
+ const width = Math.max(right - left, 100);
+ const uiHeight = Math.max(25, Math.min(50, height / 10));
+ const uiMargin = Math.min(10, height / 20);
+ const vidHeight = (height * this.heightPercent) / 100;
+ const yPos = top + vidHeight - uiHeight - uiMargin;
+ const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
+ const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
+ return this._fullScreen || right - left < 50 ? null : (
+ <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? 'top 0.5s' : '', opacity: opacity }}>
+ {this.UIButtons}
+ </div>
+ </div>
+ );
+ };
+
+ @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);
+ return (
+ <>
+ <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
+ </div>
+
+ {this.timeline && width > 150 && (
+ <div className="timecode-controls">
+ <div className="timecode-current">{formatTime(curTime)}</div>
+
+ {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
+ <div className="timeline-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={this.timeline.clipStart}
+ max={this.timeline.clipEnd}
+ value={curTime}
+ className="toolbar-slider time-progress"
+ onPointerDown={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = true;
+ })}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ onPointerUp={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = false;
+ })}
+ />
+ </div>
+ ) : (
+ <div>/</div>
+ )}
+
+ <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
+ </div>
+ )}
+
+ <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+ <FontAwesomeIcon icon="expand" />
+ </div>
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" />
+ </div>
+ )}
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
+ </div>
+ )}
+
+ <div
+ className="videobox-button"
+ title={this._muted ? 'unmute' : 'mute'}
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.toggleMute();
+ }}>
+ <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+ </div>
+ {width > 300 && (
+ <input
+ type="range"
+ style={{ width: `min(25%, 50px)` }}
+ step="0.1"
+ min="0"
+ max="1"
+ value={this._muted ? 0 : this._volume}
+ className="toolbar-slider volume"
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+ />
+ )}
+
+ {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
+ <>
+ <div className="videobox-button" title="zoom">
+ <FontAwesomeIcon icon="search-plus" />
+ </div>
+ <input
+ type="range"
+ step="0.1"
+ min="1"
+ max="5"
+ value={this.timeline?._zoomFactor}
+ className="toolbar-slider zoom"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ this.zoom(Number(e.target.value));
+ }}
+ />
+ </>
+ )}
+ </>
+ );
+ }
+
+ // renders CollectionStackedTimeline
+ @computed get renderTimeline() {
+ return (
+ <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+ <CollectionStackedTimeline
+ ref={action((r: any) => (this._stackedTimeline = r))}
+ {...this.props}
+ fieldKey={this.annotationKey}
+ dictationKey={this.fieldKey + '-dictation'}
+ mediaPath={this.audiopath}
+ renderDepth={this.props.renderDepth + 1}
+ startTag={'_timecodeToShow' /* videoStart */}
+ endTag={'_timecodeToHide' /* videoEnd */}
+ bringToFront={emptyFunction}
+ CollectionView={undefined}
+ playFrom={this.playFrom}
+ setTime={this.setPlayheadTime}
+ playing={this.playing}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ Play={this.Play}
+ Pause={this.Pause}
+ playLink={this.playLink}
+ PanelHeight={this.timelineHeight}
+ rawDuration={this.rawDuration}
+ />
+ </div>
+ );
+ }
+
+ // renders annotation layer
+ @computed get annotationLayer() {
+ return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+ }
+
+ savedAnnotations = () => this._savedAnnotations;
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
- const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
- return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
- style={{
- pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
- borderRadius,
- overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
- }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
- <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
- <div style={{
- position: "absolute", transition: this.transition,
- width: this.panelWidth(),
- height: this.panelHeight(),
- top: 0,
- left: (this.props.PanelWidth() - this.panelWidth()) / 2
+ const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / this.scaling()}px` : borderRad;
+ return (
+ <div
+ className="videoBox"
+ onContextMenu={this.specificContextMenu}
+ ref={this._mainCont}
+ style={{
+ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
+ borderRadius,
+ overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined,
+ }}
+ onWheel={e => {
+ e.stopPropagation();
+ e.preventDefault();
}}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- renderDepth={this.props.renderDepth + 1}
- fieldKey={this.annotationKey}
- CollectionView={undefined}
- isAnnotationOverlay={true}
- annotationLayerHostsContent={true}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- docFilters={this.timelineDocFilter}
- select={emptyFunction}
- scaling={returnOne}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
- </CollectionFreeFormView>
+ <div className="videoBox-viewer" onPointerDown={this.marqueeDown}>
+ <div
+ style={{
+ position: 'absolute',
+ transition: this.transition,
+ width: this.panelWidth(),
+ height: this.panelHeight(),
+ top: 0,
+ left: (this.props.PanelWidth() - this.panelWidth()) / 2,
+ }}>
+ <CollectionFreeFormView
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ renderDepth={this.props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ isAnnotationOverlay={true}
+ annotationLayerHostsContent={true}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ docFilters={this.timelineDocFilter}
+ select={emptyFunction}
+ NativeDimScaling={returnOne}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocWithTimecode}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ {this.annotationLayer}
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={this.marqueeFitScaling}
+ docView={this.props.docViewPath().slice(-1)[0]}
+ containerOffset={this.marqueeOffset}
+ addDocument={this.addDocWithTimecode}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current}
+ />
+ )}
+ {this.renderTimeline}
</div>
- {this.annotationLayer}
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator
- rootDoc={this.rootDoc}
- scrollTop={0}
- down={this._marqueeing}
- scaling={this.marqueeFitScaling}
- docView={this.props.docViewPath().slice(-1)[0]}
- containerOffset={this.marqueeOffset}
- addDocument={this.addDocWithTimecode}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this._savedAnnotations}
- annotationLayer={this._annotationLayer.current}
- mainCont={this._mainCont.current}
- />}
- {this.renderTimeline}
- {this.uIButtons}
</div>
- </div >);
+ );
}
}
-VideoBox._nativeControls = false; \ No newline at end of file
+VideoBox._nativeControls = false;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 7dc970496..d8dd074a5 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -5,11 +5,18 @@
height: 100%;
position: relative;
display: flex;
- .webBox-background {
+
+ .webBox-sideResizer {
+ position: absolute;
width: 100%;
height: 100%;
cursor: ew-resize;
- background: lightGray;
+ background: darkgray;
+ }
+
+ .webBox-background {
+ width: 100%;
+ height: 100%;
}
.webBox-ui {
@@ -114,7 +121,7 @@
box-shadow: $standard-box-shadow;
transition: 0.2s;
- &:hover{
+ &:hover {
filter: brightness(0.85);
}
}
@@ -149,11 +156,15 @@
position: absolute;
top: 0;
left: 0;
+ cursor: text;
+ padding: 15px;
+ height: 100%
}
.webBox-cont {
pointer-events: none;
}
+
.webBox-cont,
.webBox-cont-interactive {
padding: 0vw;
@@ -187,13 +198,14 @@
top: 0;
left: 0;
overflow: auto;
+
.webBox-innerContent {
position: relative;
}
}
div.webBox-outerContent::-webkit-scrollbar-thumb {
- display: none;
+ cursor: nw-resize;
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 7ff47107e..ca9f363c1 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,50 +1,48 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+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 { 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 { ComputedField } from "../../../fields/ScriptField";
-import { Cast, ImageCast, NumCast, StrCast } from "../../../fields/Types";
-import { ImageField, WebField } from "../../../fields/URLField";
-import { TraceMobx } from "../../../fields/util";
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { CurrentUserUtils } from "../../util/CurrentUserUtils";
-import { KeyCodes } from "../../util/KeyCodes";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { SnappingManager } from "../../util/SnappingManager";
-import { undoBatch } from "../../util/UndoManager";
-import { MarqueeOptionsMenu } from "../collections/collectionFreeForm";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-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 { SidebarAnnos } from "../SidebarAnnos";
-import { StyleProp } from "../StyleProvider";
-import { DocumentViewProps } from "./DocumentView";
+import { Doc, DocListCast, 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 { ImageField, WebField } from '../../../fields/URLField';
+import { TraceMobx } from '../../../fields/util';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SnappingManager } from '../../util/SnappingManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+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 { SidebarAnnos } from '../SidebarAnnos';
+import { StyleProp } from '../StyleProvider';
+import { DocumentViewProps } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
-import { LinkDocPreview } from "./LinkDocPreview";
-import { VideoBox } from "./VideoBox";
-import "./WebBox.scss";
-import React = require("react");
-const { CreateImage } = require("./WebBoxRenderer");
-const _global = (window /* browser */ || global /* node */) as any;
-const htmlToText = require("html-to-text");
-
+import { LinkDocPreview } from './LinkDocPreview';
+import { VideoBox } from './VideoBox';
+import './WebBox.scss';
+import React = require('react');
+const { CreateImage } = require('./WebBoxRenderer');
+const _global = (window /* browser */ || global) /* node */ as any;
+const htmlToText = require('html-to-text');
@observer
export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(WebBox, fieldKey);
+ }
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
@@ -53,12 +51,13 @@ 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>;
+ private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop));
+ private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
private _sidebarRef = React.createRef<SidebarAnnos>();
private _searchRef = React.createRef<HTMLInputElement>();
- private _searchString = "";
- @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 _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
+ private _searchString = '';
+ @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 _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;
@@ -69,20 +68,42 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable private _iframeClick: HTMLIFrameElement | undefined = undefined;
@observable private _iframe: HTMLIFrameElement | null = null;
@observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
- @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight, 1500);
- @computed get _url() { return this.webField?.toString() || ""; }
- @computed get _urlHash() { return this._url ? WebBox.urlHash(this._url) + "" : ""; }
- @computed get scrollHeight() { return this._scrollHeight; }
- @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); }
- @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); }
- @computed get webField() { return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url; }
- @computed get webThumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url; }
+ @observable private _scrollHeight = NumCast(this.layoutDoc.scrollHeight);
+ @computed get _url() {
+ return this.webField?.toString() || '';
+ }
+ @computed get _urlHash() {
+ return this._url ? WebBox.urlHash(this._url) + '' : '';
+ }
+ @computed get scrollHeight() {
+ return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight);
+ }
+ @computed get allAnnotations() {
+ return DocListCast(this.dataDoc[this.annotationKey]);
+ }
+ @computed get inlineTextAnnotations() {
+ return this.allAnnotations.filter(a => a.textInlineAnnotations);
+ }
+ @computed get webField() {
+ return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url;
+ }
+ @computed get webThumb() {
+ return (
+ this.props.thumbShown?.() &&
+ 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.thumb
+ : undefined
+ )
+ )?.url
+ );
+ }
constructor(props: any) {
super(props);
- Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
- Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
- runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it.
+ runInAction(() => (this._webUrl = this._url)); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it.
}
@action
@@ -102,68 +123,94 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
(this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
}
return true;
- }
+ };
+ @action
+ setScrollPos = (pos: number) => {
+ if (!this._outerRef.current || this._outerRef.current.scrollHeight < pos) {
+ if (this._webPageHasBeenRendered) setTimeout(() => this.setScrollPos(pos), 250);
+ } else {
+ this._outerRef.current.scrollTop = pos;
+ this._initialScroll = undefined;
+ }
+ };
+
+ updateThumb = async () => {
+ const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href;
+ const scrollTop = NumCast(this.layoutDoc._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) => {
+ 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);
+ });
+ }
+ };
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)
- this.dataDoc[this.fieldKey + "-annotations"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`);
- this.dataDoc[this.fieldKey + "-sidebar"] = ComputedField.MakeFunction(`copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`);
+ reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`;
+ reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`;
+ DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
});
- reaction(() => this.props.isSelected(),
- async (selected) => {
+ reaction(
+ () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document),
+ async selected => {
if (selected) {
this._webPageHasBeenRendered = true;
- setTimeout(action(() => {
- this._scrollHeight = Math.max(this.scrollHeight, this._iframe?.contentDocument?.body.scrollHeight || 0);
- if (this._initialScroll !== undefined && this._outerRef.current) {
- setTimeout(() => {
- this._outerRef.current!.scrollTop = this._initialScroll!;
- this._initialScroll = undefined;
- });
- }
- }));
- } else if (!this.props.isContentActive() &&
- !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.
- const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href;
- if (this._iframe && !imageBitmap) {
- 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;
- const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
- CreateImage(
- this._webUrl.endsWith("/") ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl,
- this._iframe.contentDocument?.styleSheets ?? [],
- htmlString,
- nativeWidth,
- nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
- NumCast(this.layoutDoc._scrollTop)
- ).then
- ((dataUrl: any) => {
- VideoBox.convertDataUri(dataUrl, this.layoutDoc[Id] + "-thumb" + (new Date()).getTime(), true).then(
- returnedfilename => setTimeout(action(() => this.layoutDoc.thumb = new ImageField(returnedfilename)), 500));
- })
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
- });
- }
+ } 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.autoHeight = reaction(() => this.layoutDoc._autoHeight,
+ 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.scaling?.() || 1));
+ 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));
}
- });
+ }
+ );
- if (this.webField?.href.indexOf("youtube") !== -1) {
+ if (this.webField?.href.indexOf('youtube') !== -1) {
const youtubeaspect = 400 / 315;
const nativeWidth = Doc.NativeWidth(this.layoutDoc);
const nativeHeight = Doc.NativeHeight(this.layoutDoc);
@@ -181,42 +228,38 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
- var quickScroll = true;
- this._disposers.scrollReaction = reaction(() => NumCast(this.layoutDoc._scrollTop),
- (scrollTop) => {
- if (quickScroll) this._initialScroll = scrollTop;
- else {
- const viewTrans = StrCast(this.Document._viewTransition);
- const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
- const durationSecStr = viewTrans.match(/([0-9.]*)s/);
- const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
- this.goTo(scrollTop, duration);
- }
+ this._disposers.scrollReaction = reaction(
+ () => NumCast(this.layoutDoc._scrollTop),
+ scrollTop => {
+ const viewTrans = StrCast(this.Document._viewTransition);
+ const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
+ const durationSecStr = viewTrans.match(/([0-9.]*)s/);
+ const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
+ this.goTo(scrollTop, duration);
},
{ fireImmediately: true }
);
- quickScroll = false;
}
- componentWillUnmount() {
+ @action componentWillUnmount() {
Object.values(this._disposers).forEach(disposer => disposer?.());
- this._iframe?.removeEventListener('wheel', this.iframeWheel, true);
- this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
+ // this._iframe?.removeEventListener('wheel', this.iframeWheel, true);
+ // this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
}
@action
- createTextAnnotation = (sel: Selection, selRange: Range) => {
- if (this._mainCont.current) {
+ createTextAnnotation = (sel: Selection, selRange: Range | undefined) => {
+ if (this._mainCont.current && selRange) {
const clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
const rect = clientRects.item(i);
if (rect && rect.width !== this._mainCont.current.clientWidth) {
- const annoBox = document.createElement("div");
- annoBox.className = "marqueeAnnotator-annotationBox";
+ const annoBox = document.createElement('div');
+ annoBox.className = 'marqueeAnnotator-annotationBox';
// transforms the positions from screen onto the pdf div
annoBox.style.top = (rect.top + this._mainCont.current.scrollTop).toString();
- annoBox.style.left = (rect.left).toString();
- annoBox.style.width = (rect.width).toString();
- annoBox.style.height = (rect.height).toString();
+ annoBox.style.left = rect.left.toString();
+ annoBox.style.width = rect.width.toString();
+ annoBox.style.height = rect.height.toString();
this._annotationLayer.current && MarqueeAnnotator.previewNewAnnotation(this._savedAnnotations, this._annotationLayer.current, annoBox, 1);
}
}
@@ -224,69 +267,76 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
//this._selectionText = selRange.cloneContents().textContent || "";
// clear selection
- if (sel.empty) sel.empty();// Chrome
- else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox
- }
+ if (sel.empty) sel.empty(); // Chrome
+ else if (sel.removeAllRanges) sel.removeAllRanges(); // Firefox
+ return this._savedAnnotations;
+ };
menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
scrollFocus = (doc: Doc, smooth: boolean) => {
if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
- if (DocListCast(this.props.Document[this.fieldKey + "-sidebar"]).includes(doc) && !this.SidebarShown) {
+ if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
this.toggleSidebar(!smooth);
}
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
if (doc !== this.rootDoc && this._outerRef.current) {
- const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1);
- if (scrollTo !== undefined) {
+ 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 = smooth ? 500 : 0;
- this._initialScroll !== undefined && (this._initialScroll = scrollTo);
this.goTo(scrollTo, focusSpeed);
return focusSpeed;
+ } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
+ this._initialScroll = scrollTo;
}
}
- this._initialScroll = NumCast(this.layoutDoc._scrollTop);
- return 0;
- }
+ return undefined;
+ };
getAnchor = () => {
const anchor =
- AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ??
+ this._getAnchor(this._savedAnnotations) ??
Docs.Create.WebanchorDocument(this._url, {
- title: StrCast(this.rootDoc.title + " " + this.layoutDoc._scrollTop),
+ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop),
y: NumCast(this.layoutDoc._scrollTop),
- unrendered: true
+ unrendered: true,
});
this.addDocumentWrapper(anchor);
return anchor;
- }
+ };
+
+ _textAnnotationCreator: (() => ObservableMap<number, HTMLDivElement[]>) | undefined;
+ savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations;
@action
iframeUp = (e: PointerEvent) => {
+ this._textAnnotationCreator = undefined;
this.props.docViewPath().lastElement()?.docView?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here.
if (this._iframe?.contentWindow && this._iframe.contentDocument && !this._iframe.contentWindow.getSelection()?.isCollapsed) {
const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
- const scale = (this.props.scaling?.() || 1) * mainContBounds.scale;
+ const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const sel = this._iframe.contentWindow.getSelection();
if (sel) {
- this.createTextAnnotation(sel, sel.getRangeAt(0));
- AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX,
- e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale);
+ 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);
}
}
- }
+ };
@action
iframeDown = (e: PointerEvent) => {
+ const sel = this._iframe?.contentWindow?.getSelection?.();
const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
- const scale = (this.props.scaling?.() || 1) * mainContBounds.scale;
+ const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX * scale + mainContBounds.translateX,
- e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._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), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
+ this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._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)),
+ 100
+ ); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
} else {
this._iframeClick = this._iframe ?? undefined;
this._isAnnotating = true;
@@ -302,20 +352,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ContextMenu.Instance.closeMenu();
ContextMenu.Instance.setIgnoreEvents(true);
}
- }
+ };
getScrollHeight = () => this._scrollHeight;
isFirefox = () => {
- return "InstallTrigger" in window; // navigator.userAgent.indexOf("Chrome") !== -1;
- }
+ return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
+ };
iframeClick = () => this._iframeClick;
iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale;
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
- let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString());
+ if (this._initialScroll !== undefined) {
+ this.setScrollPos(this._initialScroll);
+ }
+ let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString());
if (requrlraw !== this._url.toString()) {
if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g);
@@ -323,85 +376,104 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (matches) {
requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch));
for (let i = 1; i < Array.from(matches)?.length; i++) {
- requrlraw = requrlraw.replace(matches[i], "");
+ requrlraw = requrlraw.replace(matches[i], '');
}
}
- requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", "");
+ requrlraw = requrlraw
+ .replace(/q=[^&]*/, newsearch.substring(1))
+ .replace('search&', 'search?')
+ .replace('?gbv=1', '');
}
this.submitURL(requrlraw, undefined, true);
}
if (iframe?.contentDocument) {
- iframe.contentDocument.addEventListener("pointerup", this.iframeUp);
- iframe.contentDocument.addEventListener("pointerdown", this.iframeDown);
- this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight);
- setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000);
- iframe.setAttribute("enable-annotation", "true");
- iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => {
- let href = "";
- for (let ele = e.target as any; ele; ele = ele.parentElement) {
- href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href;
- }
- const origin = this.webField?.origin;
- if (href && origin) {
- e.stopPropagation();
- setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin)));
- if (this._outerRef.current) {
- this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop);
- this._outerRef.current.scrollLeft = 0;
- }
- }
- })));
+ iframe.contentDocument.addEventListener('pointerup', this.iframeUp);
+ iframe.contentDocument.addEventListener('pointerdown', this.iframeDown);
+ this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument.body.scrollHeight);
+ setTimeout(
+ action(() => (this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0))),
+ 5000
+ );
+ iframe.setAttribute('enable-annotation', 'true');
+ iframe.contentDocument.addEventListener(
+ 'click',
+ undoBatch(
+ action((e: MouseEvent) => {
+ let href = '';
+ for (let ele = e.target as any; ele; ele = ele.parentElement) {
+ href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href;
+ }
+ const origin = this.webField?.origin;
+ if (href && origin) {
+ e.stopPropagation();
+ setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin)));
+ if (this._outerRef.current) {
+ this._outerRef.current.scrollTop = NumCast(this.layoutDoc._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));
}
- }
+ };
@action
iframeWheel = (e: any) => {
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
+ this._scrollTimer = setTimeout(
+ action(() => (this._scrollTimer = undefined)),
+ 250
+ ); // this turns events off on the iframe which allows scrolling to change direction smoothly
}
- }
+ };
@action
setDashScrollTop = (scrollTop: number, timeout: number = 250) => {
- const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight());
- timeout = scrollTop > iframeHeight ? 0 : timeout;
+ const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
this._scrollTimer && clearTimeout(this._scrollTimer);
- this._scrollTimer = setTimeout(action(() => {
- this._scrollTimer = undefined;
- if (!LinkDocPreview.LinkInfo && this._outerRef.current &&
- (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
- this.layoutDoc._scrollTop = this._outerRef.current.scrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
- }
- }), timeout);
- }
+ 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
+ );
+ };
goTo = (scrollTop: number, duration: number) => {
if (this._outerRef.current) {
- const iframeHeight = Math.max(1000, this._scrollHeight - this.panelHeight());
- scrollTop = scrollTop > iframeHeight + 50 ? iframeHeight : scrollTop;
+ const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
if (duration) {
smoothScroll(duration, [this._outerRef.current], scrollTop);
this.setDashScrollTop(scrollTop, duration);
} else {
this.setDashScrollTop(scrollTop);
}
- }
- }
+ } else this._initialScroll = scrollTop;
+ };
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.dataDoc[this.fieldKey + '-future'], listSpec('string'), []);
+ const history = Cast(this.dataDoc[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()!));
if (this._webUrl === this._url) {
this._webUrl = curUrl;
- setTimeout(action(() => this._webUrl = this._url));
+ setTimeout(action(() => (this._webUrl = this._url)));
} else {
this._webUrl = this._url;
}
@@ -409,21 +481,21 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
});
return false;
- }
+ };
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.dataDoc[this.fieldKey + '-future'], listSpec('string'));
+ const history = Cast(this.dataDoc[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()!));
if (this._webUrl === this._url) {
this._webUrl = curUrl;
- setTimeout(action(() => this._webUrl = this._url));
+ setTimeout(action(() => (this._webUrl = this._url)));
} else {
this._webUrl = this._url;
}
@@ -431,22 +503,33 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
});
return false;
- }
+ };
static urlHash = (s: string) => {
- return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0));
- }
+ return Math.abs(
+ s.split('').reduce((a: any, b: any) => {
+ a = (a << 5) - a + b.charCodeAt(0);
+ return a & a;
+ }, 0)
+ );
+ };
@action
submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => {
if (!newUrl) return;
- if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl;
+ 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 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.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]);
this.layoutDoc._scrollTop = 0;
+ 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) {
@@ -454,47 +537,54 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
!dontUpdateIframe && (this._webUrl = this._url);
}
} catch (e) {
- console.log("WebBox URL error:" + this._url);
+ console.log('WebBox URL error:' + this._url);
}
return true;
- }
+ };
onWebUrlDrop = (e: React.DragEvent) => {
const { dataTransfer } = e;
- const html = dataTransfer.getData("text/html");
- 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;
+ const html = dataTransfer.getData('text/html');
+ 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);
e.stopPropagation();
- }
+ };
onWebUrlValueKeyDown = (e: React.KeyboardEvent) => {
- e.key === "Enter" && this.submitURL(this._keyInput.current!.value);
+ e.key === 'Enter' && this.submitURL(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}
+ <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) => {
+ onClick={e => {
this._keyInput.current!.select();
e.stopPropagation();
}}
ref={this._keyInput}
/>
- <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}>
+ <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>
+ <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>
);
@@ -503,44 +593,59 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
specificContextMenu = (e: React.MouseEvent | PointerEvent): void => {
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
- if (!cm.findByDescription("Options...")) {
- !Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" });
+ 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' });
funcs.push({
- description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => {
+ description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts',
+ event: () => {
this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts;
if (this._iframe) {
- runInAction(() => this._hackHide = true);
- setTimeout(action(() => this._hackHide = false));
+ runInAction(() => (this._hackHide = true));
+ setTimeout(action(() => (this._hackHide = false)));
}
- }, icon: "snowflake"
+ },
+ icon: 'snowflake',
});
funcs.push({
- description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => {
- const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1);
+ description: (!this.layoutDoc.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;
if (nw) {
- Doc.SetInPlace(this.layoutDoc, this.fieldKey + "-nativeWidth", nw, true);
+ Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true);
}
- }, icon: "snowflake"
+ },
+ icon: 'snowflake',
});
- cm.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+ cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
- }
+ };
@action
onMarqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ 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, e?: PointerEvent) => {
+ this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this._marqueeing = undefined;
this._isAnnotating = false;
this._iframeClick = undefined;
+ const sel = this._iframe?.contentDocument?.getSelection();
+ if (sel?.empty) sel.empty(); // Chrome
+ else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
if (x !== undefined && y !== undefined) {
this._setPreviewCursor?.(x, y, false, false);
ContextMenu.Instance.closeMenu();
@@ -550,141 +655,228 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.props.docViewPath().lastElement().docView?.onContextMenu(undefined, x, y);
}
}
- }
+ };
@computed get urlContent() {
- if (this._hackHide || (this.webThumb && (!this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc))) return (null);
+ 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;
if (field instanceof HtmlField) {
- view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
+ 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 = <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}
- // 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`} />;
+ view = (
+ <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}
+ // 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"} />;
+ 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._webPageHasBeenRendered = true));
+ 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;
}
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.webUrl = this._url));
return this.addDocument(doc, annotationKey);
- }
+ };
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
return this.addDocumentWrapper(doc, sidebarKey);
- }
+ };
@observable _draggingSidebar = false;
- sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf
- setupMoveUpEvents(this, e, action((e, down, delta) => {
- this._draggingSidebar = true;
- const localDelta = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformDirection(delta[0], delta[1]);
- 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.scaling?.() || 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;
+ sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => {
+ const batch = UndoManager.StartBatch('sidebar');
+ // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e, down, delta) => {
+ this._draggingSidebar = true;
+ const localDelta = this.props
+ .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 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;
+ }
+ return false;
+ }),
+ action((e, movement, isClick) => {
+ this._draggingSidebar = false;
+ !isClick && batch.end();
+ }),
+ () => {
+ this.toggleSidebar();
+ batch.end();
}
- return false;
- }), action(() => this._draggingSidebar = false), () => this.toggleSidebar());
+ );
+ };
+ @computed get sidebarHandle() {
+ return (
+ <div
+ className="webBox-overlayButton-sidebar"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}
+ onPointerDown={e => this.sidebarBtnDown(e, true)}>
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+ </div>
+ );
}
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- const 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]();
+ 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']);
+ }
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;
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
if (preview) {
this._previewNativeWidth = nativeWidth * sideratio;
- this._previewWidth = this.layoutDoc[WidthSym]() * nativeWidth * sideratio / curNativeWidth;
+ this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth;
this._showSidebar = true;
- }
- else {
- this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
- this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ } else {
+ this.layoutDoc._showSidebar = !this.layoutDoc._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;
+ } else {
+ this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
+ }
}
});
- sidebarWidth = () => !this.SidebarShown ? 0 :
- this._previewWidth ? WebBox.openSidebarWidth :
- (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() /
- NumCast(this.layoutDoc.nativeWidth)
+ sidebarWidth = () =>
+ !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
@computed get content() {
- const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents !== "none" && CurrentUserUtils.SelectedTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
- return <div className={"webBox-cont" + (interactive ? "-interactive" : "")}
- style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]) || `100%` : "100%", }}>
- {this.urlContent}
- </div>;
+ const interactive =
+ !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+ 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>
+ );
}
@computed get annotationLayer() {
TraceMobx();
- const pe = this.pointerEvents();
- return <div className="webBox-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 {...this.props} fieldKey={this.annotationKey} pointerEvents={pe} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
- </div>;
-
+ return (
+ <div className="webBox-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 {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
+ ))}
+ </div>
+ );
+ }
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
}
- @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
@computed get searchUI() {
- return <div className="webBox-ui"
- onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? "flex" : "none" }}>
- <div className="webBox-overlayCont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
- <button className="webBox-overlayButton" title={"search"} />
- <input className="webBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged}
- onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, e.shiftKey)} />
- <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
- <FontAwesomeIcon icon="search" size="sm" />
+ return (
+ <div className="webBox-ui" onPointerDown={e => e.stopPropagation()} style={{ display: this.props.isContentActive() ? 'flex' : 'none' }}>
+ <div className="webBox-overlayCont" onPointerDown={e => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
+ <button className="webBox-overlayButton" title={'search'} />
+ <input
+ className="webBox-searchBar"
+ placeholder="Search"
+ ref={this._searchRef}
+ onChange={this.searchStringChanged}
+ onKeyDown={e => {
+ e.key === 'Enter' && this.search(this._searchString, e.shiftKey);
+ e.stopPropagation();
+ }}
+ />
+ <button className="webBox-search" title="Search" onClick={e => this.search(this._searchString, e.shiftKey)}>
+ <FontAwesomeIcon icon="search" size="sm" />
+ </button>
+ </div>
+ <button
+ className="webBox-overlayButton"
+ title={'search'}
+ onClick={action(() => {
+ this._searching = !this._searching;
+ this.search('', false, true);
+ })}>
+ <div className="webBox-overlayButton-arrow" onPointerDown={e => e.stopPropagation()} />
+ <div className="webBox-overlayButton-iconCont" onPointerDown={e => e.stopPropagation()}>
+ <FontAwesomeIcon icon={this._searching ? 'times' : 'search'} size="lg" />
+ </div>
</button>
</div>
- <button className="webBox-overlayButton" title={"search"}
- onClick={action(() => { this._searching = !this._searching; this.search("", false, true); })} >
- <div className="webBox-overlayButton-arrow" onPointerDown={(e) => e.stopPropagation()} />
- <div className="webBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
- <FontAwesomeIcon icon={this._searching ? "times" : "search"} size="lg" />
- </div>
- </button>
- </div>;
+ );
}
- 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.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.scaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+ 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));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
- basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter("textInlineAnnotations")];
+ basicFilter = () => [...this.props.docFilters(), Utils.PropUnsetFilter('textInlineAnnotations')];
transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
- childStyleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+ childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (doc instanceof Doc && property === StyleProp.PointerEvents) {
- if (doc.textInlineAnnotations) return "none";
+ if (doc.textInlineAnnotations) return 'none';
}
return this.props.styleProvider?.(doc, props, property);
- }
- pointerEvents = () => !this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents !== "none" && !MarqueeOptionsMenu.Instance.isShown() ? "all" : SnappingManager.GetIsDragging() ? undefined : "none";
+ };
+ pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
+ annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none');
render() {
- const pointerEvents = this.props.layerProvider?.(this.layoutDoc) === false ? "none" : this.props.pointerEvents ? this.props.pointerEvents as any : undefined;
+ const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
- const scale = previewScale * (this.props.scaling?.() || 1);
- const renderAnnotations = (docFilters?: () => string[]) =>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
+ const renderAnnotations = (docFilters?: () => string[]) => (
+ <CollectionFreeFormView
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.annotationKey}
@@ -693,88 +885,105 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.scrollXf}
- scaling={returnOne}
- dropAction={"alias"}
+ NativeDimScaling={returnOne}
+ dropAction={'alias'}
docFilters={docFilters || this.basicFilter}
dontRenderDocuments={docFilters ? false : true}
select={emptyFunction}
- ContentScaling={returnOne}
bringToFront={emptyFunction}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={this.sidebarAddDocument}
+ addDocument={this.addDocument}
styleProvider={this.childStyleProvider}
- childPointerEvents={this.props.isContentActive() ? "all" : undefined}
- pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
+ pointerEvents={this.annotationPointerEvents}
+ />
+ );
return (
- <div className="webBox" ref={this._mainCont}
- style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? "none" : undefined }} >
- <div className="webBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
- <div className="webBox-container" style={{
- position: "absolute",
- width: `calc(${100 / scale}% - ${(this.sidebarWidth() + WebBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`,
- transform: `scale(${scale})`,
- pointerEvents
- }} onContextMenu={this.specificContextMenu}>
- <div className={"webBox-outerContent"} ref={this._outerRef}
+ <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: this.props.thumbShown?.() ? 'none' : undefined }}>
+ <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
+ <div
+ className="webBox-container"
+ style={{
+ width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : ((this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale) * (this._previewWidth ? scale : 1)}px)`,
+ transform: `scale(${scale})`,
+ pointerEvents,
+ }}
+ onContextMenu={this.specificContextMenu}>
+ <div
+ className={'webBox-outerContent'}
+ ref={this._outerRef}
style={{
height: `${100 / scale}%`,
- pointerEvents
+ pointerEvents,
}}
- onWheel={e => { e.stopPropagation(); e.preventDefault(); }} // block wheel events from propagating since they're handled by the iframe
+ onWheel={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ }} // block wheel events from propagating since they're handled by the iframe
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
- onPointerDown={this.onMarqueeDown}
- >
- <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}>
+ onPointerDown={this.onMarqueeDown}>
+ <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
{this.content}
- <div style={{ mixBlendMode: "multiply" }}>
- {renderAnnotations(this.transparentFilter)}
- </div>
+ <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
- {SnappingManager.GetIsDragging() ? (null) : renderAnnotations()}
+ {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
{this.annotationLayer}
</div>
</div>
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator rootDoc={this.rootDoc}
- iframe={this.isFirefox() ? this.iframeClick : undefined}
- iframeScaling={this.isFirefox() ? this.iframeScaling : undefined}
- 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}
- mainCont={this._mainCont.current} />}
- </div >
- <SidebarAnnos ref={this._sidebarRef}
- {...this.props}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- fieldKey={this.fieldKey + "-" + this._urlHash}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- setHeight={emptyFunction}
- nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
- showSidebar={this.SidebarShown}
- sidebarAddDocument={this.sidebarAddDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- />
- <div className="webBox-overlayButton-sidebar" key="sidebar" title="Toggle Sidebar"
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <div style={{ transformOrigin: 'top left', transform: `scale(${1 / scale})` }}>
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ iframe={this.isFirefox() ? this.iframeClick : undefined}
+ iframeScaling={this.isFirefox() ? this.iframeScaling : undefined}
+ anchorMenuClick={this.anchorMenuClick}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={returnOne}
+ addDocument={this.addDocumentWrapper}
+ docView={this.props.docViewPath().lastElement()}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotationsCreator}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current}
+ />{' '}
+ </div>
+ )}
+ </div>
+ <div
+ className="webBox-sideResizer"
style={{
- display: !this.props.isContentActive() ? "none" : undefined,
- top: StrCast(this.rootDoc._showTitle) === "title" ? 20 : 5,
- backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK
+ display: this.SidebarShown ? undefined : 'none',
+ width: WebBox.sidebarResizerWidth,
+ left: `calc(100% - ${this.sidebarWidth() - WebBox.sidebarResizerWidth}px)`,
}}
- onPointerDown={e => this.sidebarBtnDown(e, true)} >
- <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={"comment-alt"} size="sm" />
+ onPointerDown={e => this.sidebarBtnDown(e, false)}
+ />
+ <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ fieldKey={this.fieldKey + '-' + this._urlHash}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ setHeight={emptyFunction}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this.props.NativeDimScaling?.() || 1)}
+ showSidebar={this.SidebarShown}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ />
</div>
- {!this.props.isContentActive() ? (null) : this.searchUI}
- </div>);
+ {this.sidebarHandle}
+ {!this.props.isContentActive() ? null : this.searchUI}
+ </div>
+ );
}
}
-ScriptingGlobals.add(function urlHash(url: string) { return WebBox.urlHash(url); }); \ No newline at end of file
+ScriptingGlobals.add(function urlHash(url: string) {
+ return WebBox.urlHash(url);
+});
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index 08a5746d1..f3f1bcf5c 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -176,7 +176,7 @@ var ForeignHtmlRenderer = function (styleSheets) {
*
* @returns {Promise<String>}
*/
- const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll) {
+ const buildSvgDataUri = async function (webUrl, contentHtml, width, height, scroll, xoff) {
return new Promise(async function (resolve, reject) {
@@ -214,8 +214,8 @@ var ForeignHtmlRenderer = function (styleSheets) {
.replace(/noscript/g, "div").replace(/<div class="mediaset"><\/div>/g, "") // when scripting isn't available (ie, rendering web pages here), <noscript> tags should become <div>'s. But for Brown CS, there's a layout problem if you leave the empty <mediaset> tag
.replace(/<link[^>]*>/g, "") // don't need to keep any linked style sheets because we've already processed all style sheets above
.replace(/srcset="([^ "]*)[^"]*"/g, "src=\"$1\""); // instead of converting each item in the srcset to a data url, just convert the first one and use that
- let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml);
- const fetchedResources = await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml);
+ let urlsFoundInHtml = getImageUrlsFromFromHtml(contentHtml).filter(url => !url.startsWith("data:"));
+ const fetchedResources = webUrl ? await getMultipleResourcesAsBase64(webUrl, urlsFoundInHtml) : [];
for (let i = 0; i < fetchedResources.length; i++) {
const r = fetchedResources[i];
if (r.resourceUrl) {
@@ -240,7 +240,7 @@ var ForeignHtmlRenderer = function (styleSheets) {
// build SVG string
const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${width}' height='${height}'>
- <foreignObject x='0' y='${-scroll}' width='${width}' height='${scroll + height}'>
+ <foreignObject x='${-xoff}' y='${-scroll}' width='${xoff + width}' height='${scroll + height}'>
${contentRootElemString}
</foreignObject>
</svg>`;
@@ -259,11 +259,11 @@ var ForeignHtmlRenderer = function (styleSheets) {
*
* @return {Promise<Image>}
*/
- this.renderToImage = async function (webUrl, html, width, height, scroll) {
+ this.renderToImage = async function (webUrl, html, width, height, scroll, xoff) {
return new Promise(async function (resolve, reject) {
const img = new Image();
console.log("BUILDING SVG for:" + webUrl);
- img.src = await buildSvgDataUri(webUrl, html, width, height, scroll);
+ img.src = await buildSvgDataUri(webUrl, html, width, height, scroll, xoff);
img.onload = function () {
console.log("IMAGE SVG created:" + webUrl);
@@ -279,16 +279,16 @@ var ForeignHtmlRenderer = function (styleSheets) {
*
* @return {Promise<Image>}
*/
- this.renderToCanvas = async function (webUrl, html, width, height, scroll) {
+ this.renderToCanvas = async function (webUrl, html, width, height, scroll, xoff, oversample) {
return new Promise(async function (resolve, reject) {
- const img = await self.renderToImage(webUrl, html, width, height, scroll);
+ const img = await self.renderToImage(webUrl, html, width, height, scroll, xoff);
const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
+ canvas.width = img.width * oversample;
+ canvas.height = img.height * oversample;
const canvasCtx = canvas.getContext('2d');
- canvasCtx.drawImage(img, 0, 0, img.width, img.height);
+ canvasCtx.drawImage(img, 0, 0, img.width * oversample, img.height * oversample);
resolve(canvas);
});
@@ -301,9 +301,9 @@ var ForeignHtmlRenderer = function (styleSheets) {
*
* @return {Promise<String>}
*/
- this.renderToBase64Png = async function (webUrl, html, width, height, scroll) {
+ this.renderToBase64Png = async function (webUrl, html, width, height, scroll, xoff, oversample) {
return new Promise(async function (resolve, reject) {
- const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll);
+ const canvas = await self.renderToCanvas(webUrl, html, width, height, scroll, xoff, oversample);
resolve(canvas.toDataURL('image/png'));
});
};
@@ -311,8 +311,8 @@ var ForeignHtmlRenderer = function (styleSheets) {
};
-export function CreateImage(webUrl, styleSheets, html, width, height, scroll) {
- const val = (new ForeignHtmlRenderer(styleSheets)).renderToBase64Png(webUrl, html.replace(/\n/g, "").replace(/<script((?!\/script).)*<\/script>/g, ""), width, height, scroll);
+export function CreateImage(webUrl, styleSheets, html, width, height, scroll, xoff = 0, oversample = 1) {
+ const val = (new ForeignHtmlRenderer(styleSheets)).renderToBase64Png(webUrl, html.replace(/docView-hack/g, 'documentView-hack').replace(/\n/g, "").replace(/<script((?!\/script).)*<\/script>/g, ""), width, height, scroll, xoff, oversample);
return val;
}
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
index f3731b8f9..b4a382faf 100644
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ b/src/client/views/nodes/button/ButtonScripts.ts
@@ -1,5 +1,6 @@
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) {
@@ -8,7 +9,8 @@ ScriptingGlobals.add(function changeView(view: string) {
});
// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay() {
+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/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
index cf86b5e07..3b5aac221 100644
--- a/src/client/views/nodes/button/FontIconBadge.tsx
+++ b/src/client/views/nodes/button/FontIconBadge.tsx
@@ -7,30 +7,30 @@ import { DragManager } from "../../../util/DragManager";
import "./FontIconBadge.scss";
interface FontIconBadgeProps {
- collection: Doc | undefined;
+ value: string | undefined;
}
@observer
export class FontIconBadge extends React.Component<FontIconBadgeProps> {
_notifsRef = React.createRef<HTMLDivElement>();
- onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e,
- (e: PointerEvent) => {
- const dragData = new DragManager.DocumentDragData([this.props.collection!]);
- DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
- return true;
- },
- returnFalse, emptyFunction, false);
- }
+ // onPointerDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(this, e,
+ // (e: PointerEvent) => {
+ // const dragData = new DragManager.DocumentDragData([this.props.collection!]);
+ // DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
+ // return true;
+ // },
+ // returnFalse, emptyFunction, false);
+ // }
render() {
- if (!(this.props.collection instanceof Doc)) return (null);
- const length = DocListCast(this.props.collection.data).filter(d => GetEffectiveAcl(d) !== AclPrivate).length; // Object.keys(d).length).length; // filter out any documents that we can't read
+ if (this.props.value === undefined) return (null);
return <div className="fontIconBadge-container" ref={this._notifsRef}>
- <div className="fontIconBadge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
- onPointerDown={this.onPointerDown} >
- {length}
+ <div className="fontIconBadge" style={{ "display": "initial" }}
+ // onPointerDown={this.onPointerDown}
+ >
+ {this.props.value}
</div>
</div>;
}
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index 079c767b9..a1ca777b3 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -1,4 +1,4 @@
-@import "../../global/globalCssVariables";
+@import '../../global/globalCssVariables';
.menuButton {
height: 100%;
@@ -18,12 +18,12 @@
.fontIconBox-label {
color: $white;
- position: relative;
+ bottom: 0;
+ position: absolute;
text-align: center;
font-size: 7px;
letter-spacing: normal;
background-color: inherit;
- margin-top: 5px;
border-radius: 8px;
padding: 0;
width: 100%;
@@ -31,8 +31,6 @@
text-transform: uppercase;
font-weight: bold;
transition: 0.15s;
-
-
}
.fontIconBox-icon {
@@ -40,8 +38,10 @@
height: 80%;
}
- &.clickBtn {
+ &.clickBtn,
+ &.clickBtnLabel {
cursor: pointer;
+ flex-direction: column;
&:hover {
background-color: rgba(0, 0, 0, 0.3) !important;
@@ -53,6 +53,12 @@
}
}
+ &.clickBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.textBtn {
display: grid;
/* grid-row: auto; */
@@ -64,12 +70,14 @@
justify-items: center;
&:hover {
- filter:brightness(0.85) !important;
+ filter: brightness(0.85) !important;
}
}
- &.tglBtn {
+ &.tglBtn,
+ &.tglBtnLabel {
cursor: pointer;
+ flex-direction: column;
&.switch {
//TOGGLE
@@ -96,31 +104,31 @@
right: 0;
bottom: 0;
background-color: lightgrey;
- -webkit-transition: .4s;
- transition: .4s;
+ -webkit-transition: 0.4s;
+ transition: 0.4s;
}
.slider:before {
position: absolute;
- content: "";
+ content: '';
height: 21px;
width: 21px;
left: 2px;
bottom: 2px;
background-color: $white;
- -webkit-transition: .4s;
- transition: .4s;
+ -webkit-transition: 0.4s;
+ transition: 0.4s;
}
- input:checked+.slider {
+ input:checked + .slider {
background-color: $medium-blue;
}
- input:focus+.slider {
+ input:focus + .slider {
box-shadow: 0 0 1px $medium-blue;
}
- input:checked+.slider:before {
+ input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
@@ -146,10 +154,19 @@
}
}
- &.toolBtn {
+ &.tglBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
+ &.toolBtn,
+ &.toolBtnLabel {
cursor: pointer;
width: 100%;
border-radius: 100%;
+ flex-direction: column;
+ margin-top: -4px;
svg {
width: 60% !important;
@@ -157,6 +174,12 @@
}
}
+ &.toolBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.menuBtn {
cursor: pointer !important;
border-radius: 0px;
@@ -166,15 +189,14 @@
width: 45% !important;
height: 45%;
}
-
- &:hover{
+
+ &:hover {
filter: brightness(0.85);
}
}
-
-
- &.colorBtn {
+ &.colorBtn,
+ &.colorBtnLabel {
color: black;
cursor: pointer;
flex-direction: column;
@@ -204,6 +226,12 @@
}
}
+ &.colorBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.drpdownList {
width: 100%;
display: grid;
@@ -278,6 +306,26 @@
background-color: #e3e3e3;
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
border-radius: $standard-border-radius;
+
+ .menu-slider {
+ height: 10px;
+ }
+ input[type='range']::-webkit-slider-runnable-track {
+ background: gray;
+ height: 3px;
+ }
+
+ input[type='range']::-webkit-slider-thumb {
+ box-shadow: 1px 1px 1px #000000;
+ border: 1px solid #000000;
+ height: 10px;
+ width: 10px;
+ border-radius: 5px;
+ background: #ffffff;
+ cursor: pointer;
+ -webkit-appearance: none;
+ margin-top: -4px;
+ }
}
}
@@ -407,5 +455,4 @@
background: transparent;
position: fixed;
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index ca13590de..78ef85ec2 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -5,52 +5,49 @@ import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ColorState, SketchPicker } from 'react-color';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, HeightSym, StrListCast, WidthSym } from '../../../../fields/Doc';
import { InkTool } from '../../../../fields/InkField';
-import { createSchema } from '../../../../fields/Schema';
import { ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { WebField } from '../../../../fields/URLField';
-import { Utils } from '../../../../Utils';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
+import { aggregateBounds, Utils } from '../../../../Utils';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SelectionManager } from '../../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
-import { CollectionViewType } from '../../collections/CollectionView';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { ContextMenu } from '../../ContextMenu';
import { DocComponent } from '../../DocComponent';
import { EditableView } from '../../EditableView';
import { GestureOverlay } from '../../GestureOverlay';
import { Colors } from '../../global/globalEnums';
import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
+import { InkTranscription } from '../../InkTranscription';
import { StyleProp } from '../../StyleProvider';
import { FieldView, FieldViewProps } from '.././FieldView';
import { RichTextMenu } from '../formattedText/RichTextMenu';
import { WebBox } from '../WebBox';
import { FontIconBadge } from './FontIconBadge';
import './FontIconBox.scss';
-const FontIconSchema = createSchema({
- icon: "string",
-});
export enum ButtonType {
- TextButton = "textBtn",
- MenuButton = "menuBtn",
- DropdownList = "drpdownList",
- DropdownButton = "drpdownBtn",
- ClickButton = "clickBtn",
- DoubleButton = "dblBtn",
- ToggleButton = "tglBtn",
- ColorButton = "colorBtn",
- ToolButton = "toolBtn",
- NumberButton = "numBtn",
- EditableText = "editableText"
+ TextButton = 'textBtn',
+ MenuButton = 'menuBtn',
+ DropdownList = 'drpdownList',
+ DropdownButton = 'drpdownBtn',
+ ClickButton = 'clickBtn',
+ DoubleButton = 'dblBtn',
+ ToggleButton = 'tglBtn',
+ ColorButton = 'colorBtn',
+ ToolButton = 'toolBtn',
+ NumberButton = 'numBtn',
+ EditableText = 'editableText',
}
export enum NumButtonType {
- Slider = "slider",
- DropdownOptions = "list",
- Inline = "inline"
+ Slider = 'slider',
+ DropdownOptions = 'list',
+ Inline = 'inline',
}
export interface ButtonProps extends FieldViewProps {
@@ -58,29 +55,52 @@ export interface ButtonProps extends FieldViewProps {
}
@observer
export class FontIconBox extends DocComponent<ButtonProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(FontIconBox, fieldKey);
+ }
showTemplate = (): void => {
const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
- dragFactory && this.props.addDocTab(dragFactory, "add:right");
- }
- dragAsTemplate = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)'); };
- useAsPrototype = (): void => { this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)'); };
+ dragFactory && this.props.addDocTab(dragFactory, 'add:right');
+ };
+ dragAsTemplate = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ };
+ useAsPrototype = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
+ };
specificContextMenu = (): void => {
- if (!Doc.UserDoc().noviceMode) {
+ if (!Doc.noviceMode) {
const cm = ContextMenu.Instance;
- cm.addItem({ description: "Show Template", event: this.showTemplate, icon: "tag" });
- cm.addItem({ description: "Use as Render Template", event: this.dragAsTemplate, icon: "tag" });
- cm.addItem({ description: "Use as Prototype", event: this.useAsPrototype, icon: "tag" });
+ cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
+ cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
+ cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
}
+ };
+
+ static GetShowLabels() {
+ return BoolCast(Doc.UserDoc()._showLabel);
+ }
+ static SetShowLabels(show: boolean) {
+ Doc.UserDoc()._showLabel = show;
}
// Determining UI Specs
- @observable private label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
- @observable private icon = StrCast(this.dataDoc.icon, "user") as any;
- @observable private dropdown: boolean = BoolCast(this.rootDoc.dropDownOpen);
- @observable private buttonList: string[] = StrListCast(this.rootDoc.btnList);
- @observable private type = StrCast(this.rootDoc.btnType);
+ @computed get label() {
+ return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
+ }
+ @computed get icon() {
+ return StrCast(this.dataDoc.icon, 'user') as any;
+ }
+ @computed get dropdown() {
+ return BoolCast(this.rootDoc.dropDownOpen);
+ }
+ @computed get buttonList() {
+ return StrListCast(this.rootDoc.btnList);
+ }
+ @computed get type() {
+ return StrCast(this.rootDoc.btnType);
+ }
/**
* Types of buttons in dash:
@@ -89,11 +109,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
* - Expandable button (CollectionLinearView)
* - Button inside of CollectionLinearView vs. outside of CollectionLinearView
* - Action button
- * - Dropdown button
+ * - Dropdown button
* - Color button
* - Dropdown list
* - Number button
- **/
+ **/
_batch: UndoManager.Batch | undefined = undefined;
/**
@@ -107,25 +127,32 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// Script for checking the outcome of the toggle
const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
+ 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")}
+ 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)); }}
+ onChange={e => {
+ e.stopPropagation();
+ setValue(Number(e.target.value));
+ }}
/>
- </div>;
+ </div>
+ );
return (
- <div
- className={`menuButton ${this.type} ${numBtnType}`}
- onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}
- >
+ <div className={`menuButton ${this.type} ${numBtnType}`} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
{checkResult}
+ {label}
{this.rootDoc.dropDownOpen ? dropdown : null}
</div>
);
@@ -136,62 +163,56 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
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>;
+ 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 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) => {
+ 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={action((e) => setValue(Number(e.target.value)))}
- />
+ onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={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 className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
</div>
- {this.rootDoc.dropDownOpen ?
+ {this.rootDoc.dropDownOpen ? (
<div>
- <div className="menuButton-dropdownList"
- style={{ left: "25%" }}>
+ <div className="menuButton-dropdownList" style={{ left: '25%' }}>
{list}
</div>
- <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
- </div> : null}
-
+ <div
+ className="dropbox-background"
+ onClick={e => {
+ e.stopPropagation();
+ this.rootDoc.dropDownOpen = false;
+ }}
+ />
+ </div>
+ ) : null}
</div>
);
} else {
- return (
- <div>
-
- </div>
- );
+ return <div></div>;
}
-
-
}
/**
@@ -202,20 +223,21 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
return (
- <div className={`menuButton ${this.type} ${active}`}
+ <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))}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
- <div
- className="menuButton-dropdown"
- style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
+ {!this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
+ {' '}
+ {this.label}{' '}
+ </div>
+ )}
+ <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
<FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
</div>
- {this.rootDoc.dropDownOpen ?
- <div className="menuButton-dropdownBox">
- {/* DROPDOWN BOX CONTENTS */}
- </div> : null}
+ {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null}
</div>
);
}
@@ -229,11 +251,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const script = ScriptCast(this.rootDoc.script);
+ if (!script) {
+ return null;
+ }
let noviceList: string[] = [];
let text: string | undefined;
let dropdown = true;
- let icon: IconProp = "caret-down";
+ let icon: IconProp = 'caret-down';
try {
if (script.script.originalScript.startsWith('setView')) {
const selected = SelectionManager.Docs().lastElement();
@@ -242,125 +267,141 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
text = StrCast(selected._viewType);
} else {
dropdown = false;
- text = selected.type === DocumentType.RTF ? "Text" : StrCast(selected.type);
+ text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type);
icon = Doc.toIcon(selected);
}
} else {
dropdown = false;
- icon = "globe-asia";
- text = "User Default";
+ icon = 'globe-asia';
+ text = 'User Default';
}
noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
} 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"];
+ noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
}
} catch (e) {
console.log(e);
}
// Get items to place into the list
- const list = this.buttonList.map((value) => {
- if (Doc.UserDoc().noviceMode && !noviceList.includes(value)) {
+ const list = this.buttonList.map(value => {
+ if (Doc.noviceMode && !noviceList.includes(value)) {
return;
}
- return <div className="list-item" key={`${value}`}
- style={{
- fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined,
- backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined
- }}
- onClick={() => script.script.run({ value }).result}>
- {value[0].toUpperCase() + value.slice(1)}
- </div>;
+ return (
+ <div
+ className="list-item"
+ key={`${value}`}
+ style={{
+ fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined,
+ backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
+ }}
+ onClick={() => script.script.run({ value }).result}>
+ {value[0].toUpperCase() + value.slice(1)}
+ </div>
+ );
});
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
- {this.label}
- </div>;
+ const label =
+ !this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ bottom: 0, position: 'absolute', color: color, backgroundColor: backgroundColor }}>
+ {this.label}
+ </div>
+ );
return (
- <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}>
- {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>
+ <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}>
+ {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}
- {!dropdown ? (null) : <div className="menuButton-dropDown">
- <FontAwesomeIcon icon={icon} color={color} size="sm" />
- </div>}
- {this.rootDoc.dropDownOpen ?
+ {!dropdown ? null : (
+ <div className="menuButton-dropDown">
+ <FontAwesomeIcon icon={icon} color={color} size="sm" />
+ </div>
+ )}
+ {this.rootDoc.dropDownOpen ? (
<div>
- <div className="menuButton-dropdownList"
- style={{ left: 0 }}>
+ <div className="menuButton-dropdownList" style={{ left: 0 }}>
{list}
</div>
- <div className="dropbox-background" onClick={(e) => { e.stopPropagation(); this.rootDoc.dropDownOpen = false; }} />
+ <div
+ className="dropbox-background"
+ onClick={e => {
+ e.stopPropagation();
+ this.rootDoc.dropDownOpen = false;
+ }}
+ />
</div>
- : null}
+ ) : null}
</div>
);
}
@observable colorPickerClosed: boolean = true;
- @computed get colorScript() { return ScriptCast(this.rootDoc.script); }
+ @computed get colorScript() {
+ return ScriptCast(this.rootDoc.script);
+ }
colorPicker = (curColor: string) => {
const change = (value: ColorState) => {
const s = this.colorScript;
s && undoBatch(() => s.script.run({ 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}
- color={curColor}
- presetColors={presets} />;
- }
+ const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent'];
+ return <SketchPicker onChange={change} color={curColor} presetColors={presets} />;
+ };
/**
* Color button
*/
@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 label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}>
- {this.label}
- </div>;
-
- const dropdownCaret = <div
- className="menuButton-dropDown"
- style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
- <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
- </div>;
+ const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? 'transparent';
+
+ const label =
+ !this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color, backgroundColor }}>
+ {this.label}
+ </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} ${this.colorPickerClosed}`}
+ <div
+ className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)}
+ onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
onPointerDown={e => e.stopPropagation()}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
<div className="colorButton-color" style={{ backgroundColor: curColor }} />
{label}
{/* {dropdownCaret} */}
- {this.colorPickerClosed ? (null) :
+ {this.colorPickerClosed ? null : (
<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()}>
{this.colorPicker(curColor)}
</div>
- <div className="dropbox-background" onClick={action((e) => {
- e.stopPropagation(); this.colorPickerClosed = true;
- })} />
- </div>}
+ <div
+ className="dropbox-background"
+ onPointerDown={action(e => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.colorPickerClosed = true;
+ })}
+ />
+ </div>
+ )}
</div>
);
}
@@ -374,27 +415,26 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
// Button label
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}>
- {this.label}
- </div>;
+ const label =
+ !this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color, backgroundColor }}>
+ {this.label}
+ </div>
+ );
if (switchToggle) {
return (
<div className={`menuButton ${this.type} ${'switch'}`}>
{buttonText ? buttonText : null}
<label className="switch">
- <input type="checkbox"
- checked={backgroundColor === Colors.MEDIUM_BLUE}
- />
- <span className="slider round"></span>
+ <input type="checkbox" checked={backgroundColor === Colors.MEDIUM_BLUE} />
+ <span className="slider round" />
</label>
</div>
);
} else {
return (
- <div className={`menuButton ${this.type}`}
- style={{ opacity: 1, backgroundColor, color }}>
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
@@ -402,8 +442,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
}
-
-
/**
* Default
*/
@@ -412,11 +450,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const active: string = StrCast(this.rootDoc.dropDownOpen);
return (
- <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu}
- style={{ backgroundColor: "transparent", borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
+ <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
<div className="menuButton-wrap">
- <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={"black"} size={"sm"} />
- {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
+ <FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={'black'} size={'sm'} />
+ {!this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
+ {' '}
+ {this.label}{' '}
+ </div>
+ )}
</div>
</div>
);
@@ -426,14 +468,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// Script for running the toggle
const script = ScriptCast(this.rootDoc.script);
// Function to run the script
- const checkResult = script?.script.run({ value: "", _readOnly_: true }).result;
+ const checkResult = script?.script.run({ value: '', _readOnly_: true }).result;
const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ value, _readOnly_: false }).result;
return (
<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} />
+ <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} />
</div>
</div>
);
@@ -442,31 +484,31 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
render() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color, backgroundColor, position: "absolute" }}>
- {this.label}
- </div>;
+ const label =
+ !this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color, backgroundColor }}>
+ {this.label}
+ </div>
+ );
- const menuLabel = !this.label || !Doc.UserDoc()._showMenuLabel ? (null) :
- <div className="fontIconBox-label" style={{ color, backgroundColor: "transparent" }}>
- {this.label}
- </div>;
+ const menuLabel =
+ !this.label || !FontIconBox.GetShowLabels() ? null : (
+ <div className="fontIconBox-label" style={{ color, backgroundColor: 'transparent' }}>
+ {this.label}
+ </div>
+ );
const buttonText = StrCast(this.rootDoc.buttonText);
// TODO:glr Add label of button type
- let button = this.defaultButton;
+ let button: JSX.Element | null = this.defaultButton;
switch (this.type) {
case ButtonType.TextButton:
button = (
<div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- {buttonText ?
- <div className="button-text">
- {buttonText}
- </div>
- : null}
+ {buttonText ? <div className="button-text">{buttonText}</div> : null}
{label}
</div>
);
@@ -489,7 +531,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
case ButtonType.ToolButton:
button = (
- <div className={`menuButton ${this.type}`} style={{ opacity: 1, backgroundColor, color }}>
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
@@ -501,49 +543,47 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
case ButtonType.ClickButton:
button = (
- <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1 }}>
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ color, backgroundColor, opacity: 1 }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
);
break;
case ButtonType.MenuButton:
- const trailsIcon = <img src={`/assets/${"presTrails.png"}`}
- style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? "0%" : "100%"})` }} />;
+ const trailsIcon = <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
button = (
<div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
- {this.icon === "pres-trail" ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />}
+ {this.icon === 'pres-trail' ? trailsIcon : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />}
{menuLabel}
- <FontIconBadge collection={Cast(this.rootDoc.watchedDocuments, Doc, null)} />
- </div >
+ <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
+ </div>
);
break;
default:
break;
}
- return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? button :
- <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>
- {button}
- </Tooltip>;
+ return !this.layoutDoc.toolTip || this.type === ButtonType.DropdownList || this.type === ButtonType.ColorButton || this.type === ButtonType.NumberButton || this.type === ButtonType.EditableText ? (
+ button
+ ) : button !== null ? (
+ <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>
+ ) : null;
}
}
-
// 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._viewType = 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.Docs().lastElement();
+ const selected = SelectionManager.Views().lastElement();
if (checkResult) {
- return selected?._backgroundColor ?? "transparent";
+ return selected?.props.Document._backgroundColor ?? 'transparent';
}
- if (selected) selected._backgroundColor = color;
+ if (selected) selected.props.Document._backgroundColor = color;
});
// toggle: Set overlay status of selected document
@@ -553,17 +593,17 @@ 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().showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().showTitle, 'creationDate');
});
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (checkResult && selected) {
- if (NumCast(selected.Document.z) >= 1) return Colors.MEDIUM_BLUE;
- return "transparent";
+ if (checkResult) {
+ if (NumCast(selected?.Document.z) >= 1) return Colors.MEDIUM_BLUE;
+ return 'transparent';
}
- selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("[FontIconBox.tsx] toggleOverlay failed");
+ selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
});
/** TEXT
@@ -580,7 +620,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
- SelectionManager.Docs().map(doc => doc._fontFamily = font);
+ SelectionManager.Docs().map(doc => (doc._fontFamily = font));
const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
@@ -589,38 +629,38 @@ ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
else Doc.UserDoc().fontFamily = font;
});
-ScriptingGlobals.add(function getActiveTextInfo(info: "family" | "size" | "color" | "highlight") {
+ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') {
const editorView = RichTextMenu.Instance.TextView?.EditorView;
const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
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];
+ case 'family':
+ return style?.activeFamilies[0];
+ case 'size':
+ return style?.activeSizes[0];
+ case 'color':
+ return style?.activeColors[0];
+ case 'highlight':
+ return style?.activeHighlights[0];
}
});
-ScriptingGlobals.add(function setAlignment(align: "left" | "right" | "center", checkResult?: boolean) {
+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 (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent';
}
if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align);
else Doc.UserDoc().textAlign = align;
});
-ScriptingGlobals.add(function setBulletList(mapStyle: "bullet" | "decimal", checkResult?: boolean) {
+ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', 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";
- }
- if (editorView) {
- const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
- editorView?.state && RichTextMenu.Instance.changeListType(
- editorView.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? "" : mapStyle }));
+ return 'transparent';
}
+ editorView?.state && RichTextMenu.Instance.changeListType(mapStyle);
});
// toggle: Set overlay status of selected document
@@ -656,72 +696,160 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return (editorView ? RichTextMenu.Instance.fontSize : StrCast(Doc.UserDoc().fontSize, "10px")).replace("px", "");
+ return RichTextMenu.Instance.fontSize.replace('px', '');
}
- if (typeof size === "number") size = size.toString();
- if (size && Number(size).toString() === size) size += "px";
+ if (typeof size === 'number') size = size.toString();
+ if (size && Number(size).toString() === size) size += 'px';
if (editorView) RichTextMenu.Instance.setFontSize(size);
else Doc.UserDoc()._fontSize = 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 toggleBold(checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === "bold") ? Colors.MEDIUM_BLUE : "transparent";
+ 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";
+ 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";
+ 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";
+ 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";
+ 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";
+ else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics';
});
+export function checkInksToGroup() {
+ // console.log("getting here to inks group");
+ if (Doc.ActiveTool === InkTool.Write) {
+ CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
+ // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
+ // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other
+ const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => {
+ // console.log(inkDoc.x, inkDoc.y);
+ });
+ });
+ }
+}
+
+export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
+ // TODO nda - if document being added to is a inkGrouping then we can just add to that group
+ if (Doc.ActiveTool === InkTool.Write) {
+ CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
+ // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
+ const selected = ffView.unprocessedDocs;
+ // loop through selected an get the bound
+ const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
+
+ selected.map(
+ action(d => {
+ const x = NumCast(d.x);
+ const y = NumCast(d.y);
+ const width = d[WidthSym]();
+ const height = d[HeightSym]();
+ bounds.push({ x, y, width, height });
+ })
+ );
+
+ const aggregBounds = aggregateBounds(bounds, 0, 0);
+ const marqViewRef = ffView._marqueeViewRef.current;
+
+ // set the vals for bounds in marqueeView
+ if (marqViewRef) {
+ marqViewRef._downX = aggregBounds.x;
+ marqViewRef._downY = aggregBounds.y;
+ marqViewRef._lastX = aggregBounds.r;
+ marqViewRef._lastY = aggregBounds.b;
+ }
+
+ selected.map(
+ action(d => {
+ const dx = NumCast(d.x);
+ const dy = NumCast(d.y);
+ delete d.x;
+ delete d.y;
+ delete d.activeFrame;
+ delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ // calculate pos based on bounds
+ if (marqViewRef?.Bounds) {
+ d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
+ d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
+ }
+ return d;
+ })
+ );
+ ffView.props.removeDocument?.(selected);
+ // TODO: nda - this is the code to actually get a new grouped collection
+ const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+ if (newCollection) {
+ newCollection.height = newCollection[HeightSym]();
+ newCollection.width = newCollection[WidthSym]();
+ }
+ // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
+ newCollection && ffView.props.addDocument?.(newCollection);
+ // TODO: nda - will probably need to go through and only remove the unprocessed selected docs
+ ffView.unprocessedDocs = [];
+ InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ });
+ }
+ CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
+}
/** INK
- * setActiveInkTool
+ * setActiveTool
* setStrokeWidth
* setStrokeColor
**/
-ScriptingGlobals.add(function setActiveInkTool(tool: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) {
+ InkTranscription.Instance?.createInkGroup();
if (checkResult) {
- return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ?
- Colors.MEDIUM_BLUE : "transparent";
+ return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent';
}
- if (["circle", "square", "line"].includes(tool)) {
+ if (['circle', 'square', 'line'].includes(tool)) {
if (GestureOverlay.Instance.InkShape === tool) {
- Doc.UserDoc().activeInkTool = InkTool.None;
+ Doc.ActiveTool = InkTool.None;
GestureOverlay.Instance.InkShape = InkTool.None;
} else {
- Doc.UserDoc().activeInkTool = InkTool.Pen;
+ Doc.ActiveTool = InkTool.Pen;
GestureOverlay.Instance.InkShape = tool;
}
- } else if (tool) { // pen or eraser
- if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) {
- Doc.UserDoc().activeInkTool = InkTool.None;
+ } else if (tool) {
+ // pen or eraser
+ if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+ Doc.ActiveTool = InkTool.None;
+ } else if (tool == InkTool.Write) {
+ // console.log("write mode selected - create groupDoc here!", tool)
+ Doc.ActiveTool = tool;
+ GestureOverlay.Instance.InkShape = '';
} else {
- Doc.UserDoc().activeInkTool = tool;
- GestureOverlay.Instance.InkShape = "";
+ Doc.ActiveTool = tool as any;
+ GestureOverlay.Instance.InkShape = '';
}
} else {
- Doc.UserDoc().activeInkTool = InkTool.None;
+ Doc.ActiveTool = InkTool.None;
}
});
@@ -735,7 +863,9 @@ ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean
return ActiveFillColor();
}
SetActiveFillColor(StrCast(color));
- SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.fillColor = color);
+ SelectionManager.Docs()
+ .filter(doc => doc.type === DocumentType.INK)
+ .map(doc => (doc.fillColor = color));
});
ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) {
@@ -747,7 +877,9 @@ ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolea
return ActiveInkWidth();
}
SetActiveInkWidth(width.toString());
- SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.strokeWidth = Number(width));
+ SelectionManager.Docs()
+ .filter(doc => doc.type === DocumentType.INK)
+ .map(doc => (doc.strokeWidth = Number(width)));
});
// toggle: Set overlay status of selected document
@@ -760,10 +892,11 @@ ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boole
return ActiveInkColor();
}
SetActiveInkColor(StrCast(color));
- SelectionManager.Docs().filter(doc => doc.type === DocumentType.INK).map(doc => doc.color = String(color));
+ SelectionManager.Docs()
+ .filter(doc => doc.type === DocumentType.INK)
+ .map(doc => (doc.color = String(color)));
});
-
/** WEB
* webSetURL
**/
@@ -778,21 +911,20 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
}
});
ScriptingGlobals.add(function webForward(checkResult?: boolean) {
- const selected = (SelectionManager.Views().lastElement()?.ComponentView as WebBox);
+ const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
if (checkResult) {
- return selected?.forward(checkResult) ? undefined : "lightGray";
+ return selected?.forward(checkResult) ? undefined : 'lightGray';
}
selected?.forward();
});
ScriptingGlobals.add(function webBack(checkResult?: boolean) {
- const selected = (SelectionManager.Views().lastElement()?.ComponentView as WebBox);
+ const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
if (checkResult) {
- return selected?.back(checkResult) ? undefined : "lightGray";
+ return selected?.back(checkResult) ? undefined : 'lightGray';
}
selected?.back();
});
-
/** Schema
* toggleSchemaPreview
**/
@@ -801,13 +933,12 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
if (checkResult && selected) {
const result: boolean = NumCast(selected.schemaPreviewWidth) > 0;
if (result) return Colors.MEDIUM_BLUE;
- else return "transparent";
- }
- else if (selected) {
+ else return 'transparent';
+ } else if (selected) {
if (NumCast(selected.schemaPreviewWidth) > 0) {
- selected.schemaPreviewWidth = 200;
- } else {
selected.schemaPreviewWidth = 0;
+ } else {
+ selected.schemaPreviewWidth = 200;
}
}
});
@@ -816,11 +947,11 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
* groupBy
*/
ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
- SelectionManager.Docs().map(doc => doc._fontFamily = key);
+ SelectionManager.Docs().map(doc => (doc._fontFamily = key));
const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
}
if (editorView) RichTextMenu.Instance.setFontFamily(key);
else Doc.UserDoc().fontFamily = key;
-}); \ No newline at end of file
+});
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
index 235495250..7f414ddbb 100644
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
@@ -5,6 +5,7 @@ import { IButtonProps } from '../ButtonInterface';
import { ColorState, SketchPicker } from 'react-color';
import { ScriptField } from '../../../../../fields/ScriptField';
import { Doc } from '../../../../../fields/Doc';
+import { FontIconBox } from '../FontIconBox';
export class ColorDropdown extends Component<IButtonProps> {
render() {
@@ -31,7 +32,7 @@ export class ColorDropdown extends Component<IButtonProps> {
disableAlpha={!stroke}
onChange={func} color={boolResult ? boolResult : "#FFFFFF"}
presetColors={colorOptions} />;
- const label = !this.props.label || !Doc.UserDoc()._showLabel ? (null) :
+ 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>;
diff --git a/src/client/views/nodes/button/textButton/TextButton.tsx b/src/client/views/nodes/button/textButton/TextButton.tsx
index e18590a95..5d7d55863 100644
--- a/src/client/views/nodes/button/textButton/TextButton.tsx
+++ b/src/client/views/nodes/button/textButton/TextButton.tsx
@@ -9,9 +9,22 @@ export class TextButton extends Component<IButtonProps> {
// Determine the type of toggle button
const buttonText: boolean = BoolCast(this.props.rootDoc.switchToggle);
- return (<div className={`menuButton ${this.props.type}`} style={{ opacity: 1, backgroundColor: this.props.backgroundColor, color: this.props.color }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} />
- {this.props.label}
- </div>);
+ return (
+ <div
+ className={`menuButton ${this.props.type}`}
+ style={{
+ opacity: 1,
+ backgroundColor: this.props.backgroundColor,
+ color: this.props.color,
+ }}
+ >
+ <FontAwesomeIcon
+ className={`fontIconBox-icon-${this.props.type}`}
+ icon={this.props.icon}
+ color={this.props.color}
+ />
+ {this.props.label}
+ </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 5c75a589a..40dd6fbc7 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -1,37 +1,44 @@
-import { TextSelection } from "prosemirror-state";
+import { TextSelection } from 'prosemirror-state';
import * as ReactDOM from 'react-dom';
-import { Doc } from "../../../../fields/Doc";
-import { DocServer } from "../../../DocServer";
-import React = require("react");
-
+import { Doc } from '../../../../fields/Doc';
+import { DocServer } from '../../../DocServer';
+import React = require('react');
// creates an inline comment in a note when '>>' is typed.
// the comment sits on the right side of the note and vertically aligns with its anchor in the text.
// the comment can be toggled on/off with the '<-' text anchor.
export class DashDocCommentView {
- _fieldWrapper: HTMLDivElement; // container for label and value
+ dom: HTMLDivElement; // container for label and value
constructor(node: any, view: any, getPos: any) {
- this._fieldWrapper = document.createElement("div");
- this._fieldWrapper.style.width = node.attrs.width;
- this._fieldWrapper.style.height = node.attrs.height;
- this._fieldWrapper.style.fontWeight = "bold";
- this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.display = "inline-block";
- this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
-
- ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this._fieldWrapper);
- (this as any).dom = this._fieldWrapper;
+ this.dom = document.createElement('div');
+ this.dom.style.width = node.attrs.width;
+ this.dom.style.height = node.attrs.height;
+ this.dom.style.fontWeight = 'bold';
+ this.dom.style.position = 'relative';
+ this.dom.style.display = 'inline-block';
+ this.dom.onkeypress = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeydown = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeyup = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onmousedown = function (e: any) {
+ e.stopPropagation();
+ };
+
+ ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this.dom);
+ (this as any).dom = this.dom;
}
destroy() {
- ReactDOM.unmountComponentAtNode(this._fieldWrapper);
+ ReactDOM.unmountComponentAtNode(this.dom);
}
- selectNode() { }
+ selectNode() {}
}
interface IDashDocCommentViewInternal {
@@ -40,8 +47,7 @@ interface IDashDocCommentViewInternal {
getPos: any;
}
-export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal>{
-
+export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> {
constructor(props: IDashDocCommentViewInternal) {
super(props);
this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this);
@@ -71,7 +77,9 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
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));
- 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) { }
+ 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) {}
}, 0);
}
e.stopPropagation();
@@ -81,32 +89,35 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
e.stopPropagation();
}
- targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ targetNode = () => {
+ // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
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) {
- return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ 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 { this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0);
+ setTimeout(() => {
+ try {
+ this.props.view.dispatch(state.tr.setSelection(TextSelection.create(state.tr.doc, this.props.getPos() + 2)));
+ } catch (e) {}
+ }, 0);
return undefined;
- }
+ };
render() {
return (
<span
className="formattedTextBox-inlineComment"
- id={"DashDocCommentView-" + this.props.docid}
+ id={'DashDocCommentView-' + this.props.docid}
onPointerLeave={this.onPointerLeaveCollapsed}
onPointerEnter={this.onPointerEnterCollapsed}
onPointerUp={this.onPointerUpCollapsed}
- onPointerDown={this.onPointerDownCollapsed}
- >
- </span>
+ onPointerDown={this.onPointerDownCollapsed}></span>
);
}
}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 364be461f..73a711b9d 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -1,52 +1,53 @@
-import { IReactionDisposer, reaction, observable, action } from "mobx";
-import { NodeSelection } from "prosemirror-state";
-import { Doc, HeightSym, WidthSym } from "../../../../fields/Doc";
-import { Cast, StrCast, NumCast } from "../../../../fields/Types";
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, Utils, returnTransparent } from "../../../../Utils";
-import { DocServer } from "../../../DocServer";
-import { Docs, DocUtils } from "../../../documents/Documents";
-import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
-import { Transform } from "../../../util/Transform";
-import { DocumentView } from "../DocumentView";
-import { FormattedTextBox } from "./FormattedTextBox";
-import React = require("react");
+import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { NodeSelection } from 'prosemirror-state';
import * as ReactDOM from 'react-dom';
-import { observer } from "mobx-react";
-import { ColorScheme } from "../../../util/SettingsManager";
+import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnFalse, Utils } from '../../../../Utils';
+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 { FormattedTextBox } from './FormattedTextBox';
+import React = require('react');
export class DashDocView {
- _fieldWrapper: HTMLSpanElement; // container for label and value
+ dom: HTMLSpanElement; // container for label and value
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- this._fieldWrapper = document.createElement("span");
- this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.textIndent = "0";
- this._fieldWrapper.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "dimgray" : "lightGray"));
- this._fieldWrapper.style.width = node.attrs.width;
- this._fieldWrapper.style.height = node.attrs.height;
- this._fieldWrapper.style.display = node.attrs.hidden ? "none" : "inline-block";
- (this._fieldWrapper.style as any).float = node.attrs.float;
- this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
-
- ReactDOM.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}
- />, this._fieldWrapper);
- (this as any).dom = this._fieldWrapper;
+ this.dom = document.createElement('span');
+ this.dom.style.position = 'relative';
+ this.dom.style.textIndent = '0';
+ this.dom.style.border = '1px solid ' + StrCast(tbox.layoutDoc.color, Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'dimgray' : 'lightGray');
+ this.dom.style.width = node.attrs.width;
+ this.dom.style.height = node.attrs.height;
+ this.dom.style.display = node.attrs.hidden ? 'none' : 'inline-block';
+ (this.dom.style as any).float = node.attrs.float;
+ this.dom.onkeypress = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeydown = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeyup = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onmousedown = function (e: any) {
+ e.stopPropagation();
+ };
+
+ ReactDOM.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} />,
+ this.dom
+ );
+ (this as any).dom = this.dom;
}
- destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); }
- selectNode() { }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this.dom);
+ }
+ selectNode() {}
}
interface IDashDocViewInternal {
@@ -69,7 +70,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
@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;
@@ -81,13 +83,20 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
}
this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null);
}
- if (this.props.width !== (this._dashDoc?._width ?? "") + "px" || this.props.height !== (this._dashDoc?._height ?? "") + "px") {
- try { // bcz: an exception will be thrown if two aliases 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, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px"
- }));
+ 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
+ this.props.view.dispatch(
+ this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
+ ...this.props.node.attrs,
+ width: this._width + 'px',
+ height: this._height + 'px',
+ })
+ );
} catch (e) {
- console.log("DashDocView:" + e);
+ console.log('DashDocView:' + e);
}
}
});
@@ -98,14 +107,15 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
- this.props.alias && 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);
- }
- });
+ this.props.alias &&
+ 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);
+ }
+ });
} else {
this.updateDoc(dashDoc);
}
@@ -113,67 +123,70 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
}
componentDidMount() {
- this._disposers.upater = reaction(() => this._dashDoc && (NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width)),
- () => {
+ this._disposers.upater = reaction(
+ () => this._dashDoc && NumCast(this._dashDoc._height) + NumCast(this._dashDoc._width),
+ action(() => {
if (this._dashDoc) {
- this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
- ...this.props.node.attrs, width: (this._dashDoc?._width ?? "") + "px", height: (this._dashDoc?._height ?? "") + "px"
- }));
+ this._width = NumCast(this._dashDoc._width);
+ this._height = NumCast(this._dashDoc._height);
+ const parent = this._spanRef.current?.parentNode as HTMLElement;
+ if (parent) {
+ parent.style.width = this._width + 'px';
+ parent.style.height = this._height + 'px';
+ }
}
- });
+ })
+ );
}
-
removeDoc = () => {
- this.props.view.dispatch(this.props.view.state.tr
- .setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos())))
- .deleteSelection());
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.getPos()))).deleteSelection());
return true;
- }
+ };
getDocTransform = () => {
if (!this._spanRef.current) return Transform.Identity();
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) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
onKeyDown = (e: any) => {
e.stopPropagation();
- if (e.key === "Tab" || e.key === "Enter") {
+ if (e.key === 'Tab' || e.key === 'Enter') {
e.preventDefault();
}
- }
+ };
onPointerLeave = () => {
- const ele = document.getElementById("DashDocCommentView-" + this.props.docid) as HTMLDivElement;
- ele && (ele.style.backgroundColor = "");
- }
+ 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;
- ele && (ele.style.backgroundColor = "orange");
- }
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ ele && (ele.style.backgroundColor = 'orange');
+ };
componentWillUnmount = () => Object.values(this._disposers).forEach(disposer => disposer?.());
render() {
- return !this._dashDoc || !this._finalLayout || this.props.hidden ? null :
- <div ref={this._spanRef}
+ return !this._dashDoc || !this._finalLayout || this.props.hidden ? null : (
+ <div
+ ref={this._spanRef}
className="dash-span"
style={{
- width: this.props.width,
- height: this.props.height,
+ width: this._width,
+ height: this._height,
position: 'absolute',
- display: 'inline-block'
+ display: 'inline-block',
}}
onPointerLeave={this.onPointerLeave}
onPointerEnter={this.onPointerEnter}
onKeyDown={this.onKeyDown}
onKeyPress={e => e.stopPropagation()}
onKeyUp={e => e.stopPropagation()}
- onWheel={e => e.preventDefault()}
- >
+ onWheel={e => e.preventDefault()}>
<DocumentView
Document={this._finalLayout}
DataDoc={this._resolvedDataDoc}
@@ -182,7 +195,6 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
removeDocument={this.removeDoc}
isDocumentActive={returnFalse}
isContentActive={this._textBox.props.isContentActive}
- layerProvider={this._textBox.props.layerProvider}
styleProvider={this._textBox.props.styleProvider}
docViewPath={this._textBox.props.docViewPath}
ScreenToLocalTransform={this.getDocTransform}
@@ -201,6 +213,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
ContainingCollectionView={this._textBox.props.ContainingCollectionView}
ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
/>
- </div>;
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 34908e54b..8c8b74560 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,46 +1,55 @@
-import { action, computed, IReactionDisposer, observable } from "mobx";
-import { observer } from "mobx-react";
+import { action, computed, IReactionDisposer, observable } from 'mobx';
+import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom';
-import { DataSym, Doc, DocListCast, 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 { DocServer } from "../../../DocServer";
-import { DocUtils } from "../../../documents/Documents";
-import { CollectionViewType } from "../../collections/CollectionView";
-import "./DashFieldView.scss";
-import { FormattedTextBox } from "./FormattedTextBox";
-import React = require("react");
+import { DataSym, Doc, DocListCast, 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 { DocServer } from '../../../DocServer';
+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';
export class DashFieldView {
- _fieldWrapper: HTMLDivElement; // container for label and value
+ dom: HTMLDivElement; // container for label and value
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- this._fieldWrapper = document.createElement("div");
- this._fieldWrapper.style.width = node.attrs.width;
- this._fieldWrapper.style.height = node.attrs.height;
- this._fieldWrapper.style.fontWeight = "bold";
- this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.display = "inline-block";
- this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
-
- ReactDOM.render(<DashFieldViewInternal
- fieldKey={node.attrs.fieldKey}
- docid={node.attrs.docid}
- width={node.attrs.width}
- height={node.attrs.height}
- hideKey={node.attrs.hideKey}
- tbox={tbox}
- />, this._fieldWrapper);
- (this as any).dom = this._fieldWrapper;
+ const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
+
+ this.dom = document.createElement('div');
+ this.dom.style.width = node.attrs.width;
+ this.dom.style.height = node.attrs.height;
+ this.dom.style.fontWeight = 'bold';
+ this.dom.style.position = 'relative';
+ this.dom.style.display = 'inline-block';
+ this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal;
+ this.dom.onkeypress = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeydown = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeyup = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onmousedown = function (e: any) {
+ e.stopPropagation();
+ };
+
+ setTimeout(() => ReactDOM.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} tbox={tbox} />, this.dom));
+ (this as any).dom = this.dom;
}
- destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); }
- selectNode() { }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this.dom);
+ }
+ selectNode() {}
}
interface IDashFieldViewInternal {
@@ -58,7 +67,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
_textBoxDoc: Doc;
_fieldKey: string;
_fieldStringRef = React.createRef<HTMLSpanElement>();
- @observable _showEnumerables: boolean = false;
@observable _dashDoc: Doc | undefined;
constructor(props: IDashFieldViewInternal) {
@@ -67,8 +75,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
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)));
+ DocServer.GetRefField(this.props.docid).then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
} else {
this._dashDoc = this.props.tbox.rootDoc;
}
@@ -77,45 +84,53 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
this._reactionDisposer?.();
}
- multiValueDelimeter = ";";
+ 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 dashVal = this._dashDoc[this._fieldKey] ?? this._dashDoc[DataSym][this._fieldKey] ?? (this._fieldKey === "PARAMS" ? this._textBoxDoc[this._fieldKey] : "");
- const fval = dashVal instanceof List ? dashVal.join(this.multiValueDelimeter) : StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal;
- const boolVal = Cast(fval, "boolean", null);
- const strVal = Field.toString(fval as Field) || "";
-
+ 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);
- }}
- />;
- }
- else // field value is a string, so display it as an editable span
- {
+ 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) => {
- this._showEnumerables = true;
- e.stopPropagation();
- }));
- }} >
- {strVal}
- </span>;
+ 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 => e.stopPropagation())
+ );
+ }}>
+ {strVal}
+ </span>
+ );
}
}
}
@@ -123,12 +138,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
// 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 === "Enter") { // handle the enter key by "submitting" the current text to Dash's database.
- e.ctrlKey && DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+ 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
+ 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 (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);
@@ -137,59 +153,46 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
}
e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
}
- e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
- }
+ e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
+ };
@action
updateText = (nodeText: string, forceMatch: boolean) => {
- this._showEnumerables = false;
if (nodeText) {
- const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : 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)));
+ 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;
- DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
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(":=")) {
+ else if (nodeText.startsWith(':=')) {
this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
- } else if (nodeText.startsWith("=:=")) {
+ } 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);
+ if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText);
Doc.SetInPlace(this._dashDoc!, this._fieldKey, newText, true);
} else {
- const splits = newText.split(this.multiValueDelimeter);
- if (this._fieldKey !== "PARAMS" || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) {
+ 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;
+ if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal;
Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true);
}
}
}
});
}
- }
-
- // display a collection of all the enumerable values for this field
- onPointerDownEnumerables = async (e: any) => {
- e.stopPropagation();
- const collview = await DocUtils.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
- collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "add:right");
- }
-
+ };
- // clicking on the label creates a pivot view collection of all documents
- // in the same collection. The pivot field is the fieldKey of this label
- onPointerDownLabelSpan = (e: any) => {
- e.stopPropagation();
+ createPivotForField = (e: React.MouseEvent) => {
let container = this.props.tbox.props.ContainingCollectionView;
while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
container = container.props.ContainingCollectionView;
@@ -201,27 +204,72 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
if (!list) {
alias._columnHeaders = 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, "add:right");
+ 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, 'add:right');
}
- }
+ };
+
+ // clicking on the label creates a pivot view collection of all documents
+ // in the same collection. The pivot field is the fieldKey of this label
+ onPointerDownLabelSpan = (e: any) => {
+ setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
+ DashFieldViewMenu.createFieldView = this.createPivotForField;
+ DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16);
+ });
+ };
render() {
- return <div className="dashFieldView" style={{
- width: this.props.width,
- height: this.props.height,
- }}>
- {this.props.hideKey ? (null) :
- <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
- {this._fieldKey}
- </span>}
+ return (
+ <div
+ className="dashFieldView"
+ style={{
+ width: this.props.width,
+ height: this.props.height,
+ }}>
+ {this.props.hideKey ? null : (
+ <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
+ {this._fieldKey}
+ </span>
+ )}
- {this.props.fieldKey.startsWith("#") ? (null) : this.fieldValueContent}
+ {this.props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
+ </div>
+ );
+ }
+}
+@observer
+export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
+ static Instance: DashFieldViewMenu;
+ static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
+ constructor(props: any) {
+ super(props);
+ DashFieldViewMenu.Instance = this;
+ }
+ @action
+ showFields = (e: React.MouseEvent) => {
+ DashFieldViewMenu.createFieldView(e);
+ DashFieldViewMenu.Instance.fadeOut(true);
+ };
- {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
+ public show = (x: number, y: number) => {
+ this.jumpTo(x, y, true);
+ const hideMenu = () => {
+ this.fadeOut(true);
+ document.removeEventListener('pointerdown', hideMenu);
+ };
+ document.addEventListener('pointerdown', hideMenu);
+ };
+ render() {
+ const buttons = [
+ <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}>
+ <button className="antimodeMenu-button" onPointerDown={this.showFields}>
+ <FontAwesomeIcon icon="eye" size="lg" />
+ </button>
+ </Tooltip>,
+ ];
- </div >;
+ return this.getElement(buttons);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 508500ab6..98d611ca6 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -1,38 +1,38 @@
-import EquationEditor from "equation-editor-react";
-import { IReactionDisposer } from "mobx";
-import { observer } from "mobx-react";
+import EquationEditor from 'equation-editor-react';
+import { IReactionDisposer } from 'mobx';
+import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom';
-import { Doc } from "../../../../fields/Doc";
-import { StrCast } from "../../../../fields/Types";
-import "./DashFieldView.scss";
-import { FormattedTextBox } from "./FormattedTextBox";
-import React = require("react");
+import { Doc } from '../../../../fields/Doc';
+import { StrCast } from '../../../../fields/Types';
+import './DashFieldView.scss';
+import { FormattedTextBox } from './FormattedTextBox';
+import React = require('react');
export class EquationView {
- _fieldWrapper: HTMLDivElement; // container for label and value
+ dom: HTMLDivElement; // container for label and value
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- this._fieldWrapper = document.createElement("div");
- this._fieldWrapper.style.width = node.attrs.width;
- this._fieldWrapper.style.height = node.attrs.height;
- this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.display = "inline-block";
- this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
+ this.dom = document.createElement('div');
+ this.dom.style.width = node.attrs.width;
+ this.dom.style.height = node.attrs.height;
+ this.dom.style.position = 'relative';
+ this.dom.style.display = 'inline-block';
+ this.dom.onmousedown = function (e: any) {
+ e.stopPropagation();
+ };
- ReactDOM.render(<EquationViewInternal
- fieldKey={node.attrs.fieldKey}
- width={node.attrs.width}
- height={node.attrs.height}
- setEditor={this.setEditor}
- tbox={tbox}
- />, this._fieldWrapper);
- (this as any).dom = this._fieldWrapper;
+ ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} setEditor={this.setEditor} tbox={tbox} />, this.dom);
+ (this as any).dom = this.dom;
}
_editor: EquationEditor | undefined;
- setEditor = (editor?: EquationEditor) => this._editor = editor;
- destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); }
- selectNode() { this._editor?.mathField.focus(); }
- deselectNode() { }
+ setEditor = (editor?: EquationEditor) => (this._editor = editor);
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this.dom);
+ }
+ selectNode() {
+ this._editor?.mathField.focus();
+ }
+ deselectNode() {}
}
interface IEquationViewInternal {
@@ -56,24 +56,33 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
this._textBoxDoc = this.props.tbox.props.Document;
}
- componentWillUnmount() { this._reactionDisposer?.(); }
- componentDidMount() { this.props.setEditor(this._ref.current ?? undefined); }
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ }
+ componentDidMount() {
+ this.props.setEditor(this._ref.current ?? undefined);
+ }
render() {
- return <div className="equationView" style={{
- position: "relative",
- display: "inline-block",
- width: this.props.width,
- height: this.props.height,
- bottom: 3,
- }}>
- <EquationEditor ref={this._ref}
- value={StrCast(this._textBoxDoc[this._fieldKey], "y=")}
- onChange={str => this._textBoxDoc[this._fieldKey] = str}
- autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
- autoOperatorNames="sin cos tan"
- spaceBehavesLikeTab={true}
- />
- </div >;
+ return (
+ <div
+ className="equationView"
+ style={{
+ position: 'relative',
+ display: 'inline-block',
+ width: this.props.width,
+ height: this.props.height,
+ bottom: 3,
+ }}>
+ <EquationEditor
+ ref={this._ref}
+ value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')}
+ onChange={str => (this._textBoxDoc[this._fieldKey] = str)}
+ autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
+ autoOperatorNames="sin cos tan"
+ spaceBehavesLikeTab={true}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 27817f317..d3d8c47c0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import "../../global/globalCssVariables";
+@import '../../global/globalCssVariables';
.ProseMirror {
width: 100%;
@@ -11,13 +11,13 @@
}
audiotag {
- left: 0;
- position: absolute;
- cursor: pointer;
- border-radius: 10px;
- width: 10px;
- margin-top: -2px;
- font-size: 4px;
+ left: 0;
+ position: absolute;
+ cursor: pointer;
+ border-radius: 10px;
+ width: 10px;
+ margin-top: -2px;
+ font-size: 4px;
background: lightblue;
}
audiotag:hover {
@@ -63,12 +63,11 @@ audiotag:hover {
.formattedTextBox-outer-selected {
cursor: text;
}
-
+
.formattedTextBox-sidebar-handle {
position: absolute;
top: 0;
- left: 17px;
- //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views
+ right: 0;
width: 17px;
height: 17px;
font-size: 11px;
@@ -79,15 +78,14 @@ audiotag:hover {
display: flex;
justify-content: center;
align-items: center;
- cursor:grabbing;
+ cursor: grabbing;
box-shadow: $standard-box-shadow;
// transition: 0.2s;
opacity: 0.3;
- &:hover{
+ &:hover {
opacity: 1 !important;
filter: brightness(0.85);
}
-
}
.formattedTextBox-sidebar,
@@ -117,14 +115,15 @@ audiotag:hover {
left: 10%;
}
-.formattedTextBox-inner-rounded, .formattedTextBox-inner-rounded-selected,
-.formattedTextBox-inner,
+.formattedTextBox-inner-rounded,
+.formattedTextBox-inner-rounded-selected,
+.formattedTextBox-inner,
.formattedTextBox-inner-minimal,
.formattedTextBox-inner-selected {
height: 100%;
white-space: pre-wrap;
.ProseMirror:hover {
- background: rgba(200,200,200,0.2);
+ background: rgba(200, 200, 200, 0.2);
}
hr {
display: block;
@@ -141,7 +140,7 @@ audiotag:hover {
.formattedTextBox-inner-rounded-selected,
.formattedTextBox-inner-selected {
> .ProseMirror {
- padding:10px;
+ padding: 10px;
}
}
.formattedTextBox-outer-selected {
@@ -236,18 +235,17 @@ footnote::after {
position: absolute;
top: -5px;
left: 27px;
- content: " ";
+ content: ' ';
height: 0;
width: 0;
}
-
.formattedTextBox-inlineComment {
position: relative;
width: 40px;
height: 20px;
&::before {
- content: "→";
+ content: '→';
}
&:hover {
background: orange;
@@ -260,7 +258,7 @@ footnote::after {
width: 40px;
height: 20px;
&::after {
- content: "←";
+ content: '←';
}
}
@@ -270,21 +268,21 @@ footnote::after {
width: 40px;
height: 20px;
&::after {
- content: "...";
+ content: '...';
}
}
.prosemirror-anchor {
- overflow:hidden;
- display:inline-grid;
+ overflow: hidden;
+ display: inline-grid;
}
.prosemirror-linkBtn {
- background:unset;
- color:unset;
- padding:0;
+ background: unset;
+ color: unset;
+ padding: 0;
text-transform: unset;
letter-spacing: unset;
- font-size:unset;
+ font-size: unset;
}
.prosemirror-links {
display: none;
@@ -294,28 +292,28 @@ footnote::after {
z-index: 1;
padding: 5;
border-radius: 2px;
- }
- .prosemirror-hrefoptions{
- width:0px;
- border:unset;
- padding:0px;
- }
-
- .prosemirror-links a {
+}
+.prosemirror-hrefoptions {
+ width: 0px;
+ border: unset;
+ padding: 0px;
+}
+
+.prosemirror-links a {
float: left;
color: white;
text-decoration: none;
border-radius: 3px;
- }
+}
- .prosemirror-links a:hover {
+.prosemirror-links a:hover {
background-color: #eee;
color: black;
- }
+}
- .prosemirror-anchor:hover .prosemirror-links {
+.prosemirror-anchor:hover .prosemirror-links {
display: grid;
- }
+}
.ProseMirror {
padding: 0px;
@@ -334,7 +332,8 @@ footnote::after {
border-left: solid 2px dimgray;
}
- ol, ul {
+ ol,
+ ul {
counter-reset: deci1 0 multi1 0;
padding-left: 1em;
font-family: inherit;
@@ -342,42 +341,231 @@ footnote::after {
ol {
font-family: inherit;
}
- .bullet { p { font-family: inherit} margin-left: 0; }
- .bullet1 { p { font-family: inherit} }
- .bullet2,.bullet3,.bullet4,.bullet5,.bullet6 { p { font-family: inherit} font-size: smaller; }
+ .bullet {
+ p {
+ font-family: inherit;
+ }
+ margin-left: 0;
+ }
+ .bullet1 {
+ p {
+ font-family: inherit;
+ }
+ }
+ .bullet2,
+ .bullet3,
+ .bullet4,
+ .bullet5,
+ .bullet6 {
+ p {
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
- .decimal1-ol { counter-reset: deci1; p {display: inline-block; font-family: inherit} margin-left: 0; }
- .decimal2-ol { counter-reset: deci2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.1em;}
- .decimal3-ol { counter-reset: deci3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
- .decimal4-ol { counter-reset: deci4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
- .decimal5-ol { counter-reset: deci5; p {display: inline-block; font-family: inherit} font-size: smaller; }
- .decimal6-ol { counter-reset: deci6; p {display: inline-block; font-family: inherit} font-size: smaller; }
- .decimal7-ol { counter-reset: deci7; p {display: inline-block; font-family: inherit} font-size: smaller; }
+ .decimal1-ol {
+ counter-reset: deci1;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ margin-left: 0;
+ }
+ .decimal2-ol {
+ counter-reset: deci2;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2.1em;
+ }
+ .decimal3-ol {
+ counter-reset: deci3;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2.85em;
+ }
+ .decimal4-ol {
+ counter-reset: deci4;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 3.85em;
+ }
+ .decimal5-ol {
+ counter-reset: deci5;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
+ .decimal6-ol {
+ counter-reset: deci6;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
+ .decimal7-ol {
+ counter-reset: deci7;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
- .multi1-ol { counter-reset: multi1; p {display: inline-block; font-family: inherit} margin-left: 0; padding-left: 1.2em }
- .multi2-ol { counter-reset: multi2; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .multi3-ol { counter-reset: multi3; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 2.85em;}
- .multi4-ol { counter-reset: multi4; p {display: inline-block; font-family: inherit} font-size: smaller; padding-left: 3.85em;}
+ .multi1-ol {
+ counter-reset: multi1;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ margin-left: 0;
+ padding-left: 1.2em;
+ }
+ .multi2-ol {
+ counter-reset: multi2;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2em;
+ }
+ .multi3-ol {
+ counter-reset: multi3;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2.85em;
+ }
+ .multi4-ol {
+ counter-reset: multi4;
+ p {
+ display: inline-block;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 3.85em;
+ }
//.bullet:before, .bullet1:before, .bullet2:before, .bullet3:before, .bullet4:before, .bullet5:before { transition: 0.5s; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content:" " }
- .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; vertical-align: top; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
- .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; vertical-align: top; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
- .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; vertical-align: top; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
- .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; vertical-align: top; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
- .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; vertical-align: top; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
- .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; vertical-align: top; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
- .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; vertical-align: top; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
+ .decimal1:before {
+ transition: 0.5s;
+ counter-increment: deci1;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -1em;
+ width: 1em;
+ content: counter(deci1) '. ';
+ }
+ .decimal2:before {
+ transition: 0.5s;
+ counter-increment: deci2;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2.1em;
+ width: 2.1em;
+ content: counter(deci1) '.' counter(deci2) '. ';
+ }
+ .decimal3:before {
+ transition: 0.5s;
+ counter-increment: deci3;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2.85em;
+ width: 2.85em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '. ';
+ }
+ .decimal4:before {
+ transition: 0.5s;
+ counter-increment: deci4;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -3.85em;
+ width: 3.85em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '. ';
+ }
+ .decimal5:before {
+ transition: 0.5s;
+ counter-increment: deci5;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2em;
+ width: 5em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '. ';
+ }
+ .decimal6:before {
+ transition: 0.5s;
+ counter-increment: deci6;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2em;
+ width: 6em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '. ';
+ }
+ .decimal7:before {
+ transition: 0.5s;
+ counter-increment: deci7;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2em;
+ width: 7em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '.' counter(deci7) '. ';
+ }
- .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; vertical-align: top; margin-left: -1.3em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
- .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; vertical-align: top; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
- .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; vertical-align: top; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
- .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; vertical-align: top; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
+ .multi1:before {
+ transition: 0.5s;
+ counter-increment: multi1;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -1.3em;
+ width: 1.2em;
+ content: counter(multi1, upper-alpha) '. ';
+ }
+ .multi2:before {
+ transition: 0.5s;
+ counter-increment: multi2;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2em;
+ width: 2em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '. ';
+ }
+ .multi3:before {
+ transition: 0.5s;
+ counter-increment: multi3;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -2.85em;
+ width: 2.85em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '. ';
+ }
+ .multi4:before {
+ transition: 0.5s;
+ counter-increment: multi4;
+ display: inline-block;
+ vertical-align: top;
+ margin-left: -4.2em;
+ width: 4.2em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '.' counter(multi4, lower-roman) '. ';
+ }
}
-
@media only screen and (max-width: 1000px) {
- @import "../../global/globalCssVariables";
+ @import '../../global/globalCssVariables';
.ProseMirror {
width: 100%;
@@ -425,7 +613,7 @@ footnote::after {
width: 100%;
height: 100%;
}
-
+
.formattedTextBox-sidebar-handle {
position: absolute;
background: lightgray;
@@ -562,18 +750,17 @@ footnote::after {
position: absolute;
top: -5px;
left: 27px;
- content: " ";
+ content: ' ';
height: 0;
width: 0;
}
-
.formattedTextBox-inlineComment {
position: relative;
width: 40px;
height: 20px;
&::before {
- content: "→";
+ content: '→';
}
&:hover {
background: orange;
@@ -586,7 +773,7 @@ footnote::after {
width: 40px;
height: 20px;
&::after {
- content: "←";
+ content: '←';
}
}
@@ -596,7 +783,7 @@ footnote::after {
width: 40px;
height: 20px;
&::after {
- content: "...";
+ content: '...';
}
}
@@ -606,7 +793,8 @@ footnote::after {
font-family: inherit;
}
- ol, ul {
+ ol,
+ ul {
counter-reset: deci1 0 multi1 0;
padding-left: 1em;
font-family: inherit;
@@ -616,30 +804,191 @@ footnote::after {
font-family: inherit;
}
- .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; }
- .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;}
- .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;}
- .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; }
- .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; }
- .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; }
-
- .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em }
- .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
- .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
-
- .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
- .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
- .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
- .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
- .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
- .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
- .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
-
- .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
- .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
- .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
- .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
+ .decimal1-ol {
+ counter-reset: deci1;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ margin-left: 0;
+ }
+ .decimal2-ol {
+ counter-reset: deci2;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 1em;
+ }
+ .decimal3-ol {
+ counter-reset: deci3;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2em;
+ }
+ .decimal4-ol {
+ counter-reset: deci4;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 3em;
+ }
+ .decimal5-ol {
+ counter-reset: deci5;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
+ .decimal6-ol {
+ counter-reset: deci6;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
+ .decimal7-ol {
+ counter-reset: deci7;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ }
+
+ .multi1-ol {
+ counter-reset: multi1;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ margin-left: 0;
+ padding-left: 1.2em;
+ }
+ .multi2-ol {
+ counter-reset: multi2;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 1.4em;
+ }
+ .multi3-ol {
+ counter-reset: multi3;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 2em;
+ }
+ .multi4-ol {
+ counter-reset: multi4;
+ p {
+ display: inline;
+ font-family: inherit;
+ }
+ font-size: smaller;
+ padding-left: 3.4em;
+ }
+
+ .decimal1:before {
+ transition: 0.5s;
+ counter-increment: deci1;
+ display: inline-block;
+ margin-left: -1em;
+ width: 1em;
+ content: counter(deci1) '. ';
+ }
+ .decimal2:before {
+ transition: 0.5s;
+ counter-increment: deci2;
+ display: inline-block;
+ margin-left: -2.1em;
+ width: 2.1em;
+ content: counter(deci1) '.' counter(deci2) '. ';
+ }
+ .decimal3:before {
+ transition: 0.5s;
+ counter-increment: deci3;
+ display: inline-block;
+ margin-left: -2.85em;
+ width: 2.85em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '. ';
+ }
+ .decimal4:before {
+ transition: 0.5s;
+ counter-increment: deci4;
+ display: inline-block;
+ margin-left: -3.85em;
+ width: 3.85em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '. ';
+ }
+ .decimal5:before {
+ transition: 0.5s;
+ counter-increment: deci5;
+ display: inline-block;
+ margin-left: -2em;
+ width: 5em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '. ';
+ }
+ .decimal6:before {
+ transition: 0.5s;
+ counter-increment: deci6;
+ display: inline-block;
+ margin-left: -2em;
+ width: 6em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '. ';
+ }
+ .decimal7:before {
+ transition: 0.5s;
+ counter-increment: deci7;
+ display: inline-block;
+ margin-left: -2em;
+ width: 7em;
+ content: counter(deci1) '.' counter(deci2) '.' counter(deci3) '.' counter(deci4) '.' counter(deci5) '.' counter(deci6) '.' counter(deci7) '. ';
+ }
+
+ .multi1:before {
+ transition: 0.5s;
+ counter-increment: multi1;
+ display: inline-block;
+ margin-left: -1em;
+ width: 1.2em;
+ content: counter(multi1, upper-alpha) '. ';
+ }
+ .multi2:before {
+ transition: 0.5s;
+ counter-increment: multi2;
+ display: inline-block;
+ margin-left: -2em;
+ width: 2em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '. ';
+ }
+ .multi3:before {
+ transition: 0.5s;
+ counter-increment: multi3;
+ display: inline-block;
+ margin-left: -2.85em;
+ width: 2.85em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '. ';
+ }
+ .multi4:before {
+ transition: 0.5s;
+ counter-increment: multi4;
+ display: inline-block;
+ margin-left: -4.2em;
+ width: 4.2em;
+ content: counter(multi1, upper-alpha) '.' counter(multi2, decimal) '.' counter(multi3, lower-alpha) '.' counter(multi4, lower-roman) '. ';
+ }
}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 6192b6829..a018f51c2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,89 +1,91 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx";
-import { observer } from "mobx-react";
-import { baseKeymap, selectAll } from "prosemirror-commands";
-import { history } from "prosemirror-history";
+import { isEqual } from 'lodash';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { baseKeymap, selectAll } from 'prosemirror-commands';
+import { history } from 'prosemirror-history';
import { inputRules } from 'prosemirror-inputrules';
-import { keymap } from "prosemirror-keymap";
-import { Fragment, Mark, Node, Slice } from "prosemirror-model";
-import { ReplaceStep } from 'prosemirror-transform';
-import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
+import { keymap } from 'prosemirror-keymap';
+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, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
+import { AclAdmin, AclAugment, AclEdit, AclReadonly, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
-import { RichTextField } from "../../../../fields/RichTextField";
+import { RichTextField } from '../../../../fields/RichTextField';
import { RichTextUtils } from '../../../../fields/RichTextUtils';
-import { Cast, DateCast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { ComputedField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
-import { DocServer } from "../../../DocServer";
+import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
-import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from "../../../util/DragManager";
-import { makeTemplate } from '../../../util/DropConverter';
-import { SelectionManager } from "../../../util/SelectionManager";
+import { DragManager } from '../../../util/DragManager';
+import { MakeTemplate } from '../../../util/DropConverter';
+import { LinkManager } from '../../../util/LinkManager';
+import { SelectionManager } from '../../../util/SelectionManager';
import { SnappingManager } from '../../../util/SnappingManager';
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
-import { ViewBoxAnnotatableComponent } from "../../DocComponent";
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
-import { AudioBox } from '../AudioBox';
-import { FieldView, FieldViewProps } from "../FieldView";
+import { FieldView, FieldViewProps } from '../FieldView';
import { LinkDocPreview } from '../LinkDocPreview';
-import { DashDocCommentView } from "./DashDocCommentView";
-import { DashDocView } from "./DashDocView";
-import { DashFieldView } from "./DashFieldView";
-import { EquationView } from "./EquationView";
-import { FootnoteView } from "./FootnoteView";
-import "./FormattedTextBox.scss";
+import { DashDocCommentView } from './DashDocCommentView';
+import { DashDocView } from './DashDocView';
+import { DashFieldView } from './DashFieldView';
+import { EquationView } from './EquationView';
+import { FootnoteView } from './FootnoteView';
+import './FormattedTextBox.scss';
import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { OrderedListView } from "./OrderedListView";
-import { buildKeymap, updateBullets } from "./ProsemirrorExampleTransfer";
-import { removeMarkWithAttrs } from "./prosemirrorPatches";
+import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer';
+import { removeMarkWithAttrs } from './prosemirrorPatches';
import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu';
-import { RichTextRules } from "./RichTextRules";
-import { schema } from "./schema_rts";
-import { SummaryView } from "./SummaryView";
-import applyDevTools = require("prosemirror-dev-tools");
-import React = require("react");
-import { SidebarAnnos } from '../../SidebarAnnos';
-import { Colors } from '../../global/globalEnums';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-const translateGoogleApi = require("translate-google-api");
+import { RichTextRules } from './RichTextRules';
+import { schema } from './schema_rts';
+import { SummaryView } from './SummaryView';
+import applyDevTools = require('prosemirror-dev-tools');
+import React = require('react');
+import { text } from 'body-parser';
+import { CollectionTreeView } from '../../collections/CollectionTreeView';
+const translateGoogleApi = require('translate-google-api');
export interface FormattedTextBoxProps {
- makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
- xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
+ makeLink?: () => Opt<Doc>; // bcz: hack: notifies the text document when the container has made a link. allows the text doc to react and setup a hyeprlink for any selected text
+ xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView
yPadding?: number;
noSidebar?: boolean;
dontScale?: boolean;
dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field)
}
-export const GoogleRef = "googleDocId";
+export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps)>() {
- public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
+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 _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"];
+ static _globalHighlights: 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();
@@ -93,7 +95,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
private _editorView: Opt<EditorView>;
- private _applyingChange: string = "";
+ private _applyingChange: string = '';
private _searchIndex = 0;
private _lastTimedMark: Mark | undefined = undefined;
private _cachedLinks: Doc[] = [];
@@ -102,7 +104,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _dropDisposer?: DragManager.DragDropDisposer;
private _recordingStart: number = 0;
private _ignoreScroll = false;
- private _lastText = "";
+ private _lastText = '';
private _focusSpeed: Opt<number>;
private _keymap: any = undefined;
private _rules: RichTextRules | undefined;
@@ -113,21 +115,48 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
private _downY = 0;
private _break = true;
public ProseRef?: HTMLDivElement;
- public get EditorView() { return this._editorView; }
- public get SidebarKey() { return this.fieldKey + "-sidebar"; }
- @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); }
-
- @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
- @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
- @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; }
- @computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
- @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); }
- @computed get sidebarHeight() { 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 _recording() { return this.dataDoc?.mediaState === "recording"; }
+ public get EditorView() {
+ return this._editorView;
+ }
+ public get SidebarKey() {
+ 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;
+ }
+ @computed get sidebarWidthPercent() {
+ return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
+ }
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4'));
+ }
+ @computed get autoHeight() {
+ return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight;
+ }
+ @computed get textHeight() {
+ return NumCast(this.rootDoc[this.fieldKey + '-height']);
+ }
+ @computed get scrollHeight() {
+ return NumCast(this.rootDoc[this.fieldKey + '-scrollHeight']);
+ }
+ @computed get sidebarHeight() {
+ 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 _recording() {
+ return this.dataDoc?.mediaState === 'recording';
+ }
set _recording(value) {
- !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? "recording" : undefined);
+ !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? 'recording' : undefined);
}
@computed get config() {
this._keymap = buildKeymap(schema, this.props);
@@ -140,28 +169,33 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
history(),
keymap(this._keymap),
keymap(baseKeymap),
- new Plugin({ props: { attributes: { class: "ProseMirror-example-setup-style" } } }),
- new Plugin({ view(editorView) { return new FormattedTextBoxComment(editorView); } })
- ]
+ new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }),
+ new Plugin({
+ view(editorView) {
+ return new FormattedTextBoxComment(editorView);
+ },
+ }),
+ ],
};
}
public static PasteOnLoad: ClipboardEvent | undefined;
- public static SelectOnLoad = "";
+ public static SelectOnLoad = '';
public static DontSelectInitialText = false; // whether initial text should be selected or not
- public static SelectOnLoadChar = "";
- public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; }
+ public static SelectOnLoadChar = '';
+ public static IsFragment(html: string) {
+ return html.indexOf('data-pm-slice') !== -1;
+ }
public static GetHref(html: string): string {
const parser = new DOMParser();
const parsedHtml = parser.parseFromString(html, 'text/html');
- if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
- (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+ if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
}
- return "";
+ 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) {
@@ -172,7 +206,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
// removes all hyperlink anchors for the removed linkDoc
- // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one.
+ // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one.
// but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing.
public RemoveLinkFromDoc(linkDoc?: Doc) {
this.unhighlightSearchTerms();
@@ -195,9 +229,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
}
- // removes all the specified link references from the selection.
+ // removes all the specified link references from the selection.
// NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references.
- public RemoveAnchorFromSelection(allAnchors: { href: string, title: string, linkId: string, targetId: string }[]) {
+ public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) {
const state = this._editorView?.state;
if (state && this._editorView) {
this._editorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors }));
@@ -205,11 +239,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
- getAnchor = () => this.makeLinkAnchor(undefined, "add:right", undefined, "Anchored Selection");
+ getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection');
@action
setupAnchorMenu = () => {
- AnchorMenu.Instance.Status = "marquee";
+ AnchorMenu.Instance.Status = 'marquee';
AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
!this.layoutDoc.showSidebar && this.toggleSidebar();
@@ -220,15 +254,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return undefined;
});
AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
+ AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
- * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
+ * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
*/
AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
const targetCreator = (annotationOn?: Doc) => {
- const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn);
+ const target = DocUtils.GetNewTextDoc('Note linked to ' + this.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn);
FormattedTextBox.SelectOnLoad = target[Id];
return target;
};
@@ -237,56 +272,56 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
});
const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
- }
+ };
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- const tsel = this._editorView.state.selection.$from;
- tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
- const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
- const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box
- const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
+ const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+ const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box
+ const curProto = Cast(Cast(this.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 effectiveAcl = GetEffectiveAcl(this.dataDoc);
- const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ?
- json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\"");
+ const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"'));
- if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) {
+ if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) {
const accumTags = [] as string[];
state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => {
- if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith("#")) {
+ if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) {
accumTags.push(node.attrs.fieldKey);
}
});
- const curTags = Object.keys(this.dataDoc).filter(key => key.startsWith("#"));
+ const curTags = Object.keys(this.dataDoc).filter(key => key.startsWith('#'));
const added = accumTags.filter(tag => !curTags.includes(tag));
const removed = curTags.filter(tag => !accumTags.includes(tag));
- removed.forEach(r => this.dataDoc[r] = undefined);
- added.forEach(a => this.dataDoc[a] = a);
+ removed.forEach(r => (this.dataDoc[r] = undefined));
+ added.forEach(a => (this.dataDoc[a] = a));
let unchanged = true;
if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
this._applyingChange = this.fieldKey;
- (curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text) && (this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now())));
- if ((!curTemp && !curProto) || curText || json.includes("dash")) { // 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)
+ curText !== Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text && (this.dataDoc[this.props.fieldKey + '-lastModified'] = new DateField(new Date(Date.now())));
+ if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+ // 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)) {
this.dataDoc[this.props.fieldKey] = new RichTextField(json, curText);
- this.dataDoc[this.props.fieldKey + "-noTemplate"] = true;//(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ this.dataDoc[this.props.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 });
unchanged = false;
}
- } else { // if we've deleted all the text in a note driven by a template, then restore the template data
+ } else {
+ // if we've deleted all the text in a note driven by a template, then restore the template data
this.dataDoc[this.props.fieldKey] = undefined;
this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
- this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
+ this.dataDoc[this.props.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 });
unchanged = false;
}
- this._applyingChange = "";
+ this._applyingChange = '';
if (!unchanged) {
this.updateTitle();
this.tryUpdateScrollHeight();
@@ -304,16 +339,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
AnchorMenu.Instance.fadeOut(true);
}
}
- }
+ };
- // for inserting timestamps
+ // for inserting timestamps
insertTime = () => {
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;
- if (anchor && (anchor.annotationOn as Doc).mediaState === "recording") {
+ const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
+ if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') {
linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
linkAnchor = anchor;
link = l;
@@ -344,40 +379,85 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
}
}
- }
+ };
+
+ autoLink = () => {
+ const newAutoLinks = new Set<Doc>();
+ const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+ if (this._editorView?.state.doc.textContent) {
+ const f = this._editorView.state.selection.from;
+ const t = this._editorView.state.selection.to;
+ var tr = this._editorView.state.tr as any;
+ const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
+ tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
+ DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
+ tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+ this._editorView?.dispatch(tr);
+ }
+ oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
+ };
updateTitle = () => {
- if (!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
- StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.dataDoc["title-custom"] &&
- (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === "text")) {
+ const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text);
+ if (
+ !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'] &&
+ (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text')
+ ) {
let node = this._editorView.state.doc;
- while (node.firstChild && node.firstChild.type.name !== "text") node = node.firstChild;
+ while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild;
const str = node.textContent;
- this.dataDoc.title = "-" + str.substr(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : "");
+ const prefix = str.startsWith('@') ? '' : '-';
+
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title));
+ if (!(cfield instanceof ComputedField)) {
+ this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '');
+ if (str.startsWith('@') && str.length > 1) {
+ Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc);
+ }
+ }
}
- }
+ };
- // needs a better API for taking in a set of words with target documents instead of just one target
- hyperlinkTerms = (terms: string[], target: Doc) => {
- if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
- const res1 = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- let tr = this._editorView.state.tr;
- const flattened1: TextSelection[] = [];
- res1.map(r => r.map(h => flattened1.push(h)));
+ // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@'
+ hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
+ const editorView = this._editorView;
+ if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) {
+ const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
+ const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
+ var alink: Doc | undefined;
flattened1.forEach((flat, i) => {
- const flattened: TextSelection[] = [];
- const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- res.map(r => r.map(h => flattened.push(h)));
+ const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm);
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
- const anchor = Docs.Create.TextanchorDocument();
- const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!;
- const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }];
- const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location });
- tr = tr.addMark(flattened[i].from, flattened[i].to, link);
+ const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
+ const sel = flattened[i];
+ tr = tr.addMark(sel.from, sel.to, splitter);
+ tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
+ if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
+ 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)!);
+ 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 ?? []));
+ const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' });
+ tr = tr.addMark(pos, pos + node.nodeSize, link);
+ }
+ });
+ tr = tr.removeMark(sel.from, sel.to, splitter);
});
- this._editorView.dispatch(tr);
}
- }
+ return tr;
+ };
+ @action
+ search = (searchString: string, bwd?: boolean, clear: boolean = false) => {
+ if (clear) this.unhighlightSearchTerms();
+ else this.highlightSearchTerms([searchString], bwd!);
+ return true;
+ };
highlightSearchTerms = (terms: string[], backward: boolean) => {
if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
@@ -391,21 +471,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (backward === true) {
if (this._searchIndex > 1) {
this._searchIndex += -2;
- }
- else if (this._searchIndex === 1) {
+ } else if (this._searchIndex === 1) {
this._searchIndex = length - 1;
- }
- else if (this._searchIndex === 0 && length !== 1) {
+ } else if (this._searchIndex === 0 && length !== 1) {
this._searchIndex = length - 2;
}
-
}
const lastSel = Math.min(flattened.length - 1, this._searchIndex);
- flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
+ flattened.forEach((h: TextSelection, ind: number) => (tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark)));
flattened[lastSel] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
}
- }
+ };
unhighlightSearchTerms = () => {
if (window.screen.width < 600) null;
@@ -414,20 +491,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
const end = this._editorView.state.doc.nodeSize - 2;
this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
-
}
if (FormattedTextBox.PasteOnLoad) {
- const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfOrigin");
- const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData("dash/pdfRegion");
+ const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfOrigin');
+ const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfRegion');
FormattedTextBox.PasteOnLoad = undefined;
setTimeout(() => pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, undefined), 10);
}
- }
+ };
adoptAnnotation = (start: number, end: number, mark: Mark) => {
const view = this._editorView!;
const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail });
view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
- }
+ };
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
this.ProseRef = ele;
@@ -435,8 +511,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.setupEditor(this.config, this.props.fieldKey);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
}
- // if (this.autoHeight) this.tryUpdateScrollHeight();
- }
+ // if (this.autoHeight) this.tryUpdateScrollHeight();
+ };
@undoBatch
@action
@@ -454,22 +530,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
// embed document when dragg marked as embed
} else if (de.embedKey) {
const target = dragData.droppedDocuments[0];
- target._fitToBox = true;
+ target._fitContentsToBox = true;
const node = schema.nodes.dashDoc.create({
width: target[WidthSym](),
height: target[HeightSym](),
- title: "dashDoc",
+ title: 'dashDoc',
docid: target[Id],
- float: "unset"
+ float: 'unset',
});
const view = this._editorView!;
view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
e.stopPropagation();
} // otherwise, fall through to outer collection to handle drop
}
- }
+ };
- getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+ getNodeEndpoints(context: Node, node: Node): { from: number; to: number } | null {
let offset = 0;
if (context === node) return { from: offset, to: offset + node.nodeSize };
@@ -480,8 +556,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const result = this.getNodeEndpoints((context.content as any).content[i], node);
if (result) {
return {
- from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
- to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+ from: result.from + offset + (context.type.name === 'doc' ? 0 : 1),
+ to: result.to + offset + (context.type.name === 'doc' ? 0 : 1),
};
}
offset += (context.content as any).content[i].nodeSize;
@@ -497,9 +573,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
let ret: TextSelection[] = [];
if (node.isTextblock) {
- let index = 0, foundAt;
+ let index = 0,
+ foundAt;
const ep = this.getNodeEndpoints(pm.state.doc, node);
- const regexp = new RegExp(find.replace("*", ""), "i");
+ const regexp = new RegExp(find.replace('*', ''), 'i');
if (regexp) {
while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) {
const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
@@ -508,165 +585,247 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
} else {
- node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
+ node.content.forEach((child, i) => (ret = ret.concat(this.findInNode(pm, child, find))));
}
return ret;
}
updateHighlights = () => {
+ const highlights = FormattedTextBox._globalHighlights;
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
- if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, "");
+ if (highlights.indexOf('Audio Tags') === -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, '');
+ }
+ if (highlights.indexOf('Text from Others') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
}
- if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
+ if (highlights.indexOf('My Text') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' });
}
- if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ if (highlights.indexOf('Todo Items') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'todo', { outline: 'black solid 1px' });
}
- if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" });
+ if (highlights.indexOf('Important Items') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'important', { 'font-size': 'larger' });
}
- if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
- 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 (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" });
+ if (highlights.indexOf('Disagree Items') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'disagree', { 'text-decoration': 'line-through' });
}
- if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" });
+ if (highlights.indexOf('Ignore Items') !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'ignore', { 'font-size': '1' });
}
- if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ if (highlights.indexOf('By Recent Minute') !== -1) {
+ 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() }));
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
setTimeout(this.updateHighlights);
}
- if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ if (highlights.indexOf('By Recent Hour') !== -1) {
+ 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() }));
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
- }
+ };
@observable _showSidebar = false;
- @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._showSidebar ? true : false; }
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ }
@action
toggleSidebar = (preview: boolean = false) => {
- const prevWidth = this.sidebarWidth();
+ 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._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._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);
- }
+ this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth);
+ };
sidebarDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), false);
- }
+ const batch = UndoManager.StartBatch('sidebar');
+ setupMoveUpEvents(
+ this,
+ e,
+ this.sidebarMove,
+ (e, movement, isClick) => !isClick && batch.end(),
+ () => {
+ this.toggleSidebar();
+ batch.end();
+ },
+ true
+ );
+ };
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%";
+ const localDelta = this.props
+ .ScreenToLocalTransform()
+ .scale(this.props.NativeDimScaling?.() || 1)
+ .transformDirection(delta[0], delta[1]);
+ const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100;
+ const width = this.layoutDoc[WidthSym]() + localDelta[0];
+ this.layoutDoc._sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
+ this.layoutDoc.width = width;
+ this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
- }
+ };
+
+ @undoBatch
+ deleteAnnotation = (anchor: Doc) => {
+ LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]);
+ // const docAnnotations = DocListCast(this.props.dataDoc[this.props.fieldKey]);
+ // this.props.dataDoc[this.props.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
+ // AnchorMenu.Instance.fadeOut(true);
+ this.props.select(false);
+ };
+
+ @undoBatch
+ pinToPres = (anchor: Doc) => this.props.pinToPres(anchor);
+
+ @undoBatch
+ makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin);
+
+ isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin);
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
+ const editor = this._editorView!;
+ const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
+ let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
+ while (target && !target.dataset?.targethrefs) target = target.parentElement;
+ if (target) {
+ const hrefs = (target.dataset?.targethrefs as string)
+ ?.trim()
+ .split(' ')
+ .filter(h => h);
+ const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0];
+ e.persist();
+ anchorDoc &&
+ DocServer.GetRefField(anchorDoc).then(
+ action(anchor => {
+ AnchorMenu.Instance.Status = 'annotation';
+ AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
+ AnchorMenu.Instance.Pinned = false;
+ AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
+ AnchorMenu.Instance.MakePushpin = () => this.makePushpin(anchor as Doc);
+ AnchorMenu.Instance.IsPushpin = () => this.isPushpin(anchor as Doc);
+ AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ })
+ );
+ e.stopPropagation();
+ return;
+ }
+
const changeItems: ContextMenuProps[] = [];
changeItems.push({
- description: "plain", event: undoBatch(() => {
+ description: 'plain',
+ event: undoBatch(() => {
Doc.setNativeView(this.rootDoc);
this.layoutDoc.autoHeightMargins = undefined;
- }), icon: "eye"
+ }),
+ icon: 'eye',
});
changeItems.push({
- description: "metadata", event: undoBatch(() => {
+ 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);
- }), icon: "eye"
+ this.rootDoc.layoutKey = 'layout_meta';
+ setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._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(() => {
+ description: StrCast(note.title),
+ event: undoBatch(() => {
this.layoutDoc.autoHeightMargins = undefined;
Doc.setNativeView(this.rootDoc);
DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
- }), icon: icon
+ }),
+ icon: icon,
});
});
- !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" });
const highlighting: ContextMenuProps[] = [];
- const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others"];
- const expertHighlighting = [...noviceHighlighting, "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"];
- (Doc.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
+ const noviceHighlighting = ['Audio Tags', 'My Text', 'Text from Others', 'Bold Text'];
+ 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._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+ description: (FormattedTextBox._globalHighlights.indexOf(option) === -1 ? 'Highlight ' : 'Unhighlight ') + option,
+ event: () => {
e.stopPropagation();
- if (FormattedTextBox._highlights.indexOf(option) === -1) {
- FormattedTextBox._highlights.push(option);
+ if (FormattedTextBox._globalHighlights.indexOf(option) === -1) {
+ FormattedTextBox._globalHighlights.push(option);
} else {
- FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1);
}
+ runInAction(() => (this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join('')));
this.updateHighlights();
- }, icon: "expand-arrows-alt"
- }));
+ },
+ icon: 'expand-arrows-alt',
+ })
+ );
const uicontrols: ContextMenuProps[] = [];
- !Doc.UserDoc().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.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" });
- uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" });
- !Doc.UserDoc().noviceMode && uicontrols.push({
- description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto =>
- proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt"
- });
- cm.addItem({ description: "UI Controls...", subitems: uicontrols, icon: "asterisk" });
+ !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.layoutDoc._showAudio ? 'Hide' : 'Show'} Dictation Icon`, event: () => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: 'expand-arrows-alt' });
+ uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
+ !Doc.noviceMode &&
+ uicontrols.push({
+ description: 'Broadcast Message',
+ event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)),
+ icon: 'expand-arrows-alt',
+ });
+ cm.addItem({ description: 'UI Controls...', subitems: uicontrols, icon: 'asterisk' });
- const appearance = cm.findByDescription("Appearance...");
- const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
- appearanceItems.push({ description: "Change Perspective...", noexpand: true, subitems: changeItems, icon: "external-link-alt" });
+ const appearance = cm.findByDescription('Appearance...');
+ const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : [];
+ appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });
// this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" });
- !Doc.UserDoc().noviceMode && appearanceItems.push({
- description: "Make Default Layout", event: () => {
- if (!this.layoutDoc.isTemplateDoc) {
- const title = StrCast(this.rootDoc.title);
- this.rootDoc.title = "text";
- this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
- } else if (!this.rootDoc.isTemplateDoc) {
- const title = StrCast(this.rootDoc.title);
- this.rootDoc.title = "text";
- this.rootDoc.layout = this.layoutDoc.layout as string;
- this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
- this.rootDoc.isTemplateDoc = false;
- this.rootDoc.isTemplateForField = "";
- this.rootDoc.layoutKey = "layout";
- this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
- setTimeout(() => {
- this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // 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);
- this.rootDoc.backgroundColor = Cast(this.layoutDoc.backgroundColor, "string", null);
- }, 10);
- }
- Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc);
- Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.rootDoc);
- }, icon: "eye"
- });
- cm.addItem({ description: "Appearance...", subitems: appearanceItems, icon: "eye" });
+ !Doc.noviceMode &&
+ appearanceItems.push({
+ description: 'Make Default Layout',
+ event: () => {
+ if (!this.layoutDoc.isTemplateDoc) {
+ const title = StrCast(this.rootDoc.title);
+ this.rootDoc.title = 'text';
+ MakeTemplate(this.rootDoc, true, title);
+ } else if (!this.rootDoc.isTemplateDoc) {
+ const title = StrCast(this.rootDoc.title);
+ this.rootDoc.title = 'text';
+ this.rootDoc.layout = this.layoutDoc.layout as string;
+ this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
+ this.rootDoc.isTemplateDoc = false;
+ this.rootDoc.isTemplateForField = '';
+ this.rootDoc.layoutKey = 'layout';
+ MakeTemplate(this.rootDoc, true, title);
+ setTimeout(() => {
+ this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // 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);
+ this.rootDoc.backgroundColor = Cast(this.layoutDoc.backgroundColor, 'string', null);
+ }, 10);
+ }
+ Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc);
+ Doc.AddDocToList(Cast(Doc.UserDoc()['template-notes'], Doc, null), 'data', this.rootDoc);
+ },
+ icon: 'eye',
+ });
+ cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
- 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" });
- !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "eye" });
+ 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' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
- }
+ };
breakupDictation = () => {
if (this._editorView && this._recording) {
@@ -675,12 +834,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const state = this._editorView.state;
const to = state.selection.to;
const updated = TextSelection.create(state.doc, to, to);
- this._editorView.dispatch(state.tr.setSelection(updated).insertText("\n", to));
+ this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to));
if (this._recording) {
this.recordDictation();
}
}
- }
+ };
recordDictation = () => {
DictationManager.Controls.listen({
interimHandler: this.setDictationContent,
@@ -690,26 +849,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
DictationManager.Controls.stop();
}
});
- }
+ };
stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort);
setDictationContent = (value: string) => {
if (this._editorView && this._recordingStart) {
if (this._break) {
- const textanchor = Docs.Create.TextanchorDocument({ title: "dictation anchor" });
+ const textanchor = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
this.addDocument(textanchor);
const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement();
link && (Doc.GetProto(link).isDictation = true);
if (!link) return;
const audioanchor = Cast(link.anchor2, Doc, null);
if (!audioanchor) return;
- audioanchor.backgroundColor = "tan";
+ audioanchor.backgroundColor = 'tan';
const audiotag = this._editorView.state.schema.nodes.audiotag.create({
timeCode: NumCast(audioanchor._timecodeToShow),
audioId: audioanchor[Id],
- textId: textanchor[Id]
+ textId: textanchor[Id],
});
- Doc.GetProto(textanchor).title = "dictation:" + audiotag.attrs.timeCode;
+ Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode;
const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
@@ -719,8 +878,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const tr = this._editorView.state.tr.insertText(value);
this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView());
}
- }
+ };
+ // TODO: nda -- Look at how link anchors are added
makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) {
const state = this._editorView?.state;
if (state) {
@@ -728,7 +888,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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), unrendered: true });
+ const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -739,7 +899,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
tr = tr.addMark(pos, pos + node.nodeSize, link);
}
});
- this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
+ this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
return anchor;
@@ -750,8 +910,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
scrollFocus = (textAnchor: Doc, smooth: boolean) => {
- if (DocListCast(this.Document[this.fieldKey + "-sidebar"]).includes(textAnchor) && !this.SidebarShown) {
+ let didToggle = false;
+ if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
this.toggleSidebar(!smooth);
+ didToggle = true;
}
const textAnchorId = textAnchor[Id];
const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
@@ -780,10 +942,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor);
- return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, "")) ? { node, start: 0 } : undefined;
+ return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, '')) ? { node, start: 0 } : undefined;
};
let start = 0;
+ this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
if (this._editorView && textAnchorId) {
const editor = this._editorView;
const ret = findAnchorFrag(editor.state.doc.content, editor);
@@ -797,57 +960,72 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
- addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: "yellow", transform: "scale(3)", "transform-origin": "left bottom" });
- setTimeout(() => this._focusSpeed = undefined, this._focusSpeed);
+ 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 this._focusSpeed;
- }
+ 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.
// 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;
- }
+ };
+ @computed get contentScaling() {
+ return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.NativeDimScaling?.() || 1 : 1;
+ }
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._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
- this._disposers.autoHeight = reaction(() => this.autoHeight, autoHeight => autoHeight && this.tryUpdateScrollHeight());
- this._disposers.scrollHeight = reaction(() => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }),
+ this._disposers.autoHeight = reaction(
+ () => this.autoHeight,
+ autoHeight => autoHeight && 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);
- }, { fireImmediately: true }
+ },
+ { fireImmediately: true }
);
- this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on
+ 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 }) => {
- autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight));
- }, { fireImmediately: true });
- this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+ const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
+ if (autoHeight && newHeight && newHeight !== this.rootDoc.height) {
+ 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
newLinks => {
this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
this._cachedLinks = newLinks;
- });
+ }
+ );
this._disposers.buttonBar = reaction(
() => DocumentButtonBar.Instance,
instance => {
if (instance) {
this.pullFromGoogleDoc(this.checkState);
- this.dataDoc[GoogleRef] && this.dataDoc.googleDocUnchanged && runInAction(() => instance.isAnimatingFetch = true);
+ this.dataDoc[GoogleRef] && this.dataDoc.googleDocUnchanged && runInAction(() => (instance.isAnimatingFetch = true));
}
}
);
this._disposers.editorState = reaction(
() => {
- const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined :
- this.dataDoc?.[this.props.fieldKey + "-noTemplate"] || !this.layoutDoc[this.props.fieldKey] ?
- this.dataDoc : this.layoutDoc;
+ const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : this.dataDoc?.[this.props.fieldKey + '-noTemplate'] || !this.layoutDoc[this.props.fieldKey] ? this.dataDoc : this.layoutDoc;
return !whichDoc ? undefined : { data: Cast(whichDoc[this.props.fieldKey], RichTextField, null), str: StrCast(whichDoc[this.props.fieldKey]) };
},
incomingValue => {
@@ -862,7 +1040,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue.str)));
}
}
- },
+ }
);
this._disposers.pullDoc = reaction(
() => this.props.Document[Pulls],
@@ -883,33 +1061,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
);
- this._disposers.search = reaction(() => Doc.IsSearchMatch(this.rootDoc),
- search => search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms(),
- { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false });
+ this._disposers.search = reaction(
+ () => Doc.IsSearchMatch(this.rootDoc),
+ search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()),
+ { fireImmediately: Doc.IsSearchMatchUnmemoized(this.rootDoc) ? true : false }
+ );
- this._disposers.selected = reaction(() => this.props.isSelected(),
+ this._disposers.selected = reaction(
+ () => this.props.isSelected(),
action(selected => {
+ this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join('') : '';
if (RichTextMenu.Instance?.view === this._editorView && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
if (this._editorView && selected) {
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
+ this.autoLink();
}
- }), { fireImmediately: true });
+ }),
+ { fireImmediately: true }
+ );
if (!this.props.dontRegisterView) {
- this._disposers.record = reaction(() => this._recording,
+ this._disposers.record = reaction(
+ () => this._recording,
() => {
this.stopDictation(true);
if (this._recording) {
this.recordDictation();
}
- },
+ }
);
if (this._recording) setTimeout(this.recordDictation);
}
- var quickScroll: string | undefined = "";
- this._disposers.scroll = reaction(() => NumCast(this.layoutDoc._scrollTop),
+ var quickScroll: string | undefined = '';
+ this._disposers.scroll = reaction(
+ () => NumCast(this.layoutDoc._scrollTop),
pos => {
if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) {
const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
@@ -922,17 +1109,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._scrollRef.current.scrollTo({ top: pos });
}
}
- }, { fireImmediately: true }
+ },
+ { fireImmediately: true }
);
quickScroll = undefined;
- setTimeout(this.tryUpdateScrollHeight, 10);
+ this.tryUpdateScrollHeight();
}
pushToGoogleDoc = async () => {
this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
const modes = GoogleApiClientUtils.Docs.WriteMode;
let mode = modes.Replace;
- let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], 'string');
if (!reference) {
mode = modes.Insert;
reference = { title: StrCast(this.dataDoc.title) };
@@ -942,7 +1130,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
response && (this.dataDoc[GoogleRef] = response.documentId);
- const pushSuccess = response !== undefined && !("errors" in response);
+ const pushSuccess = response !== undefined && !('errors' in response);
dataDoc.googleDocUnchanged = pushSuccess;
DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
}
@@ -951,15 +1139,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (exportState && reference) {
const content: GoogleApiClientUtils.Docs.Content = {
text: exportState.text,
- requests: []
+ requests: [],
};
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
};
- UndoManager.AddEvent({ undo, redo, prop: "" });
+ UndoManager.AddEvent({ undo, redo, prop: '' });
redo();
});
- }
+ };
pullFromGoogleDoc = async (handler: PullHandler) => {
const dataDoc = this.dataDoc;
@@ -969,7 +1157,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
}
exportState && UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
- }
+ };
updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
let pullSuccess = false;
@@ -984,13 +1172,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}, 0);
dataDoc.title = exportState.title;
- this.dataDoc["title-custom"] = true;
+ this.dataDoc['title-custom'] = true;
dataDoc.googleDocUnchanged = true;
} else {
delete dataDoc[GoogleRef];
}
DocumentButtonBar.Instance.startPullOutcome(pullSuccess);
- }
+ };
checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
if (exportState && this._editorView) {
@@ -1000,56 +1188,61 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
dataDoc.googleDocUnchanged = unchanged;
DocumentButtonBar.Instance.setPullState(unchanged);
}
- }
+ };
clipboardTextSerializer = (slice: Slice): string => {
- let text = "", separated = true;
- const from = 0, to = slice.content.size;
- slice.content.nodesBetween(from, to, (node, pos) => {
- if (node.isText) {
- text += node.text!.slice(Math.max(from, pos) - pos, to - pos);
- separated = false;
- } else if (!separated && node.isBlock) {
- text += "\n";
- separated = true;
- } else if (node.type.name === "hard_break") {
- text += "\n";
- }
- }, 0);
+ let text = '',
+ separated = true;
+ const from = 0,
+ to = slice.content.size;
+ slice.content.nodesBetween(
+ from,
+ to,
+ (node, pos) => {
+ if (node.isText) {
+ text += node.text!.slice(Math.max(from, pos) - pos, to - pos);
+ separated = false;
+ } else if (!separated && node.isBlock) {
+ text += '\n';
+ separated = true;
+ } else if (node.type.name === 'hard_break') {
+ text += '\n';
+ }
+ },
+ 0
+ );
return text;
- }
+ };
handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
const cbe = event as ClipboardEvent;
- const pdfDocId = cbe.clipboardData?.getData("dash/pdfOrigin");
- const pdfRegionId = cbe.clipboardData?.getData("dash/pdfRegion");
+ const pdfDocId = cbe.clipboardData?.getData('dash/pdfOrigin');
+ const pdfRegionId = cbe.clipboardData?.getData('dash/pdfRegion');
return pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, slice) ? true : false;
- }
+ };
addPdfReference = (pdfDocId: string, pdfRegionId: string, slice?: Slice) => {
const view = this._editorView!;
if (pdfDocId && pdfRegionId) {
DocServer.GetRefField(pdfDocId).then(pdfDoc => {
DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
- if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
+ if (pdfDoc instanceof Doc && pdfRegion instanceof Doc) {
setTimeout(async () => {
const targetField = Doc.LayoutFieldKey(pdfDoc);
- const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations
+ const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + '-annotations']); // bcz: better to have the PDF's view handle updating its own annotations
if (targetAnnotations) targetAnnotations.push(pdfRegion);
- else Doc.AddDocToList(pdfDoc[DataSym], targetField + "-annotations", pdfRegion);
+ else Doc.AddDocToList(pdfDoc[DataSym], targetField + '-annotations', pdfRegion);
});
- const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, "PDF pasted");
+ const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, 'PDF pasted');
if (link) {
const linkId = link[Id];
- const quote = view.state.schema.nodes.blockquote.create();
- quote.content = addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+ const quote = view.state.schema.nodes.blockquote.create({ content: addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)) });
const newSlice = new Slice(Fragment.from(quote), slice?.openStart || 0, slice?.openEnd || 0);
if (slice) {
- view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+ view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
} else {
selectAll(view.state, (tx: Transaction) => view.dispatch(tx.replaceSelection(newSlice).scrollIntoView()));
-
}
}
}
@@ -1059,31 +1252,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
return false;
-
function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
const nodes: Node[] = [];
frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
-
function addLinkMark(node: Node, title: string, linkId: string) {
if (!node.isText) {
const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
return node.copy(content);
}
const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type.name === "link");
+ const linkIndex = marks.findIndex(mark => mark.type.name === 'link');
const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }];
- const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true });
+ const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: 'add:right', title, docref: true });
marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
return node.mark(marks);
}
- }
+ };
isActiveTab(el: Element | null | undefined) {
while (el && el !== document.body) {
- if (getComputedStyle(el).display === "none") return false;
+ if (getComputedStyle(el).display === 'none') return false;
el = el.parentNode as any;
}
return true;
@@ -1095,9 +1286,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
view(newView) {
runInAction(() => self.props.isSelected(true) && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView));
return new RichTextMenuPlugin({ editorProps: this.props });
- }
+ },
});
}
+ _didScroll = false;
setupEditor(config: any, fieldKey: string) {
const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]);
const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
@@ -1106,13 +1298,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._editorView?.destroy();
this._editorView = new EditorView(this.ProseRef, {
state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
- handleScrollToSelection: (editorView) => {
+ handleScrollToSelection: editorView => {
const docPos = editorView.coordsAtPos(editorView.state.selection.to);
const viewRect = self._ref.current!.getBoundingClientRect();
const scrollRef = self._scrollRef.current;
const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined;
const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined;
- if ((topOff || botOff) && scrollRef) {
+ if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) {
const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale;
if (this._focusSpeed !== undefined) {
@@ -1120,18 +1312,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} else {
scrollRef.scrollTo({ top: scrollPos });
}
+ this._didScroll = true;
}
return true;
},
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
- dashComment(node: any, view: any, getPos: any) { return new DashDocCommentView(node, view, getPos); },
- dashDoc(node: any, view: any, getPos: any) { return new DashDocView(node, view, getPos, self); },
- dashField(node: any, view: any, getPos: any) { return new DashFieldView(node, view, getPos, self); },
- equation(node: any, view: any, getPos: any) { return new EquationView(node, view, getPos, self); },
- summary(node: any, view: any, getPos: any) { return new SummaryView(node, view, getPos); },
- ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); },
- footnote(node: any, view: any, getPos: any) { return new FootnoteView(node, view, getPos); }
+ dashComment(node: any, view: any, getPos: any) {
+ return new DashDocCommentView(node, view, getPos);
+ },
+ dashDoc(node: any, view: any, getPos: any) {
+ return new DashDocView(node, view, getPos, self);
+ },
+ dashField(node: any, view: any, getPos: any) {
+ return new DashFieldView(node, view, getPos, self);
+ },
+ equation(node: any, view: any, getPos: any) {
+ return new EquationView(node, view, getPos, self);
+ },
+ summary(node: any, view: any, getPos: any) {
+ return new SummaryView(node, view, getPos);
+ },
+ //ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); },
+ footnote(node: any, view: any, getPos: any) {
+ return new FootnoteView(node, view, getPos);
+ },
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
@@ -1142,9 +1347,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (startupText) {
dispatch(state.tr.insertText(startupText));
}
- const textAlign = StrCast(this.dataDoc["text-align"], StrCast(Doc.UserDoc().textAlign, "left"));
- if (textAlign !== "left") {
- selectAll(this._editorView.state, (tr) => {
+ 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 })));
});
}
@@ -1154,33 +1359,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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)) {
- FormattedTextBox.SelectOnLoad = "";
+ const selLoadChar = FormattedTextBox.SelectOnLoadChar;
+ FormattedTextBox.SelectOnLoad = '';
this.props.select(false);
- if (FormattedTextBox.SelectOnLoadChar && this._editorView) {
+ if (selLoadChar && this._editorView) {
const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
- const tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks);
+ const tr = this._editorView.state.tr
+ .setStoredMarks(storedMarks)
+ .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
+ .setStoredMarks(storedMarks);
this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
- FormattedTextBox.SelectOnLoadChar = "";
- } else if (curText && !FormattedTextBox.DontSelectInitialText) {
- selectAll(this._editorView!.state, this._editorView?.dispatch);
+ } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) {
+ selectAll(this._editorView.state, this._editorView?.dispatch);
this.startUndoTypingBatch();
+ } else if (this._editorView) {
+ this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
FormattedTextBox.DontSelectInitialText = false;
}
selectOnLoad && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) {
- this._editorView.state.storedMarks = [...(this._editorView.state.storedMarks ?? []),
- schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }),
- ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
- ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []),
- ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []),
- ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
- ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
- ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])];
+ this._editorView.state.storedMarks = [
+ ...(this._editorView.state.storedMarks ?? []),
+ schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }),
+ ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
+ ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
+ ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
+ ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
+ ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
+ ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
+ ];
}
}
@@ -1191,12 +1403,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.unhighlightSearchTerms();
this._editorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined);
- FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none');
}
onPointerDown = (e: React.PointerEvent): void => {
+ if ((e.nativeEvent as any).handledByInnerReactInstance) {
+ return e.stopPropagation();
+ } 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.
- if ((e.target as any).tagName === "AUDIOTAG") {
+ if ((e.target as any).tagName === 'AUDIOTAG') {
e.preventDefault();
e.stopPropagation();
const timecode = Number((e.target as any)?.dataset?.timecode);
@@ -1207,10 +1424,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const func = () => {
const docView = DocumentManager.Instance.getDocumentView(audiodoc);
if (!docView) {
- this.props.addDocTab(audiodoc, "add:bottom");
+ this.props.addDocTab(audiodoc, 'add:bottom');
setTimeout(func);
- }
- else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, "number", null)); // bcz: would be nice to find the next audio tag in the doc and play until that
+ } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that
};
func();
}
@@ -1226,50 +1442,48 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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) { // stop propagation if not in sidebar
+ if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
+ // stop propagation if not in sidebar
// bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView,
// 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);
+ 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);
- }
+ document.removeEventListener('pointerup', this.onSelectEnd);
+ document.removeEventListener('pointermove', this.onSelectMove);
+ };
onPointerUp = (e: React.PointerEvent): void => {
if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate) this.setupAnchorMenu();
if (!this._downEvent) return;
this._downEvent = false;
- if ((e.nativeEvent as any).formattedHandled) {
- console.log("handled");
- }
- if (!(e.nativeEvent as any).formattedHandled && this.props.isContentActive(true)) {
+ if (this.props.isContentActive(true)) {
const editor = this._editorView!;
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))));
let target = (e.target as any).parentElement; // hrefs are stored on the database 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);
+ FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
}
- (e.nativeEvent as any).formattedHandled = true;
- if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
+ if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
e.stopPropagation();
}
- }
+ };
@action
onDoubleClick = (e: React.MouseEvent): void => {
FormattedTextBoxComment.textBox = this;
if (e.button === 0 && this.props.isSelected(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar
- e.stopPropagation(); // if the text box is selected, then it consumes all click events
+ if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
+ // stop propagation if not in sidebar
+ e.stopPropagation(); // if the text box is selected, then it consumes all click events
}
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
@@ -1277,31 +1491,34 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
FormattedTextBoxComment.Hide();
- (e.nativeEvent as any).formattedHandled = true;
-
if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
e.stopPropagation();
}
- }
+ };
setFocus = () => {
const pos = this._editorView?.state.selection.$from.pos || 1;
(this.ProseRef?.children?.[0] as any).focus();
setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
- }
+ };
@action
onFocused = (e: React.FocusEvent): void => {
//applyDevTools.applyDevTools(this._editorView);
FormattedTextBox.Focused = this;
- this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
- }
+ this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
+ };
@observable public static Focused: FormattedTextBox | undefined;
onClick = (e: React.MouseEvent): void => {
+ if ((e.nativeEvent as any).handledByInnerReactInstance) {
+ e.stopPropagation();
+ return;
+ }
if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
this._forceDownNode = undefined;
return;
}
- if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
+ if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) {
+ // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
@@ -1311,29 +1528,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (!node && this.ProseRef) {
const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div
const boundsRect = lastNode?.getBoundingClientRect();
- if (e.clientX > boundsRect.left && e.clientX < boundsRect.right &&
- e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
+ if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) {
+ // if we clicked below the last prosemirror div, then set the selection to be the end of the document
this._editorView?.focus();
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
}
- } else if ([this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node?.type) &&
- node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) {
+ } else if (node && [this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node.type) && node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) {
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos)));
}
}
- if ((e.nativeEvent as any).formattedHandled) {
- e.stopPropagation();
- return;
- }
- if (this.props.isSelected(true)) { // if text box is selected, then it consumes all click events
- (e.nativeEvent as any).formattedHandled = true;
+ 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;
this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node;
- }
+ };
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) {
this._forceUncollapse = false;
@@ -1356,39 +1569,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos);
if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) {
if (!highlightOnly) {
- if (selectOrderedList || (!collapse && listNode.attrs.visibility)) {
+ if (selectOrderedList) {
this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!)));
- } else if (!listNode.attrs.visibility || downNode === listNode) {
+ } else {
const tr = this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility });
this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, clickPos.pos)));
}
}
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'lightgray' });
}
}
}
- onMouseUp = (e: React.MouseEvent): void => {
- e.stopPropagation();
-
- const view = this._editorView as any;
- // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
- // are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
- if (view.mouseDown) {
- const originalUpHandler = view.mouseDown.up;
- view.root.removeEventListener("mouseup", originalUpHandler);
- view.mouseDown.up = (e: MouseEvent) => {
- if (!(e as any).formattedHandled) {
- originalUpHandler(e);
- (e as any).formattedHandled = true;
- } else {
- console.log("prehandled");
- }
- };
- view.root.addEventListener("mouseup", view.mouseDown.up);
- }
- }
startUndoTypingBatch() {
- !this._undoTyping && (this._undoTyping = UndoManager.StartBatch("undoTyping"));
+ !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping'));
}
public endUndoTypingBatch() {
const wasUndoing = this._undoTyping;
@@ -1399,40 +1592,53 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
onBlur = (e: any) => {
+ if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
+ this.autoLink();
FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
- FormattedTextBox._hadSelection = window.getSelection()?.toString() !== "";
+ FormattedTextBox._hadSelection = window.getSelection()?.toString() !== '';
this.endUndoTypingBatch();
FormattedTextBox.LiveTextUndo?.end();
FormattedTextBox.LiveTextUndo = undefined;
const state = this._editorView!.state;
- const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
- if (this.layoutDoc.sidebarViewType === "translation" && !this.fieldKey.includes("translation") && curText.endsWith(" ") && curText !== this._lastText) {
+ const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+ if (this.layoutDoc.sidebarViewType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) {
try {
- translateGoogleApi(curText, { from: "en", to: "es", }).then((result1: any) => {
- setTimeout(() => translateGoogleApi(result1[0], { from: "es", to: "en", }).then((result: any) => {
- this.dataDoc[this.fieldKey + "-translation"] = result1 + "\r\n\r\n" + result[0];
- }), 1000);
+ translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => {
+ setTimeout(
+ () =>
+ translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => {
+ this.dataDoc[this.fieldKey + '-translation'] = result1 + '\r\n\r\n' + result[0];
+ }),
+ 1000
+ );
});
- } catch (e: any) { console.log(e.message); }
+ } catch (e: any) {
+ console.log(e.message);
+ }
this._lastText = curText;
}
- }
- onKeyDown = (e: React.KeyboardEvent) => {
- // single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container)
- if (this.rootDoc._singleLine && ((e.key === "Backspace" && !this.dataDoc[this.fieldKey]?.Text) || ["Tab", "Enter"].includes(e.key))) {
- return;
+ if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc['title-custom']) {
+ UndoManager.RunInBatch(() => {
+ this.dataDoc['title-custom'] = true;
+ this.dataDoc.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');
}
+ };
+
+ onKeyDown = (e: React.KeyboardEvent) => {
if (e.altKey) {
e.preventDefault();
return;
}
const state = this._editorView!.state;
- if (!state.selection.empty && e.key === "%") {
+ if (!state.selection.empty && e.key === '%') {
this._rules!.EnteringStyle = true;
e.preventDefault();
e.stopPropagation();
@@ -1443,34 +1649,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._rules!.EnteringStyle = false;
}
e.stopPropagation();
- for (var i = state.selection.from; i < state.selection.to; i++) {
+ for (var i = state.selection.from; i <= state.selection.to; i++) {
const node = state.doc.resolve(i);
- if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark &&
- mark.attrs.userid !== Doc.CurrentUserEmail) &&
- [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
+ if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
e.preventDefault();
}
}
switch (e.key) {
- case "Escape":
+ case 'Escape':
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
(document.activeElement as any).blur?.();
SelectionManager.DeselectAll();
RichTextMenu.Instance.updateMenu(undefined, undefined, undefined);
return;
- case "Enter": this.insertTime();
- case "Tab": e.preventDefault(); break;
- default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
- case " ":
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({}))
- .addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
+ case 'Enter':
+ this.insertTime();
+ case 'Tab':
+ e.preventDefault();
+ break;
+ default:
+ if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
+ case ' ':
+ [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) &&
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
this.startUndoTypingBatch();
- }
+ };
ondrop = (e: React.DragEvent) => {
this._editorView!.dispatch(updateBullets(this._editorView!.state.tr, this._editorView!.state.schema));
e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash.
- }
+ };
onScroll = (e: React.UIEvent) => {
if (!LinkDocPreview.LinkInfo && this._scrollRef.current) {
if (!this.props.dontSelectOnLoad) {
@@ -1479,142 +1687,164 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._ignoreScroll = false;
}
}
- }
+ };
tryUpdateScrollHeight = () => {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (children) {
- const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
+ const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')), margins);
const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !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;
- if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+ if (this.props.setHeight && scrollHeight && this.props.renderDepth && !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);
+ if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
} else {
setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived...
}
}
}
- }
- fitToBox = () => this.props.Document._fitToBox;
- sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
- sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ };
+ fitContentsToBox = () => this.props.Document._fitContentsToBox;
+ sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
// console.log("printting allSideBarDocs");
// console.log(this.allSidebarDocs);
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();
- sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.scaling?.() || 1), 0).scale(1 / NumCast(this.layoutDoc._viewScale, 1));
+ setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '-height'] = height);
+ sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.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));
@computed get audioHandle() {
- return <div className="formattedTextBox-dictation" onClick={action(e => this._recording = !this._recording)} >
- <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? "red" : "blue", transitionDelay: "0.6s", opacity: this._recording ? 1 : 0.25, }} icon={"microphone"} size="sm" />
- </div>;
+ return (
+ <div className="formattedTextBox-dictation" onClick={action(e => (this._recording = !this._recording))}>
+ <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? 'red' : 'blue', transitionDelay: '0.6s', opacity: this._recording ? 1 : 0.25 }} icon={'microphone'} size="sm" />
+ </div>
+ );
}
@computed get sidebarHandle() {
TraceMobx();
const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length;
const color = !annotated ? Colors.WHITE : Colors.BLACK;
- const backgroundColor = !annotated ? this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ":annotated" : ""));
+ const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.WidgetColor + (annotated ? ':annotated' : ''));
- return (!annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging())) ? (null) :
- <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown}
+ return !annotated && (!this.props.isContentActive() || SnappingManager.GetIsDragging()) ? null : (
+ <div
+ className="formattedTextBox-sidebar-handle"
+ onPointerDown={this.sidebarDown}
style={{
- left: `max(0px, calc(100% - ${this.sidebarWidthPercent} - 17px))`,
backgroundColor: backgroundColor,
color: color,
- opacity: annotated ? 1 : undefined
- }} >
- <FontAwesomeIcon icon={"comment-alt"} />
- </div>;
+ opacity: annotated ? 1 : undefined,
+ }}>
+ <FontAwesomeIcon icon={'comment-alt'} />
+ </div>
+ );
}
@computed get sidebarCollection() {
const renderComponent = (tag: string) => {
- const ComponentTag = tag === "freeform" ? CollectionFreeFormView : tag === "translation" ? FormattedTextBox : CollectionStackingView;
- return ComponentTag === CollectionStackingView ?
- <SidebarAnnos ref={this._sidebarRef}
+ const ComponentTag = tag === 'freeform' ? CollectionFreeFormView : tag === 'tree' ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
+ return ComponentTag === CollectionStackingView ? (
+ <SidebarAnnos
+ ref={this._sidebarRef}
{...this.props}
fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- // usePanelWidth={true}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
showSidebar={this.SidebarShown}
PanelWidth={this.sidebarWidth}
setHeight={this.setSidebarHeight}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
- /> :
- <ComponentTag
- {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- NativeWidth={returnZero}
- NativeHeight={returnZero}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.sidebarWidth}
- xPadding={0}
- yPadding={0}
- scaleField={this.SidebarKey + "-scale"}
- isAnnotationOverlay={false}
- select={emptyFunction}
- scaling={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}
- fitContentsToDoc={this.fitToBox}
- noSidebar={true}
- fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
+ />
+ ) : (
+ <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+ //@ts-ignore
+ <ComponentTag
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.sidebarWidth}
+ xPadding={0}
+ yPadding={0}
+ scaleField={this.SidebarKey + '-scale'}
+ isAnnotationOverlay={false}
+ select={emptyFunction}
+ 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`}
+ />
+ </div>
+ );
};
- return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
- style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
- </div>;
+ return (
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ </div>
+ );
}
render() {
TraceMobx();
- const selected = this.props.isSelected();
- const active = this.props.isContentActive();
- const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
- const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
- const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false);
+ const active = this.props.isContentActive() || this.props.isSelected();
+ const selected = active;
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 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 styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
- return (styleFromString?.height === "0px" ? (null) :
- <div className="formattedTextBox-cont"
+ 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 styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
+ return styleFromString?.height === '0px' ? null : (
+ <div
+ className="formattedTextBox-cont"
onWheel={e => this.props.isContentActive() && e.stopPropagation()}
style={{
transform: this.props.dontScale ? undefined : `scale(${scale})`,
- transformOrigin: this.props.dontScale ? undefined : "top left",
+ transformOrigin: this.props.dontScale ? undefined : 'top left',
width: this.props.dontScale ? undefined : `${100 / scale}%`,
height: this.props.dontScale ? undefined : `${100 / scale}%`,
// overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
- ...styleFromString
+ ...styleFromString,
}}>
- <div className={`formattedTextBox-cont`} ref={this._ref}
+ <div
+ className={`formattedTextBox-cont`}
+ ref={this._ref}
style={{
- overflow: this.autoHeight ? "hidden" : undefined,
- height: this.props.height || (this.autoHeight && this.props.renderDepth ? "max-content" : undefined),
+ overflow: this.autoHeight ? 'hidden' : undefined,
+ height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
- fontWeight: Cast(this.layoutDoc._fontWeight, "string", null) as any,
- fontFamily: StrCast(this.layoutDoc._fontFamily, "inherit"),
- pointerEvents: interactive ? undefined : "none",
+ fontWeight: Cast(this.layoutDoc._fontWeight, 'string', null) as any,
+ fontFamily: StrCast(this.layoutDoc._fontFamily, 'inherit'),
+ pointerEvents: interactive ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyDown}
@@ -1624,32 +1854,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
- onMouseUp={this.onMouseUp}
- onDoubleClick={this.onDoubleClick}
- >
- <div className={`formattedTextBox-outer${selected ? "-selected" : ""}`} ref={this._scrollRef}
+ onDoubleClick={this.onDoubleClick}>
+ <div
+ className={`formattedTextBox-outer${selected ? '-selected' : ''}`}
+ ref={this._scrollRef}
style={{
- width: this.props.dontSelectOnLoad ? "100%" : `calc(100% - ${this.sidebarWidthPercent})`,
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? "none" : undefined,
- overflow: this.layoutDoc._singleLine ? "hidden" : undefined,
+ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
+ pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
+ overflow: this.layoutDoc._singleLine ? 'hidden' : undefined,
}}
- onScroll={this.onScroll} onDrop={this.ondrop} >
- <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
+ onScroll={this.onScroll}
+ onDrop={this.ondrop}>
+ <div
+ className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`}
+ ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
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() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined,
}}
/>
</div>
- {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === "0%" ? (null) : this.sidebarCollection}
- {(this.props.noSidebar || this.Document._noSidebar) || this.props.dontSelectOnLoad || this.Document._singleLine ? (null) : this.sidebarHandle}
- {!this.layoutDoc._showAudio ? (null) : this.audioHandle}
+ {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.layoutDoc._showAudio ? null : this.audioHandle}
</div>
- </div >
+ </div>
);
}
}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 1fde6e5f0..bdf59863b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -1,17 +1,25 @@
-import { Mark, ResolvedPos } from "prosemirror-model";
-import { EditorState } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import { Doc } from "../../../../fields/Doc";
-import { LinkDocPreview } from "../LinkDocPreview";
-import { FormattedTextBox } from "./FormattedTextBox";
+import { Mark, ResolvedPos } from 'prosemirror-model';
+import { EditorState } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
+import { Doc } from '../../../../fields/Doc';
+import { DocServer } from '../../../DocServer';
+import { LinkDocPreview } from '../LinkDocPreview';
+import { FormattedTextBox } from './FormattedTextBox';
import './FormattedTextBoxComment.scss';
-import { schema } from "./schema_rts";
+import { schema } from './schema_rts';
-export function findOtherUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail); }
-export function findUserMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.attrs.userid); }
-export function findLinkMark(marks: Mark[]): Mark | undefined { return marks.find(m => m.type === schema.marks.linkAnchor); }
-export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
- let before = 0, nbef = rpos.nodeBefore;
+export function findOtherUserMark(marks: readonly Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+}
+export function findUserMark(marks: readonly Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid);
+}
+export function findLinkMark(marks: readonly Mark[]): Mark | undefined {
+ return marks.find(m => m.type === schema.marks.autoLinkAnchor || m.type === schema.marks.linkAnchor);
+}
+export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: readonly Mark[]) => Mark | undefined) {
+ let before = 0,
+ nbef = rpos.nodeBefore;
while (nbef && finder(nbef.marks)) {
before += nbef.nodeSize;
rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
@@ -19,8 +27,9 @@ export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (ma
}
return before;
}
-export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
- let after = 0, naft = rpos.nodeAfter;
+export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: readonly Mark[]) => Mark | undefined) {
+ let after = 0,
+ naft = rpos.nodeAfter;
while (naft && finder(naft.marks)) {
after += naft.nodeSize;
rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
@@ -31,7 +40,7 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark
// this view appears when clicking on text that has a hyperlink which is configured to show a preview of its target.
// this will also display metadata information about text when the view is configured to display things like other people who authored text.
-//
+//
export class FormattedTextBoxComment {
static tooltip: HTMLElement;
static tooltipText: HTMLElement;
@@ -42,11 +51,11 @@ export class FormattedTextBoxComment {
constructor(view: any) {
if (!FormattedTextBoxComment.tooltip) {
- const tooltip = FormattedTextBoxComment.tooltip = document.createElement("div");
- const tooltipText = FormattedTextBoxComment.tooltipText = document.createElement("div");
- tooltip.className = "FormattedTextBox-tooltip";
- tooltipText.className = "FormattedTextBox-tooltipText";
- tooltip.style.display = "none";
+ const tooltip = (FormattedTextBoxComment.tooltip = document.createElement('div'));
+ const tooltipText = (FormattedTextBoxComment.tooltipText = document.createElement('div'));
+ tooltip.className = 'FormattedTextBox-tooltip';
+ tooltipText.className = 'FormattedTextBox-tooltipText';
+ tooltip.style.display = 'none';
tooltip.appendChild(tooltipText);
tooltip.onpointerdown = (e: PointerEvent) => {
const { textBox, startUserMarkRegion, endUserMarkRegion, userMark } = FormattedTextBoxComment;
@@ -54,42 +63,51 @@ export class FormattedTextBoxComment {
e.stopPropagation();
e.preventDefault();
};
- document.getElementById("root")?.appendChild(tooltip);
+ document.getElementById('root')?.appendChild(tooltip);
}
}
public static Hide() {
FormattedTextBoxComment.textBox = undefined;
- FormattedTextBoxComment.tooltip.style.display = "none";
+ FormattedTextBoxComment.tooltip.style.display = 'none';
}
public static saveMarkRegion(textBox: any, start: number, end: number, mark: Mark) {
FormattedTextBoxComment.textBox = textBox;
FormattedTextBoxComment.startUserMarkRegion = start;
FormattedTextBoxComment.endUserMarkRegion = end;
FormattedTextBoxComment.userMark = mark;
- FormattedTextBoxComment.tooltip.style.display = "";
+ FormattedTextBoxComment.tooltip.style.display = '';
}
static showCommentbox(view: EditorView, nbef: number) {
const state = view.state;
// These are in screen coordinates
- const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+ const start = view.coordsAtPos(state.selection.from - nbef),
+ end = view.coordsAtPos(state.selection.from - nbef);
// The box in which the tooltip is positioned, to use as base
- const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect();
+ const box = (document.getElementsByClassName('mainView-container') as any)[0].getBoundingClientRect();
// Find a center-ish x position from the selection endpoints (when crossing lines, end may be more to the left)
const left = Math.max((start.left + end.left) / 2, start.left + 3);
- FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
- FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
- FormattedTextBoxComment.tooltip.style.display = "";
+ FormattedTextBoxComment.tooltip.style.left = left - box.left + 'px';
+ FormattedTextBoxComment.tooltip.style.bottom = box.bottom - start.top + 'px';
+ FormattedTextBoxComment.tooltip.style.display = '';
}
- static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = "") {
+ static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '') {
FormattedTextBoxComment.textBox = textBox;
- if ((hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection))) {
- FormattedTextBoxComment.setupPreview(view, textBox, hrefs?.trim().split(" ").filter(h => h));
+ if (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection)) {
+ FormattedTextBoxComment.setupPreview(
+ view,
+ textBox,
+ hrefs
+ ?.trim()
+ .split(' ')
+ .filter(h => h),
+ linkDoc
+ );
}
}
- static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[]) {
+ static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) {
const state = view.state;
// this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
if (state.selection.$from) {
@@ -103,24 +121,27 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.saveMarkRegion(textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
}
if (mark && child && ((nbef && naft) || !noselection)) {
- FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " on " + (new Date(mark.attrs.modified * 1000)).toLocaleString();
+ FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + ' on ' + new Date(mark.attrs.modified * 1000).toLocaleString();
FormattedTextBoxComment.showCommentbox(view, nbef);
} else FormattedTextBoxComment.Hide();
}
- // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links.
+ // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links.
if (state.selection.$from && hrefs?.length) {
const nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef;
- nbef && naft && LinkDocPreview.SetLinkInfo({
- docProps: textBox.props,
- linkSrc: textBox.rootDoc,
- location: ((pos) => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
- hrefs,
- showHeader: true
- });
+ nbef &&
+ naft &&
+ LinkDocPreview.SetLinkInfo({
+ docProps: textBox.props,
+ linkSrc: textBox.rootDoc,
+ linkDoc: linkDoc ? (DocServer.GetCachedRefField(linkDoc) as Doc) : undefined,
+ location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
+ hrefs,
+ showHeader: true,
+ });
}
}
- destroy() { }
-} \ No newline at end of file
+ destroy() {}
+}
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index c76eda859..31552cf1b 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -1,22 +1,17 @@
-import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands";
-import { liftTarget } from "prosemirror-transform";
-import { redo, undo } from "prosemirror-history";
-import { Schema } from "prosemirror-model";
-import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
-import { splitListItem, wrapInList, } from "prosemirror-schema-list";
-import { EditorState, Transaction, TextSelection } from "prosemirror-state";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { NumCast, BoolCast, Cast, StrCast } from "../../../../fields/Types";
-import { Doc, DataSym, DocListCast, AclAugment, AclSelfEdit } from "../../../../fields/Doc";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { Id } from "../../../../fields/FieldSymbols";
-import { Docs } from "../../../documents/Documents";
-import { Utils } from "../../../../Utils";
-import { listSpec } from "../../../../fields/Schema";
-import { List } from "../../../../fields/List";
-import { GetEffectiveAcl } from "../../../../fields/util";
-
-const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
+import { chainCommands, deleteSelection, exitCode, joinBackward, joinDown, joinUp, lift, newlineInCode, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from 'prosemirror-commands';
+import { redo, undo } from 'prosemirror-history';
+import { Schema } from 'prosemirror-model';
+import { splitListItem, wrapInList } from 'prosemirror-schema-list';
+import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
+import { liftTarget } from 'prosemirror-transform';
+import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc';
+import { GetEffectiveAcl } from '../../../../fields/util';
+import { Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
+
+const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
export type KeyMap = { [key: string]: any };
@@ -25,12 +20,12 @@ export let updateBullets = (tx2: Transaction, schema: Schema, assignedMapStyle?:
tx2.doc.descendants((node: any, offset: any, index: any) => {
if ((from === undefined || to === undefined || (from <= offset + node.nodeSize && to >= offset)) && (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item)) {
const path = (tx2.doc.resolve(offset) as any).path;
- let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty('type') && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
if (node.type === schema.nodes.ordered_list) {
if (depth === 0 && !assignedMapStyle) mapStyle = node.attrs.mapStyle;
depth++;
}
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle, bulletStyle: depth, }, node.marks);
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle, bulletStyle: depth }, node.marks);
}
});
return tx2;
@@ -48,32 +43,10 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
keys[key] = cmd;
}
- /// bcz; Argh!! replace with an onEnter func that conditionally handles Enter
- const addTextBox = (below: boolean, force?: boolean) => {
- if (props.Document.treeViewType === "outline") return true; // bcz: Arghh .. need to determine if this is an treeViewOutlineBox in which case Enter's are ignored..
- const layoutDoc = props.Document;
- const originalDoc = layoutDoc.rootDocument || layoutDoc;
- if (force || props.Document._singleLine) {
- const layoutKey = StrCast(originalDoc.layoutKey);
- const newDoc = Doc.MakeCopy(originalDoc, true);
- const dataField = originalDoc[Doc.LayoutFieldKey(newDoc)];
- newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
- if (below) newDoc.y = NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10;
- else newDoc.x = NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10;
- if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
- newDoc[layoutKey] = originalDoc[layoutKey];
- }
- Doc.GetProto(newDoc).text = undefined;
- FormattedTextBox.SelectOnLoad = newDoc[Id];
- props.addDocument(newDoc);
- return true;
- }
- return false;
- };
-
const canEdit = (state: any) => {
- switch (GetEffectiveAcl(props.Document)) {
- case AclAugment: return false;
+ switch (GetEffectiveAcl(props.DataDoc)) {
+ case AclAugment:
+ return false;
case AclSelfEdit:
for (var i = state.selection.from; i < state.selection.to; i++) {
const marks = state.doc.resolve(i)?.marks?.();
@@ -86,96 +59,102 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return true;
};
- const toggleEditableMark = (mark: any) => (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
+ const toggleEditableMark = (mark: any) => (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && toggleMark(mark)(state, dispatch);
//History commands
- bind("Mod-z", undo);
- bind("Shift-Mod-z", redo);
- !mac && bind("Mod-y", redo);
+ bind('Mod-z', undo);
+ bind('Shift-Mod-z', redo);
+ !mac && bind('Mod-y', redo);
//Commands to modify Mark
- bind("Mod-b", toggleEditableMark(schema.marks.strong));
- bind("Mod-B", toggleEditableMark(schema.marks.strong));
+ bind('Mod-b', toggleEditableMark(schema.marks.strong));
+ bind('Mod-B', toggleEditableMark(schema.marks.strong));
- bind("Mod-e", toggleEditableMark(schema.marks.em));
- bind("Mod-E", toggleEditableMark(schema.marks.em));
+ bind('Mod-e', toggleEditableMark(schema.marks.em));
+ bind('Mod-E', toggleEditableMark(schema.marks.em));
- bind("Mod-*", toggleEditableMark(schema.marks.code));
+ bind('Mod-*', toggleEditableMark(schema.marks.code));
- bind("Mod-u", toggleEditableMark(schema.marks.underline));
- bind("Mod-U", toggleEditableMark(schema.marks.underline));
+ bind('Mod-u', toggleEditableMark(schema.marks.underline));
+ bind('Mod-U', toggleEditableMark(schema.marks.underline));
//Commands for lists
- bind("Ctrl-i", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any));
-
- bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- /// bcz; Argh!! replace layotuTEmpalteString with a onTab prop conditionally handles Tab);
- if (props.Document._singleLine) {
- if (!props.LayoutTemplateString) return addTextBox(false, true);
- return true;
- }
+ bind('Ctrl-i', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && wrapInList(schema.nodes.ordered_list)(state as any, dispatch as any));
+
+ bind('Ctrl-Tab', () => (props.onKey?.(event, props) ? true : true));
+ bind('Alt-Tab', () => (props.onKey?.(event, props) ? true : true));
+ bind('Meta-Tab', () => (props.onKey?.(event, props) ? true : true));
+ bind('Meta-Enter', () => (props.onKey?.(event, props) ? true : true));
+ bind('Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const ref = state.selection;
const range = ref.$from.blockRange(ref.$to);
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
- const tx3 = updateBullets(tx2, schema);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
- dispatch(tx3);
- })) { // couldn't sink into an existing list, so wrap in a new one
- const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
- if (!wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => {
+ if (
+ !sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
const tx3 = updateBullets(tx2, schema);
- // when promoting to a list, assume list will format things so don't copy the stored marks.
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
dispatch(tx3);
- })) {
- console.log("bullet promote fail");
+ })
+ ) {
+ // couldn't sink into an existing list, so wrap in a new one
+ const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ if (
+ !wrapInList(schema.nodes.ordered_list)(newstate.state as any, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })
+ ) {
+ console.log('bullet promote fail');
}
}
});
- bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- /// bcz; Argh!! replace with a onShiftTab prop conditionally handles Tab);
- if (props.Document._singleLine) return true;
+ bind('Shift-Tab', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
- const tx3 = updateBullets(tx2, schema);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
- dispatch(tx3);
- })) {
- console.log("bullet demote fail");
+ if (
+ !liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })
+ ) {
+ console.log('bullet demote fail');
}
});
//Command to create a new Tab with a PDF of all the command shortcuts
- bind("Mod-/", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
- const newDoc = Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 });
- props.addDocTab(newDoc, "add:right");
+ bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
+ props.addDocTab(newDoc, 'add:right');
});
//Commands to modify BlockType
- bind("Ctrl->", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit((state) && wrapIn(schema.nodes.blockquote)(state as any, dispatch as any)));
- bind("Alt-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any));
- bind("Shift-Ctrl-\\", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any));
+ bind('Ctrl->', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state && wrapIn(schema.nodes.blockquote)(state as any, dispatch as any)));
+ bind('Alt-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.paragraph)(state as any, dispatch as any));
+ bind('Shift-Ctrl-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.code_block)(state as any, dispatch as any));
- bind("Ctrl-m", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: "math" + Utils.GenerateGuid() }))));
+ bind('Ctrl-m', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(schema.nodes.equation.create({ fieldKey: 'math' + Utils.GenerateGuid() }))));
for (let i = 1; i <= 6; i++) {
- bind("Shift-Ctrl-" + i, (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any));
+ bind('Shift-Ctrl-' + i, (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && setBlockType(schema.nodes.heading, { level: i })(state as any, dispatch as any));
}
//Command to create a horizontal break line
const hr = schema.nodes.horizontal_rule;
- bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
+ bind('Mod-_', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView()));
//Command to unselect all
- bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ bind('Escape', (state: EditorState, dispatch: (tx: Transaction) => void) => {
dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
(document.activeElement as any).blur?.();
SelectionManager.DeselectAll();
@@ -187,25 +166,29 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return tx;
};
- //Command to create a text document to the right of the selected textbox
- bind("Alt-Enter", () => addTextBox(false, true));
-
- //Command to create a text document to the bottom of the selected textbox
- bind("Ctrl-Enter", () => addTextBox(true, true));
+ bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true));
+ bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true));
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
- bind("Backspace", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ bind('Backspace', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
- if (!deleteSelection(state, (tx: Transaction<S>) => {
- dispatch(updateBullets(tx, schema));
- })) {
- if (!joinBackward(state, (tx: Transaction<S>) => {
+ if (
+ !deleteSelection(state, (tx: Transaction) => {
dispatch(updateBullets(tx, schema));
- })) {
- if (!selectNodeBackward(state, (tx: Transaction<S>) => {
+ })
+ ) {
+ if (
+ !joinBackward(state, (tx: Transaction) => {
dispatch(updateBullets(tx, schema));
- })) {
+ })
+ ) {
+ if (
+ !selectNodeBackward(state, (tx: Transaction) => {
+ dispatch(updateBullets(tx, schema));
+ })
+ ) {
return false;
}
}
@@ -215,9 +198,8 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//newlineInCode, createParagraphNear, liftEmptyBlock, splitBlock
//command to break line
- bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
- if (addTextBox(true, false)) return true;
-
+ bind('Enter', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const trange = state.selection.$from.blockRange(state.selection.$to);
@@ -230,25 +212,29 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
}
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
- const cr = state.selection.$from.node().textContent.endsWith("\n");
+ const cr = state.selection.$from.node().textContent.endsWith('\n');
if (cr || !newlineInCode(state, dispatch as any)) {
- if (!splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => {
- const tx3 = updateBullets(tx2, schema);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
- dispatch(tx3);
- })) {
+ if (
+ !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })
+ ) {
const fromattrs = state.selection.$from.node().attrs;
- if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
- const tonode = tx3.selection.$to.node();
- if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
- const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
- splitMetadata(marks, tx4);
- if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
- dispatch(tx4);
- }
- } else dispatch(tx3.insertText("\r\n"));
- })) {
+ if (
+ !splitBlockKeepMarks(state, (tx3: Transaction) => {
+ const tonode = tx3.selection.$to.node();
+ if (tx3.selection.to && tx3.doc.nodeAt(tx3.selection.to - 1)) {
+ const tx4 = tx3.setNodeMarkup(tx3.selection.to - 1, tonode.type, fromattrs, tonode.marks);
+ splitMetadata(marks, tx4);
+ if (!liftListItem(schema.nodes.list_item)(tx4, dispatch as (tx: Transaction) => void)) {
+ dispatch(tx4);
+ }
+ } else dispatch(tx3.insertText('\r\n'));
+ })
+ ) {
return false;
}
}
@@ -257,16 +243,16 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
//Command to create a blank space
- bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
});
- bind("Alt-ArrowUp", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinUp(state, dispatch as any));
- bind("Alt-ArrowDown", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && joinDown(state, dispatch as any));
- bind("Mod-BracketLeft", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => canEdit(state) && lift(state, dispatch as any));
+ bind('Alt-ArrowUp', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && joinUp(state, dispatch as any));
+ bind('Alt-ArrowDown', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && joinDown(state, dispatch as any));
+ bind('Mod-BracketLeft', (state: EditorState, dispatch: (tx: Transaction) => void) => canEdit(state) && lift(state, dispatch as any));
const cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
@@ -276,10 +262,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
return false;
});
- // mac && bind("Ctrl-Enter", cmd);
- // bind("Mod-Enter", cmd);
- bind("Shift-Enter", cmd);
+ bind('Shift-Enter', cmd);
return keys;
}
-
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 4814d6e9a..2a77210ae 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,40 +1,40 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx";
-import { observer } from "mobx-react";
-import { lift, wrapIn } from "prosemirror-commands";
-import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos } from "prosemirror-model";
-import { wrapInList } from "prosemirror-schema-list";
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
-import { EditorView } from "prosemirror-view";
-import { Doc } from "../../../../fields/Doc";
-import { Cast, StrCast } from "../../../../fields/Types";
-import { DocServer } from "../../../DocServer";
-import { LinkManager } from "../../../util/LinkManager";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
-import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu";
-import { FieldViewProps } from "../FieldView";
-import { FormattedTextBox, FormattedTextBoxProps } from "./FormattedTextBox";
-import { updateBullets } from "./ProsemirrorExampleTransfer";
-import "./RichTextMenu.scss";
-import { schema } from "./schema_rts";
-const { toggleMark } = require("prosemirror-commands");
-
+import React = require('react');
+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 { lift, wrapIn } from 'prosemirror-commands';
+import { Mark, MarkType, Node as ProsNode, ResolvedPos } from 'prosemirror-model';
+import { wrapInList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, TextSelection } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
+import { Doc } from '../../../../fields/Doc';
+import { Cast, StrCast } from '../../../../fields/Types';
+import { DocServer } from '../../../DocServer';
+import { LinkManager } from '../../../util/LinkManager';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { FieldViewProps } from '../FieldView';
+import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox';
+import { updateBullets } from './ProsemirrorExampleTransfer';
+import './RichTextMenu.scss';
+import { schema } from './schema_rts';
+const { toggleMark } = require('prosemirror-commands');
@observer
-export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
+export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable static Instance: RichTextMenu;
public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
private _linkToRef = React.createRef<HTMLInputElement>();
@observable public view?: EditorView;
- public editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+ public editorProps: (FieldViewProps & FormattedTextBoxProps) | undefined;
public _brushMap: Map<string, Set<Mark>> = new Map();
@observable private collapsed: boolean = false;
+ @observable private _noLinkActive: boolean = false;
@observable private _boldActive: boolean = false;
@observable private _italicsActive: boolean = false;
@observable private _underlineActive: boolean = false;
@@ -42,53 +42,60 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _subscriptActive: boolean = false;
@observable private _superscriptActive: boolean = false;
- @observable private _activeFontSize: string = "13px";
- @observable private _activeFontFamily: string = "";
- @observable private activeListType: string = "";
- @observable private _activeAlignment: string = "left";
+ @observable private _activeFontSize: string = '13px';
+ @observable private _activeFontFamily: string = '';
+ @observable private activeListType: string = '';
+ @observable private _activeAlignment: string = 'left';
@observable private brushMarks: Set<Mark> = new Set();
@observable private showBrushDropdown: boolean = false;
- @observable private _activeFontColor: string = "black";
+ @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 = "";
+ @observable private currentLink: string | undefined = '';
@observable private showLinkDropdown: boolean = false;
_reaction: IReactionDisposer | undefined;
- _delayHide = false;
constructor(props: Readonly<{}>) {
super(props);
runInAction(() => {
RichTextMenu.Instance = this;
this._canFade = false;
- //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]);
this.Pinned = true;
});
}
- componentDidMount() {
- this._reaction = reaction(() => SelectionManager.Views(),
- () => this._delayHide && !(this._delayHide = false) && this.fadeOut(true));
+ @computed get noAutoLink() {
+ return this._noLinkActive;
}
- componentWillUnmount() {
- this._reaction?.();
+ @computed get bold() {
+ return this._boldActive;
+ }
+ @computed get underline() {
+ return this._underlineActive;
+ }
+ @computed get italics() {
+ return this._italicsActive;
+ }
+ @computed get strikeThrough() {
+ return this._strikethroughActive;
+ }
+ @computed get fontColor() {
+ return this._activeFontColor;
+ }
+ @computed get fontFamily() {
+ return this._activeFontFamily;
+ }
+ @computed get fontSize() {
+ return this._activeFontSize;
+ }
+ @computed get textAlign() {
+ return this._activeAlignment;
}
-
- @computed get bold() { return this._boldActive; }
- @computed get underline() { return this._underlineActive; }
- @computed get italics() { return this._italicsActive; }
- @computed get strikeThrough() { return this._strikethroughActive; }
- @computed get fontColor() { return this._activeFontColor; }
- @computed get fontFamily() { return this._activeFontFamily; }
- @computed get fontSize() { return this._activeFontSize; }
- @computed get textAlign() { return this._activeAlignment; }
-
- public delayHide = () => this._delayHide = true;
@action
public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) {
@@ -117,16 +124,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
- this._activeFontFamily = !activeFamilies.length ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
- this._activeFontSize = !activeSizes.length ? "13px" : activeSizes[0];
- this._activeFontColor = !activeColors.length ? "black" : activeColors.length > 0 ? String(activeColors[0]) : "...";
- this.activeHighlightColor = !activeHighlights.length ? "" : activeHighlights.length > 0 ? String(activeHighlights[0]) : "...";
+ this._activeFontFamily = !activeFamilies.length ? '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 ? 'black' : activeColors.length > 0 ? String(activeColors[0]) : '...';
+ this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
// update link in current selection
this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
}
- setMark = (mark: Mark, state: EditorState<any>, dispatch: any, dontToggle: boolean = false) => {
+ setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => {
if (mark) {
const node = (state.selection as NodeSelection).node;
if (node?.type === schema.nodes.ordered_list) {
@@ -139,7 +146,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
} else if (dontToggle) {
toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
const { from, $from, to, empty } = tx.selection;
- if (!tx.doc.rangeHasMark(from, to, mark.type)) { // hack -- should have just set the mark in the first place
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ // hack -- should have just set the mark in the first place
toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
} else dispatch(tx);
});
@@ -147,7 +155,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
toggleMark(mark.type, mark.attrs)(state, dispatch);
}
}
- }
+ };
// finds font sizes and families in selection
getActiveAlignment() {
@@ -155,11 +163,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const path = (this.view.state.selection.$from as any).path;
for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) {
if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {
- return path[i].attrs.align || "left";
+ return path[i].attrs.align || 'left';
}
}
}
- return "left";
+ return 'left';
}
// finds font sizes and families in selection
@@ -175,7 +183,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return this.view.state.selection.$from.nodeAfter?.attrs.mapStyle;
}
}
- return "";
+ return '';
}
// finds font sizes and families in selection
@@ -189,7 +197,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (this.TextView.props.isSelected(true)) {
const state = this.view.state;
const pos = this.view.state.selection.$from;
- const marks: Mark<any>[] = [...(state.storedMarks ?? [])];
+ const marks: Mark[] = [...(state.storedMarks ?? [])];
if (state.selection.empty) {
const ref_node = this.reference_node(pos);
marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : []));
@@ -208,10 +216,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return { activeFamilies, activeSizes, activeColors, activeHighlights };
}
- getMarksInSelection(state: EditorState<any>) {
+ getMarksInSelection(state: EditorState) {
const found = new Set<Mark>();
const { from, to } = state.selection as TextSelection;
- state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ state.doc.nodesBetween(from, to, node => node.marks.forEach(m => found.add(m)));
return found;
}
@@ -220,7 +228,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
let activeMarks: MarkType[] = [];
if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks;
- const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
//current selection
const { empty, ranges, $to } = this.view.state.selection as TextSelection;
@@ -233,14 +241,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
return false;
});
- }
- else {
+ } else {
const pos = this.view.state.selection.$from;
const ref_node: ProsNode | null = this.reference_node(pos);
if (ref_node !== null && ref_node !== this.view.state.doc) {
if (ref_node.isText) {
- }
- else {
+ } else {
return [];
}
activeMarks = markGroup.filter(mark_type => {
@@ -264,6 +270,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
if (!activeMarks) return;
+ this._noLinkActive = false;
this._boldActive = false;
this._italicsActive = false;
this._underlineActive = false;
@@ -273,23 +280,46 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
activeMarks.forEach(mark => {
switch (mark.name) {
- case "strong": this._boldActive = true; break;
- case "em": this._italicsActive = true; break;
- case "underline": this._underlineActive = true; break;
- case "strikethrough": this._strikethroughActive = true; break;
- case "subscript": this._subscriptActive = true; break;
- case "superscript": this._superscriptActive = true; break;
+ case 'noAutoLinkAnchor':
+ this._noLinkActive = true;
+ break;
+ case 'strong':
+ this._boldActive = true;
+ break;
+ case 'em':
+ this._italicsActive = true;
+ break;
+ case 'underline':
+ this._underlineActive = true;
+ break;
+ case 'strikethrough':
+ this._strikethroughActive = true;
+ break;
+ case 'subscript':
+ this._subscriptActive = true;
+ break;
+ case 'superscript':
+ this._superscriptActive = true;
+ break;
}
});
}
+ toggleNoAutoLinkAnchor = () => {
+ if (this.view) {
+ const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor);
+ this.setMark(mark, this.view.state, this.view.dispatch, false);
+ this.TextView.autoLink();
+ this.view.focus();
+ }
+ };
toggleBold = () => {
if (this.view) {
const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong);
this.setMark(mark, this.view.state, this.view.dispatch, false);
this.view.focus();
}
- }
+ };
toggleUnderline = () => {
if (this.view) {
@@ -297,7 +327,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(mark, this.view.state, this.view.dispatch, false);
this.view.focus();
}
- }
+ };
toggleItalics = () => {
if (this.view) {
@@ -305,17 +335,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(mark, this.view.state, this.view.dispatch, false);
this.view.focus();
}
- }
-
+ };
setFontSize = (fontSize: string) => {
if (this.view) {
- const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
- this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
- this.view.focus();
- this.updateMenu(this.view, undefined, this.props);
+ if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) {
+ this.TextView.dataDoc.fontSize = fontSize;
+ this.view.focus();
+ this.updateMenu(this.view, undefined, this.props);
+ } else {
+ const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
+ this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
+ this.view.focus();
+ this.updateMenu(this.view, undefined, this.props);
+ }
}
- }
+ };
setFontFamily = (family: string) => {
if (this.view) {
@@ -324,7 +359,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view.focus();
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 });
@@ -344,8 +379,10 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// TODO: remove doesn't work
// remove all node type and apply the passed-in one to the selected text
- changeListType = (nodeType: Node | undefined) => {
- if (!this.view || (nodeType as any)?.attrs.mapStyle === "") return;
+ changeListType = (mapStyle: string) => {
+ const active = this.view?.state && RichTextMenu.Instance.getActiveListStyle();
+ const nodeType = this.view?.state.schema.nodes.ordered_list.create({ mapStyle: active === mapStyle ? '' : mapStyle });
+ if (!this.view || nodeType?.attrs.mapStyle === '') return;
const nextIsOL = this.view.state.selection.$from.nodeAfter?.type === schema.nodes.ordered_list;
let inList: any = undefined;
@@ -359,17 +396,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
- if (inList || !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
- marks && tx3.ensureMarks([...marks]);
- marks && tx3.setStoredMarks([...marks]);
+ if (
+ inList ||
+ !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
- this.view!.dispatch(tx2);
- })) {
+ this.view!.dispatch(tx2);
+ })
+ ) {
const tx2 = this.view.state.tr;
if (nodeType && (inList || nextIsOL)) {
- const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from,
- inList ? fromList + inList.nodeSize : this.view.state.selection.to);
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle, inList ? fromList : this.view.state.selection.from, inList ? fromList + inList.nodeSize : this.view.state.selection.to);
marks && tx3.ensureMarks([...marks]);
marks && tx3.setStoredMarks([...marks]);
this.view.dispatch(tx3);
@@ -377,9 +416,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
this.view.focus();
this.updateMenu(this.view, undefined, this.props);
- }
+ };
- insertSummarizer(state: EditorState<any>, dispatch: any) {
+ insertSummarizer(state: EditorState, dispatch: any) {
if (state.selection.empty) return false;
const mark = state.schema.marks.summarize.create();
const tr = state.tr;
@@ -390,7 +429,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return true;
}
- align = (view: EditorView, dispatch: any, alignment: "left" | "right" | "center") => {
+ align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
if (this.TextView.props.isSelected(true)) {
var tr = view.state.tr;
view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => {
@@ -404,9 +443,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
view.focus();
dispatch?.(tr);
}
- }
+ };
- insetParagraph(state: EditorState<any>, dispatch: any) {
+ insetParagraph(state: EditorState, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
@@ -419,7 +458,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
dispatch?.(tr);
return true;
}
- outsetParagraph(state: EditorState<any>, dispatch: any) {
+ outsetParagraph(state: EditorState, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
@@ -433,7 +472,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return true;
}
- indentParagraph(state: EditorState<any>, dispatch: any) {
+ indentParagraph(state: EditorState, dispatch: any) {
var tr = state.tr;
const heading = false;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
@@ -449,7 +488,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return true;
}
- hangingIndentParagraph(state: EditorState<any>, dispatch: any) {
+ hangingIndentParagraph(state: EditorState, dispatch: any) {
var tr = state.tr;
state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos, parent, index) => {
if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) {
@@ -464,7 +503,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return true;
}
- insertBlockquote(state: EditorState<any>, dispatch: any) {
+ insertBlockquote(state: EditorState, dispatch: any) {
const path = (state.selection.$from as any).path;
if (path.length > 6 && path[path.length - 6].type === schema.nodes.blockquote) {
lift(state, dispatch);
@@ -474,20 +513,22 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return true;
}
- insertHorizontalRule(state: EditorState<any>, dispatch: any) {
+ insertHorizontalRule(state: EditorState, dispatch: any) {
dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView());
return true;
}
- @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+ @action toggleBrushDropdown() {
+ this.showBrushDropdown = !this.showBrushDropdown;
+ }
// todo: add brushes to brushMap to save with a style name
onBrushNameKeyPress = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
+ if (e.key === 'Enter') {
RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
- this._brushNameRef.current!.style.background = "lightGray";
+ this._brushNameRef.current!.style.background = 'lightGray';
}
- }
+ };
_brushNameRef = React.createRef<HTMLInputElement>();
@action
@@ -496,7 +537,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
@action
- fillBrush(state: EditorState<any>, dispatch: any) {
+ fillBrush(state: EditorState, dispatch: any) {
if (!this.view) return;
if (!Array.from(this.brushMarks.keys()).length) {
@@ -504,68 +545,81 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (selected_marks.size >= 0) {
this.brushMarks = selected_marks;
}
- }
- else {
+ } else {
const { from, to, $from } = this.view.state.selection;
if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
if (to - from > 0) {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
- Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
- this.setMark(mark, this.view!.state, this.view!.dispatch);
- });
+ Array.from(this.brushMarks)
+ .filter(m => m.type !== schema.marks.user_mark)
+ .forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
}
}
}
}
- get TextView() { return (this.view as any)?.TextView as FormattedTextBox; }
- get TextViewFieldKey() { return this.TextView?.props.fieldKey; }
-
-
-
- @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+ get TextView() {
+ return (this.view as any)?.TextView as FormattedTextBox;
+ }
+ get TextViewFieldKey() {
+ return this.TextView?.props.fieldKey;
+ }
+ @action setActiveHighlight(color: string) {
+ this.activeHighlightColor = color;
+ }
- @action setCurrentLink(link: string) { this.currentLink = link; }
+ @action setCurrentLink(link: string) {
+ this.currentLink = link;
+ }
createLinkButton() {
const self = this;
function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
self.TextView?.endUndoTypingBatch();
- UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), "link change");
+ UndoManager.RunInBatch(() => self.setCurrentLink(e.target.value), 'link change');
}
- const link = this.currentLink ? this.currentLink : "";
+ const link = this.currentLink ? this.currentLink : '';
- const button = <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom">
- <button className="antimodeMenu-button color-preview-button">
- <FontAwesomeIcon icon="link" size="lg" />
- </button>
- </Tooltip>;
+ const button = (
+ <Tooltip title={<div className="dash-tooltip">set hyperlink</div>} placement="bottom">
+ <button className="antimodeMenu-button color-preview-button">
+ <FontAwesomeIcon icon="link" size="lg" />
+ </button>
+ </Tooltip>
+ );
- const dropdownContent =
+ const dropdownContent = (
<div className="dropdown link-menu">
<p>Linked to:</p>
<input value={link} ref={this._linkToRef} placeholder="Enter URL" onChange={onLinkChange} />
- <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "add:right")}>Apply hyperlink</button>
+ <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, 'add:right')}>
+ Apply hyperlink
+ </button>
<div className="divider" />
- <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
- </div>;
+ <button className="remove-button" onPointerDown={e => this.deleteLink()}>
+ Remove link
+ </button>
+ </div>
+ );
- return <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />;
+ return <ButtonDropdown view={this.view} key={'link button'} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} link={true} />;
}
async getTextLinkTargetTitle() {
if (!this.view) return;
const node = this.view.state.selection.$from.nodeAfter;
- const link = node && node.marks.find(m => m.type.name === "link");
+ const link = node && node.marks.find(m => m.type.name === 'link');
if (link) {
const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined;
if (href) {
if (href.indexOf(Doc.localServerPath()) === 0) {
- const linkclicked = href.replace(Doc.localServerPath(), "").split("?")[0];
+ const linkclicked = href.replace(Doc.localServerPath(), '').split('?')[0];
if (linkclicked) {
const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
@@ -594,8 +648,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// TODO: should check for valid URL
@undoBatch
makeLinkToURL = (target: string, lcoation: string) => {
- ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, "onRadd:rightight", target, target);
- }
+ ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target);
+ };
@undoBatch
@action
@@ -606,13 +660,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const allAnchors = linkAnchor.attrs.allAnchors.slice();
this.TextView.RemoveAnchorFromSelection(allAnchors);
// bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected.
- allAnchors.filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0).forEach((aref: any) => {
- const anchorId = aref.href.replace(Doc.localServerPath(), "").split("?")[0];
- anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
- });
+ allAnchors
+ .filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0)
+ .forEach((aref: any) => {
+ const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0];
+ anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc));
+ });
}
}
- }
+ };
linkExtend($start: ResolvedPos, href: string) {
const mark = this.view!.state.schema.marks.linkAnchor;
@@ -633,7 +689,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return { from: startPos, to: endPos };
}
- reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ reference_node(pos: ResolvedPos): ProsNode | null {
if (!this.view) return null;
let ref_node: ProsNode = this.view.state.doc;
@@ -653,7 +709,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
ref_node = node;
skip = true;
}
-
});
}
}
@@ -737,21 +792,19 @@ interface ButtonDropdownProps {
openDropdownOnButton?: boolean;
link?: boolean;
pdf?: boolean;
-
}
@observer
export class ButtonDropdown extends React.Component<ButtonDropdownProps> {
-
@observable private showDropdown: boolean = false;
private ref: HTMLDivElement | null = null;
componentDidMount() {
- document.addEventListener("pointerdown", this.onBlur);
+ document.addEventListener('pointerdown', this.onBlur);
}
componentWillUnmount() {
- document.removeEventListener("pointerdown", this.onBlur);
+ document.removeEventListener('pointerdown', this.onBlur);
}
@action
@@ -767,7 +820,7 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> {
e.preventDefault();
e.stopPropagation();
this.toggleDropdown();
- }
+ };
onBlur = (e: PointerEvent) => {
setTimeout(() => {
@@ -775,37 +828,40 @@ export class ButtonDropdown extends React.Component<ButtonDropdownProps> {
this.setShowDropdown(false);
}
}, 0);
- }
-
+ };
render() {
return (
- <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
- {!this.props.pdf ?
+ <div className="button-dropdown-wrapper" ref={node => (this.ref = node)}>
+ {!this.props.pdf ? (
<div className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.props.openDropdownOnButton ? this.onDropdownClick : undefined}>
{this.props.button}
- <div style={{ marginTop: "-8.5", position: "relative" }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}>
+ <div style={{ marginTop: '-8.5', position: 'relative' }} onPointerDown={!this.props.openDropdownOnButton ? this.onDropdownClick : undefined}>
<FontAwesomeIcon icon="caret-down" size="sm" />
</div>
</div>
- :
+ ) : (
<>
{this.props.button}
<button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
<FontAwesomeIcon icon="caret-down" size="sm" />
</button>
- </>}
- {this.showDropdown ? this.props.dropdownContent : (null)}
+ </>
+ )}
+ {this.showDropdown ? this.props.dropdownContent : null}
</div>
);
}
}
-
interface RichTextMenuPluginProps {
editorProps: any;
}
export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
- render() { return null; }
- update(view: EditorView, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps); }
-} \ No newline at end of file
+ render() {
+ return null;
+ }
+ update(view: EditorView, lastState: EditorState | undefined) {
+ RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps);
+ }
+}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index bafae84dc..1916b94bf 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,17 +1,17 @@
-import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
-import { NodeSelection, TextSelection } from "prosemirror-state";
-import { DataSym, Doc } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-import { ComputedField } from "../../../../fields/ScriptField";
-import { NumCast, StrCast } from "../../../../fields/Types";
-import { normalizeEmail } from "../../../../fields/util";
-import { returnFalse, Utils } from "../../../../Utils";
-import { DocServer } from "../../../DocServer";
-import { Docs, DocUtils } from "../../../documents/Documents";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { wrappingInputRule } from "./prosemirrorPatches";
-import { RichTextMenu } from "./RichTextMenu";
-import { schema } from "./schema_rts";
+import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
+import { NodeSelection, TextSelection } from 'prosemirror-state';
+import { DataSym, Doc } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { ComputedField } from '../../../../fields/ScriptField';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { normalizeEmail } from '../../../../fields/util';
+import { returnFalse, Utils } from '../../../../Utils';
+import { DocServer } from '../../../DocServer';
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { FormattedTextBox } from './FormattedTextBox';
+import { wrappingInputRule } from './prosemirrorPatches';
+import { RichTextMenu } from './RichTextMenu';
+import { schema } from './schema_rts';
export class RichTextRules {
public Document: Doc;
@@ -34,9 +34,9 @@ export class RichTextRules {
wrappingInputRule(
/^1\.\s$/,
schema.nodes.ordered_list,
- () => ({ mapStyle: "decimal", bulletStyle: 1 }),
+ () => ({ mapStyle: 'decimal', bulletStyle: 1 }),
(match: any, node: any) => node.childCount + node.attrs.order === +match[1],
- ((type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })) as any
+ ((type: any) => ({ type: type, attrs: { mapStyle: 'decimal', bulletStyle: 1 } })) as any
),
// A. create alphabetical ordered list
@@ -45,341 +45,324 @@ export class RichTextRules {
schema.nodes.ordered_list,
// match => {
() => {
- return ({ mapStyle: "multi", bulletStyle: 1 });
+ return { mapStyle: 'multi', bulletStyle: 1 };
// return ({ order: +match[1] })
},
(match: any, node: any) => {
return node.childCount + node.attrs.order === +match[1];
},
- ((type: any) => ({ type: type, attrs: { mapStyle: "multi", bulletStyle: 1 } })) as any
+ ((type: any) => ({ type: type, attrs: { mapStyle: 'multi', bulletStyle: 1 } })) as any
),
// * + - create bullet list
- wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.ordered_list,
+ wrappingInputRule(
+ /^\s*([-+*])\s$/,
+ schema.nodes.ordered_list,
// match => {
- () => ({ mapStyle: "bullet" }), // ({ order: +match[1] })
+ () => ({ mapStyle: 'bullet' }), // ({ order: +match[1] })
(match: any, node: any) => node.childCount + node.attrs.order === +match[1],
- ((type: any) => ({ type: type, attrs: { mapStyle: "bullet" } })) as any
+ ((type: any) => ({ type: type, attrs: { mapStyle: 'bullet' } })) as any
),
// ``` create code block
textblockTypeInputRule(/^```$/, schema.nodes.code_block),
- // %<font-size> set the font size
- new InputRule(
- new RegExp(/%([0-9]+)\s$/),
- (state, match, start, end) => {
- const size = Number(match[1]);
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
- }),
+ // %<font-size> set the font size
+ new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => {
+ const size = Number(match[1]);
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
+ }),
//Create annotation to a field on the text document
- new InputRule(
- new RegExp(/>>$/),
- (state, match, start, end) => {
- const textDoc = this.Document[DataSym];
- const numInlines = NumCast(textDoc.inlineTextCount);
- 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, _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.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 sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced;
- }),
-
+ new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
+ const textDoc = this.Document[DataSym];
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ 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, _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.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 sm = state.storedMarks || undefined;
+ const replaced = node
+ ? state.tr
+ .insert(start, newNode)
+ .replaceRangeWith(start + 1, end + 1, dashDoc)
+ .insertText(' ', start + 2)
+ .setStoredMarks([...node.marks, ...(sm ? sm : [])])
+ : state.tr;
+ return replaced;
+ }),
// set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%d|d)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
- }
+ new InputRule(new RegExp(/(%d|d)$/), (state, match, start, end) => {
+ if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
+ const pos = state.doc.resolve(start) as any;
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith('%') ? result.deleteRange(start, end) : result;
}
- return null;
- }),
+ }
+ return null;
+ }),
// set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%h|h)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
- }
+ new InputRule(new RegExp(/(%h|h)$/), (state, match, start, end) => {
+ if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
+ const pos = state.doc.resolve(start) as any;
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith('%') ? result.deleteRange(start, end) : result;
}
- return null;
- }),
+ }
+ return null;
+ }),
// set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%q|q)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
- const node = state.selection.node;
- return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
- }
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
- }
+ new InputRule(new RegExp(/(%q|q)$/), (state, match, start, end) => {
+ if (!match[0].startsWith('%') && !this.EnteringStyle) return null;
+ const pos = state.doc.resolve(start) as any;
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
+ }
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith('%') ? result.deleteRange(start, end) : result;
}
- return null;
- }),
+ }
+ return null;
+ }),
// center justify text
- new InputRule(
- new RegExp(/%\^/),
- (state, match, start, end) => {
- const resolved = state.doc.resolve(start) as any;
- if (resolved?.parent.type.name === "paragraph") {
- return state.tr.deleteRange(start, end).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;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }
- }),
+ new InputRule(new RegExp(/%\^/), (state, match, start, end) => {
+ const resolved = state.doc.resolve(start) as any;
+ if (resolved?.parent.type.name === 'paragraph') {
+ return state.tr.deleteRange(start, end).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;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }
+ }),
// left justify text
- new InputRule(
- new RegExp(/%\[/),
- (state, match, start, end) => {
- const resolved = state.doc.resolve(start) as any;
- if (resolved?.parent.type.name === "paragraph") {
- return state.tr.deleteRange(start, end).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;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }
- }),
+ new InputRule(new RegExp(/%\[/), (state, match, start, end) => {
+ const resolved = state.doc.resolve(start) as any;
+ if (resolved?.parent.type.name === 'paragraph') {
+ return state.tr.deleteRange(start, end).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;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }
+ }),
// right justify text
- new InputRule(
- new RegExp(/%\]/),
- (state, match, start, end) => {
- const resolved = state.doc.resolve(start) as any;
- if (resolved?.parent.type.name === "paragraph") {
- return state.tr.deleteRange(start, end).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;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }
- }),
-
+ new InputRule(new RegExp(/%\]/), (state, match, start, end) => {
+ const resolved = state.doc.resolve(start) as any;
+ if (resolved?.parent.type.name === 'paragraph') {
+ return state.tr.deleteRange(start, end).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;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }
+ }),
// %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))));
- }),
+ 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);
- const marks = RichTextMenu.Instance._brushMap.get(color);
+ new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
- if (marks) {
- const tr = state.tr.deleteRange(start, end);
- return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
- }
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
- const isValidColor = (strColor: string) => {
- const s = new Option().style;
- s.color = strColor;
- return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
- };
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
- if (isValidColor(color)) {
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
- }
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
- return null;
- }),
+ return null;
+ }),
// 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;
-
- return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
- }),
-
- // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
- // [[<fieldKey> : <Doc>]]
- // [[:Doc]] => hyperlink
- // [[fieldKey]] => show field
+ new InputRule(new RegExp(/%%$/), (state, match, start, end) => {
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+
+ return marks
+ ? Array.from(marks)
+ .filter(m => m.type !== state.schema.marks.user_mark)
+ .reduce((tr, m) => tr.removeStoredMark(m), tr)
+ : tr;
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ // [[<fieldKey> : <Doc>]]
+ // [[:Doc]] => hyperlink
+ // [[fieldKey]] => show field
// [[fieldKey=value]] => show field and also set its value
// [[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 rawdocid = match[3];
- const docid = rawdocid ? normalizeEmail((!rawdocid.includes("@") ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1))) : undefined;
- const value = match[2]?.substring(1);
- 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: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined);
-
- const fstate = this.TextBox.EditorView?.state;
- if (fstate && selection) {
- this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
- }
- });
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
- }
- return state.tr;
- }
- if (value !== "" && value !== undefined) {
- const num = value.match(/^[0-9.]$/);
- this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value);
+ 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 rawdocid = match[3];
+ const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined;
+ const value = match[2]?.substring(1);
+ 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: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid);
+ DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { 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))));
+ }
+ });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
- return state.tr.deleteRange(start, end).insert(start, fieldView);
- }),
+ return state.tr;
+ }
+ if (value !== '' && value !== undefined) {
+ 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 });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+ // wiki:title
+ new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => {
+ const title = match[1];
+ this.TextBox.EditorView?.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))));
+
+ this.TextBox.makeLinkAnchor(undefined, 'add:right', `https://en.wikipedia.org/wiki/${title.trim()}`, 'wikipedia reference');
+
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate) {
+ const tr = fstate?.tr.deleteRange(start, start + 5);
+ return tr.setSelection(new TextSelection(tr.doc.resolve(end - 5))).insertText(' ');
+ }
+ return state.tr;
+ }),
- // create an inline view of a document {{ <layoutKey> : <Doc> }}
- // {{:Doc}} => show default view of document
- // {{<layout>}} => show layout for this doc
+ // 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 => {
+ 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;
- }),
+ 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 + ':'}`;
- }
- const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag });
- return state.tr.deleteRange(start, end).insert(start, fieldView);
- }),
-
+ 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 + ':'}`;
+ }
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
+ return state.tr.deleteRange(start, end).insert(start, fieldView).insertText(' ');
+ }),
// # heading
- textblockTypeInputRule(
- new RegExp(/^(#{1,6})\s$/),
- schema.nodes.heading,
- match => {
- return ({ level: match[1].length });
- }
- ),
+ textblockTypeInputRule(new RegExp(/^(#{1,6})\s$/), schema.nodes.heading, match => {
+ return { level: match[1].length };
+ }),
// set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/[ti!x]$/),
- (state, match, start, end) => {
-
- if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
-
- const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
- const node = (state.doc.resolve(start) as any).nodeAfter;
+ new InputRule(new RegExp(/[ti!x]$/), (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
- if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ const tag = match[0] === 't' ? 'todo' : match[0] === 'i' ? 'ignore' : match[0] === 'x' ? 'disagree' : match[0] === '!' ? 'important' : '??';
+ const node = (state.doc.resolve(start) as any).nodeAfter;
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
- }),
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
- new InputRule(
- new RegExp(/%\(/),
- (state, match, start, end) => {
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const sm = state.storedMarks || [];
- const mark = state.schema.marks.summarizeInclusive.create();
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
- sm.push(mark);
- const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
- 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;
+ new InputRule(new RegExp(/%\(/), (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks?.slice() || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
- }),
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ 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;
- new InputRule(
- new RegExp(/%\)/),
- (state, match, start, end) => {
- return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
- }),
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ }),
- ]
+ new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
+ }),
+ ],
};
}
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index c017db034..01acc3de9 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -1,35 +1,49 @@
-import { TextSelection } from "prosemirror-state";
-import { Fragment, Node, Slice } from "prosemirror-model";
+import { TextSelection } from 'prosemirror-state';
+import { Fragment, Node, Slice } from 'prosemirror-model';
import * as ReactDOM from 'react-dom';
-import React = require("react");
+import React = require('react');
// an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked.
// this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't
// really need a react view. However, it would be cleaner to figure out how to do this just as a react rendering
// method instead of changing prosemirror's text when the expand/elide buttons are clicked.
export class SummaryView {
- _fieldWrapper: HTMLSpanElement; // container for label and value
+ dom: HTMLSpanElement; // container for label and value
constructor(node: any, view: any, getPos: any) {
const self = this;
- this._fieldWrapper = document.createElement("span");
- this._fieldWrapper.className = this.className(node.attrs.visibility);
- this._fieldWrapper.onpointerdown = function (e: any) { self.onPointerDown(e, node, view, getPos); };
- this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
- this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
+ this.dom = document.createElement('span');
+ this.dom.className = this.className(node.attrs.visibility);
+ this.dom.onpointerdown = function (e: any) {
+ self.onPointerDown(e, node, view, getPos);
+ };
+ this.dom.onkeypress = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeydown = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onkeyup = function (e: any) {
+ e.stopPropagation();
+ };
+ this.dom.onmousedown = function (e: any) {
+ e.stopPropagation();
+ };
const js = node.toJSON;
- node.toJSON = function () { return js.apply(this, arguments); };
+ node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
- ReactDOM.render(<SummaryViewInternal />, this._fieldWrapper);
- (this as any).dom = this._fieldWrapper;
+ ReactDOM.render(<SummaryViewInternal />, this.dom);
+ (this as any).dom = this.dom;
}
- className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
- destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); }
- selectNode() { }
+ className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this.dom);
+ }
+ selectNode() {}
updateSummarizedText(start: any, view: any) {
const mtype = view.state.schema.marks.summarize;
@@ -44,8 +58,7 @@ export class SummaryView {
if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
visited.add(node);
endPos = i + node.nodeSize - 1;
- }
- else skip = true;
+ } else skip = true;
}
});
}
@@ -56,26 +69,28 @@ export class SummaryView {
const visible = !node.attrs.visibility;
const attrs = { ...node.attrs, visibility: visible };
let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
- if (!visible) { // update summarized text and save in attrs
+ if (!visible) {
+ // update summarized text and save in attrs
textSelection = this.updateSummarizedText(getPos() + 1, view);
attrs.text = textSelection.content();
attrs.textslice = attrs.text.toJSON();
}
- view.dispatch(view.state.tr.
- setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
- replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
- setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
+ view.dispatch(
+ view.state.tr
+ .setSelection(textSelection) // select the current summarized text (or where it will be if its collapsed)
+ .replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text) // collapse/expand it
+ .setNodeMarkup(getPos(), undefined, attrs)
+ ); // update the attrs
e.preventDefault();
e.stopPropagation();
- this._fieldWrapper.className = this.className(visible);
- }
+ this.dom.className = this.className(visible);
+ };
}
-interface ISummaryView {
-}
+interface ISummaryView {}
// currently nothing needs to be rendered for the internal view of a summary.
export class SummaryViewInternal extends React.Component<ISummaryView> {
render() {
return <> </>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 6103a28d6..00c41e187 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -1,52 +1,117 @@
-import React = require("react");
-import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
-import { Doc } from "../../../../fields/Doc";
+import React = require('react');
+import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model';
+import { Doc } from '../../../../fields/Doc';
-
-const emDOM: DOMOutputSpecArray = ["em", 0];
-const strongDOM: DOMOutputSpecArray = ["strong", 0];
-const codeDOM: DOMOutputSpecArray = ["code", 0];
+const emDOM: DOMOutputSpec = ['em', 0];
+const strongDOM: DOMOutputSpec = ['strong', 0];
+const codeDOM: DOMOutputSpec = ['code', 0];
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks: { [index: string]: MarkSpec } = {
splitter: {
attrs: {
- id: { default: "" }
+ id: { default: '' },
},
toDOM(node: any) {
- return ["div", { className: "dummy" }, 0];
- }
+ return ['div', { className: 'dummy' }, 0];
+ },
},
- // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each link has an href URL and a title for use in menus and hover (Dash links have linkIDs & targetIDs). `title`
+
+ // :: MarkSpec an autoLinkAnchor. These are automatically generated anchors to "published" documents based on the anchor text matching the
+ // published document's title.
+ // NOTE: unlike linkAnchors, the autoLinkAnchor's href's indicate the target anchor of the hyperlink and NOT the source. This is because
+ // automatic links do not create a text selection Marker document for the source anchor, but use the text document itself. Since
+ // multiple automatic links can be created each having the same source anchor (the whole document), the target href of the link is needed to
+ // disambiguate links from one another.
+ // Rendered and parsed as an `<a>`
+ // element.
+ autoLinkAnchor: {
+ attrs: {
+ allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
+ location: { default: null },
+ title: { default: null },
+ },
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: 'a[href]',
+ getAttrs(dom: any) {
+ return {
+ location: dom.getAttribute('location'),
+ title: dom.getAttribute('title'),
+ };
+ },
+ },
+ ],
+ 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];
+ },
+ },
+ noAutoLinkAnchor: {
+ attrs: {},
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: 'div',
+ getAttrs(dom: any) {
+ return {
+ noAutoLink: dom.getAttribute('data-noAutoLink'),
+ };
+ },
+ },
+ ],
+ toDOM(node: any) {
+ return ['span', { 'data-noAutoLink': 'true' }, 0];
+ },
+ },
+ // :: MarkSpec A linkAnchor. The anchor can have multiple links, where each linkAnchor specifies an href to the URL of the source selection Marker text,
+ // and a title for use in menus and hover. `title`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
linkAnchor: {
attrs: {
- allAnchors: { default: [] as { href: string, title: string, anchorId: string }[] },
+ allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
location: { default: null },
title: { default: null },
- docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
+ docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
},
inclusive: false,
- parseDOM: [{
- tag: "a[href]", getAttrs(dom: any) {
- return {
- location: dom.getAttribute("location"),
- title: dom.getAttribute("title")
- };
- }
- }],
+ parseDOM: [
+ {
+ tag: 'a[href]',
+ getAttrs(dom: any) {
+ return {
+ location: dom.getAttribute('location'),
+ title: dom.getAttribute('title'),
+ };
+ },
+ },
+ ],
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 node.attrs.docref && node.attrs.title ?
- ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", {
- ...node.attrs,
- class: "prosemirror-attribution",
- href: node.attrs.allAnchors[0].href,
- }, node.attrs.title], ["br"]] :
- //node.attrs.allLinks.length === 1 ?
- ["a", { class: anchorids, "data-targethrefs": targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 0];
+ 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 node.attrs.docref && node.attrs.title
+ ? [
+ 'div',
+ ['span', `"`],
+ ['span', 0],
+ ['span', `"`],
+ ['br'],
+ [
+ 'a',
+ {
+ ...node.attrs,
+ class: 'prosemirror-attribution',
+ href: node.attrs.allAnchors[0].href,
+ },
+ node.attrs.title,
+ ],
+ ['br'],
+ ]
+ : //node.attrs.allLinks.length === 1 ?
+ ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 0];
// ["div", { class: "prosemirror-anchor" },
// ["span", { class: "prosemirror-linkBtn" },
// ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
@@ -56,254 +121,273 @@ export const marks: { [index: string]: MarkSpec } = {
// ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title]
// )]
// ];
- }
+ },
},
/** FONT SIZES */
pFontSize: {
- attrs: { fontSize: { default: "10px" } },
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : "" };
- }
- }],
- toDOM: (node) => node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize};` }] : ['span', 0]
+ attrs: { fontSize: { default: '10px' } },
+ parseDOM: [
+ {
+ tag: 'span',
+ getAttrs(dom: any) {
+ return { fontSize: dom.style.fontSize ? dom.style.fontSize.toString() : '' };
+ },
+ },
+ ],
+ toDOM: node => (node.attrs.fontSize ? ['span', { style: `font-size: ${node.attrs.fontSize};` }] : ['span', 0]),
},
/* FONTS */
pFontFamily: {
- attrs: { family: { default: "" } },
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- const cstyle = getComputedStyle(dom);
- if (cstyle.font) {
- if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
- if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
- if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
- if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
- if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
- if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
- }
- }
- }],
- toDOM: (node) => node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0]
+ attrs: { family: { default: '' } },
+ parseDOM: [
+ {
+ tag: 'span',
+ getAttrs(dom: any) {
+ const cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf('Times New Roman') !== -1) return { family: 'Times New Roman' };
+ if (cstyle.font.indexOf('Arial') !== -1) return { family: 'Arial' };
+ if (cstyle.font.indexOf('Georgia') !== -1) return { family: 'Georgia' };
+ if (cstyle.font.indexOf('Comic Sans') !== -1) return { family: 'Comic Sans MS' };
+ if (cstyle.font.indexOf('Tahoma') !== -1) return { family: 'Tahoma' };
+ if (cstyle.font.indexOf('Crimson') !== -1) return { family: 'Crimson Text' };
+ }
+ return { family: '' };
+ },
+ },
+ ],
+ toDOM: node => (node.attrs.family ? ['span', { style: `font-family: "${node.attrs.family}";` }] : ['span', 0]),
},
// :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
pFontColor: {
- attrs: { color: { default: "" } },
+ attrs: { color: { default: '' } },
inclusive: true,
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- return { color: dom.getAttribute("color") };
- }
- }],
- toDOM: (node) => node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]
+ parseDOM: [
+ {
+ tag: 'span',
+ getAttrs(dom: any) {
+ return { color: dom.getAttribute('color') };
+ },
+ },
+ ],
+ toDOM: node => (node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0]),
},
marker: {
attrs: {
- highlight: { default: "transparent" }
+ highlight: { default: 'transparent' },
},
inclusive: true,
- parseDOM: [{
- tag: "span", getAttrs(dom: any) {
- return { highlight: dom.getAttribute("backgroundColor") };
- }
- }],
+ parseDOM: [
+ {
+ tag: 'span',
+ getAttrs(dom: any) {
+ return { highlight: dom.getAttribute('backgroundColor') };
+ },
+ },
+ ],
toDOM(node: any) {
return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
- }
+ },
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
- parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
- toDOM() { return emDOM; }
+ parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style: italic' }],
+ toDOM() {
+ return emDOM;
+ },
},
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`.
strong: {
- parseDOM: [{ tag: "strong" },
- { tag: "b" },
- { style: "font-weight" }],
- toDOM() { return strongDOM; }
+ parseDOM: [{ tag: 'strong' }, { tag: 'b' }, { style: 'font-weight' }],
+ toDOM() {
+ return strongDOM;
+ },
},
strikethrough: {
- parseDOM: [
- { tag: 'strike' },
- { style: 'text-decoration=line-through' },
- { style: 'text-decoration-line=line-through' }
+ parseDOM: [{ tag: 'strike' }, { style: 'text-decoration=line-through' }, { style: 'text-decoration-line=line-through' }],
+ toDOM: () => [
+ 'span',
+ {
+ style: 'text-decoration-line:line-through',
+ },
],
- toDOM: () => ['span', {
- style: 'text-decoration-line:line-through'
- }]
},
subscript: {
excludes: 'superscript',
- parseDOM: [
- { tag: 'sub' },
- { style: 'vertical-align=sub' }
- ],
- toDOM: () => ['sub']
+ parseDOM: [{ tag: 'sub' }, { style: 'vertical-align=sub' }],
+ toDOM: () => ['sub'],
},
superscript: {
excludes: 'subscript',
- parseDOM: [
- { tag: 'sup' },
- { style: 'vertical-align=super' }
- ],
- toDOM: () => ['sup']
+ parseDOM: [{ tag: 'sup' }, { style: 'vertical-align=super' }],
+ toDOM: () => ['sup'],
},
mbulletType: {
attrs: {
- bulletType: { default: "decimal" }
+ bulletType: { default: 'decimal' },
},
toDOM(node: any) {
- return ['span', {
- style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
- }];
- }
+ return [
+ 'span',
+ {
+ style: `background: ${node.attrs.bulletType === 'decimal' ? 'yellow' : node.attrs.bulletType === 'upper-alpha' ? 'blue' : 'green'}`,
+ },
+ ];
+ },
},
metadata: {
toDOM() {
return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
- }
+ },
},
metadataKey: {
toDOM() {
return ['span', { style: 'font-style:italic; ' }];
- }
+ },
},
metadataVal: {
toDOM() {
return ['span'];
- }
+ },
},
summarizeInclusive: {
parseDOM: [
{
- tag: "span",
+ tag: 'span',
getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
+ if (typeof p !== 'string') {
const style = getComputedStyle(p);
- if (style.textDecoration === "underline") return null;
- if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
- p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
+ if (style.textDecoration === 'underline') return null;
+ if (p.parentElement.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement.outerHTML.indexOf('text-decoration-style: solid') !== -1) {
return null;
}
}
return false;
- }
+ },
},
],
inclusive: true,
toDOM() {
- return ['span', {
- style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
- }];
- }
+ return [
+ 'span',
+ {
+ style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)',
+ },
+ ];
+ },
},
summarize: {
inclusive: false,
parseDOM: [
{
- tag: "span",
+ tag: 'span',
getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
+ if (typeof p !== 'string') {
const style = getComputedStyle(p);
- if (style.textDecoration === "underline") return null;
- if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
- p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ if (style.textDecoration === 'underline') return null;
+ if (p.parentElement.outerHTML.indexOf('text-decoration: underline') !== -1 && p.parentElement.outerHTML.indexOf('text-decoration-style: dotted') !== -1) {
return null;
}
}
return false;
- }
+ },
},
],
toDOM() {
- return ['span', {
- style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
- }];
- }
+ return [
+ 'span',
+ {
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)',
+ },
+ ];
+ },
},
underline: {
parseDOM: [
{
- tag: "span",
+ tag: 'span',
getAttrs: (p: any) => {
- if (typeof (p) !== "string") {
+ if (typeof p !== 'string') {
const style = getComputedStyle(p);
- if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
+ if (style.textDecoration === 'underline' || p.parentElement.outerHTML.indexOf('text-decoration-style:line') !== -1) {
return null;
}
}
return false;
- }
- }
+ },
+ },
// { style: "text-decoration=underline" }
],
- toDOM: () => ['span', {
- style: 'text-decoration:underline;text-decoration-style:line'
- }]
+ toDOM: () => [
+ 'span',
+ {
+ style: 'text-decoration:underline;text-decoration-style:line',
+ },
+ ],
},
search_highlight: {
attrs: {
- selected: { default: false }
+ selected: { default: false },
},
parseDOM: [{ style: 'background: yellow' }],
toDOM(node: any) {
- return ['span', { style: `background: ${node.attrs.selected ? "orange" : "yellow"}` }];
- }
+ return ['span', { style: `background: ${node.attrs.selected ? 'orange' : 'yellow'}` }];
+ },
},
// the id of the user who entered the text
user_mark: {
attrs: {
- userid: { default: "" },
- modified: { default: "when?" }, // 1 second intervals since 1970
+ userid: { default: '' },
+ modified: { default: 'when?' }, // 1 second intervals since 1970
},
- excludes: "user_mark",
- group: "inline",
+ excludes: 'user_mark',
+ group: 'inline',
toDOM(node: any) {
- const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ const uid = node.attrs.userid.replace('.', '').replace('@', '');
const min = Math.round(node.attrs.modified / 12);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
- const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " UM-remote" : "";
- return ['span', { class: "UM-" + uid + remote + " UM-min-" + min + " UM-hr-" + hr + " UM-day-" + day }, 0];
- }
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';
+ return ['span', { class: 'UM-' + uid + remote + ' UM-min-' + min + ' UM-hr-' + hr + ' UM-day-' + day }, 0];
+ },
},
// the id of the user who entered the text
user_tag: {
attrs: {
- userid: { default: "" },
- modified: { default: "when?" }, // 1 second intervals since 1970
- tag: { default: "" }
+ userid: { default: '' },
+ modified: { default: 'when?' }, // 1 second intervals since 1970
+ tag: { default: '' },
},
- group: "inline",
+ group: 'inline',
inclusive: false,
toDOM(node: any) {
- const uid = node.attrs.userid.replace(".", "").replace("@", "");
- return ['span', { class: "UT-" + uid + " UT-" + node.attrs.tag }, 0];
- }
+ const uid = node.attrs.userid.replace('.', '').replace('@', '');
+ return ['span', { class: 'UT-' + uid + ' UT-' + node.attrs.tag }, 0];
+ },
},
-
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
- parseDOM: [{ tag: "code" }],
- toDOM() { return codeDOM; }
+ parseDOM: [{ tag: 'code' }],
+ toDOM() {
+ return codeDOM;
+ },
},
};
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 2fe0a67cb..5142b7da6 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -1,15 +1,18 @@
-import React = require("react");
-import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
-import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from "./ParagraphNodeSpec";
+import React = require('react');
+import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model';
+import { listItem, orderedList } from 'prosemirror-schema-list';
+import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec';
-const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
- preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+const blockquoteDOM: DOMOutputSpec = ['blockquote', 0],
+ hrDOM: DOMOutputSpec = ['hr'],
+ preDOM: DOMOutputSpec = ['pre', ['code', 0]],
+ brDOM: DOMOutputSpec = ['br'],
+ ulDOM: DOMOutputSpec = ['ul', 0];
function formatAudioTime(time: number) {
time = Math.round(time);
const hours = Math.floor(time / 60 / 60);
- const minutes = Math.floor(time / 60) - (hours * 60);
+ const minutes = Math.floor(time / 60) - hours * 60;
const seconds = time % 60;
return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0');
@@ -19,67 +22,70 @@ function formatAudioTime(time: number) {
export const nodes: { [index: string]: NodeSpec } = {
// :: NodeSpec The top level document node.
doc: {
- content: "block+"
+ content: 'block+',
},
paragraph: ParagraphNodeSpec,
audiotag: {
- group: "block",
+ group: 'block',
attrs: {
timeCode: { default: 0 },
- audioId: { default: "" },
- textId: { default: "" }
+ audioId: { default: '' },
+ textId: { default: '' },
},
toDOM(node) {
- return ['audiotag',
+ return [
+ 'audiotag',
{
class: node.attrs.textId,
// style: see FormattedTextBox.scss
- "data-timecode": node.attrs.timeCode,
- "data-audioid": node.attrs.audioId,
- "data-textid": node.attrs.textId,
+ 'data-timecode': node.attrs.timeCode,
+ 'data-audioid': node.attrs.audioId,
+ 'data-textid': node.attrs.textId,
},
- formatAudioTime(node.attrs.timeCode.toString())
+ formatAudioTime(node.attrs.timeCode.toString()),
];
},
parseDOM: [
{
- tag: "audiotag", getAttrs(dom: any) {
+ tag: 'audiotag',
+ getAttrs(dom: any) {
return {
- timeCode: dom.getAttribute("data-timecode"),
- audioId: dom.getAttribute("data-audioid"),
- textId: dom.getAttribute("data-textid")
+ timeCode: dom.getAttribute('data-timecode'),
+ audioId: dom.getAttribute('data-audioid'),
+ textId: dom.getAttribute('data-textid'),
};
- }
+ },
},
- ]
+ ],
},
footnote: {
- group: "inline",
- content: "inline*",
+ group: 'inline',
+ content: 'inline*',
inline: true,
attrs: {
- visibility: { default: false }
+ visibility: { default: false },
},
// This makes the view treat the node as a leaf, even though it
// technically has content
atom: true,
- toDOM: () => ["footnote", 0],
- parseDOM: [{ tag: "footnote" }]
+ toDOM: () => ['footnote', 0],
+ parseDOM: [{ tag: 'footnote' }],
},
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
- content: "block*",
- group: "block",
+ content: 'block*',
+ group: 'block',
defining: true,
- parseDOM: [{ tag: "blockquote" }],
- toDOM() { return blockquoteDOM; }
+ parseDOM: [{ tag: 'blockquote' }],
+ toDOM() {
+ return blockquoteDOM;
+ },
},
-
// blockquote: {
// ...ParagraphNodeSpec,
// defining: true,
@@ -97,9 +103,11 @@ export const nodes: { [index: string]: NodeSpec } = {
// :: NodeSpec A horizontal rule (`<hr>`).
horizontal_rule: {
- group: "block",
- parseDOM: [{ tag: "hr" }],
- toDOM() { return hrDOM; }
+ group: 'block',
+ parseDOM: [{ tag: 'hr' }],
+ toDOM() {
+ return hrDOM;
+ },
},
// :: NodeSpec A heading textblock, with a `level` attribute that
@@ -112,12 +120,14 @@ export const nodes: { [index: string]: NodeSpec } = {
level: { default: 1 },
},
defining: true,
- parseDOM: [{ tag: "h1", attrs: { level: 1 } },
- { tag: "h2", attrs: { level: 2 } },
- { tag: "h3", attrs: { level: 3 } },
- { tag: "h4", attrs: { level: 4 } },
- { tag: "h5", attrs: { level: 5 } },
- { tag: "h6", attrs: { level: 6 } }],
+ parseDOM: [
+ { tag: 'h1', attrs: { level: 1 } },
+ { tag: 'h2', attrs: { level: 2 } },
+ { tag: 'h3', attrs: { level: 3 } },
+ { tag: 'h4', attrs: { level: 4 } },
+ { tag: 'h5', attrs: { level: 5 } },
+ { tag: 'h6', attrs: { level: 6 } },
+ ],
toDOM(node) {
const dom = toParagraphDOM(node) as any;
const level = node.attrs.level || 1;
@@ -129,36 +139,38 @@ export const nodes: { [index: string]: NodeSpec } = {
const level = Number(dom.nodeName.substring(1)) || 1;
attrs.level = level;
return attrs;
- }
+ },
},
// :: NodeSpec A code listing. Disallows marks or non-text inline
// nodes by default. Represented as a `<pre>` element with a
// `<code>` element inside of it.
code_block: {
- content: "inline*",
- marks: "_",
- group: "block",
+ content: 'inline*',
+ marks: '_',
+ group: 'block',
code: true,
defining: true,
- parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
- toDOM() { return preDOM; }
+ parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
+ toDOM() {
+ return preDOM;
+ },
},
// :: NodeSpec The text node.
text: {
- group: "inline"
+ group: 'inline',
},
dashComment: {
attrs: {
- docid: { default: "" },
+ docid: { default: '' },
},
inline: true,
- group: "inline",
+ group: 'inline',
toDOM(node) {
const attrs = { style: `width: 40px` };
- return ["span", { ...node.attrs, ...attrs }, "←"];
+ return ['span', { ...node.attrs, ...attrs }, '←'];
},
},
@@ -169,10 +181,10 @@ export const nodes: { [index: string]: NodeSpec } = {
text: { default: undefined },
textslice: { default: undefined },
},
- group: "inline",
+ group: 'inline',
toDOM(node) {
const attrs = { style: `width: 40px` };
- return ["span", { ...node.attrs, ...attrs }];
+ return ['span', { ...node.attrs, ...attrs }];
},
},
@@ -187,27 +199,30 @@ export const nodes: { [index: string]: NodeSpec } = {
width: { default: 100 },
alt: { default: null },
title: { default: null },
- float: { default: "left" },
- location: { default: "add:right" },
- docid: { default: "" }
+ float: { default: 'left' },
+ location: { default: 'add:right' },
+ docid: { default: '' },
},
- group: "inline",
+ group: 'inline',
draggable: true,
- parseDOM: [{
- tag: "img[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt"),
- width: Math.min(100, Number(dom.getAttribute("width"))),
- };
- }
- }],
+ parseDOM: [
+ {
+ tag: 'img[src]',
+ getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute('src'),
+ title: dom.getAttribute('title'),
+ alt: dom.getAttribute('alt'),
+ width: Math.min(100, Number(dom.getAttribute('width'))),
+ };
+ },
+ },
+ ],
// TODO if we don't define toDom, dragging the image crashes. Why?
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}` };
- return ["img", { ...node.attrs, ...attrs }];
- }
+ return ['img', { ...node.attrs, ...attrs }];
+ },
},
dashDoc: {
@@ -216,82 +231,87 @@ export const nodes: { [index: string]: NodeSpec } = {
width: { default: 200 },
height: { default: 100 },
title: { default: null },
- float: { default: "right" },
+ float: { default: 'right' },
hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
- fieldKey: { default: "" },
- docid: { default: "" },
- alias: { default: "" }
+ fieldKey: { default: '' },
+ docid: { default: '' },
+ alias: { default: '' },
},
- group: "inline",
+ group: 'inline',
draggable: false,
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
- return ["div", { ...node.attrs, ...attrs }];
- }
+ return ['div', { ...node.attrs, ...attrs }];
+ },
},
dashField: {
inline: true,
attrs: {
- fieldKey: { default: "" },
- docid: { default: "" },
- hideKey: { default: false }
+ fieldKey: { default: '' },
+ docid: { default: '' },
+ hideKey: { default: false },
},
- group: "inline",
+ group: 'inline',
draggable: false,
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
- return ["div", { ...node.attrs, ...attrs }];
- }
+ return ['div', { ...node.attrs, ...attrs }];
+ },
},
equation: {
inline: true,
attrs: {
- fieldKey: { default: "" },
+ fieldKey: { default: '' },
},
atom: true,
- group: "inline",
+ group: 'inline',
draggable: false,
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
- return ["div", { ...node.attrs, ...attrs }];
- }
+ return ['div', { ...node.attrs, ...attrs }];
+ },
},
video: {
inline: true,
attrs: {
src: {},
- width: { default: "100px" },
+ width: { default: '100px' },
alt: { default: null },
- title: { default: null }
+ title: { default: null },
},
- group: "inline",
+ group: 'inline',
draggable: true,
- parseDOM: [{
- tag: "video[src]", getAttrs(dom: any) {
- return {
- src: dom.getAttribute("src"),
- title: dom.getAttribute("title"),
- alt: dom.getAttribute("alt"),
- width: Math.min(100, Number(dom.getAttribute("width"))),
- };
- }
- }],
+ parseDOM: [
+ {
+ tag: 'video[src]',
+ getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute('src'),
+ title: dom.getAttribute('title'),
+ alt: dom.getAttribute('alt'),
+ width: Math.min(100, Number(dom.getAttribute('width'))),
+ };
+ },
+ },
+ ],
toDOM(node) {
const attrs = { style: `width: ${node.attrs.width}` };
- return ["video", { ...node.attrs, ...attrs }];
- }
+ return ['video', { ...node.attrs, ...attrs }];
+ },
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
- group: "inline",
+ group: 'inline',
selectable: false,
- parseDOM: [{ tag: "br" }],
- toDOM() { return brDOM; }
+ parseDOM: [{ tag: 'br' }],
+ toDOM() {
+ return brDOM;
+ },
},
ordered_list: {
@@ -300,85 +320,108 @@ export const nodes: { [index: string]: NodeSpec } = {
group: 'block',
attrs: {
bulletStyle: { default: 0 },
- mapStyle: { default: "decimal" },// "decimal", "multi", "bullet"
- fontColor: { default: "inherit" },
+ mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet"
+ fontColor: { default: 'inherit' },
fontSize: { default: undefined },
fontFamily: { default: undefined },
visibility: { default: true },
- indent: { default: undefined }
+ indent: { default: undefined },
},
parseDOM: [
{
- tag: "ul", getAttrs(dom: any) {
+ tag: 'ul',
+ getAttrs(dom: any) {
return {
- bulletStyle: dom.getAttribute("data-bulletStyle"),
- mapStyle: dom.getAttribute("data-mapStyle"),
+ bulletStyle: dom.getAttribute('data-bulletStyle'),
+ mapStyle: dom.getAttribute('data-mapStyle'),
fontColor: dom.style.color,
- fontSize: dom.style["font-size"],
- fontFamily: dom.style["font-family"],
- indent: dom.style["margin-left"]
+ fontSize: dom.style['font-size'],
+ fontFamily: dom.style['font-family'],
+ indent: dom.style['margin-left'],
};
- }
+ },
},
{
- style: 'list-style-type=disc', getAttrs(dom: any) {
- return { mapStyle: "bullet" };
- }
+ style: 'list-style-type=disc',
+ getAttrs(dom: any) {
+ return { mapStyle: 'bullet' };
+ },
},
{
- tag: "ol", getAttrs(dom: any) {
+ tag: 'ol',
+ getAttrs(dom: any) {
return {
- bulletStyle: dom.getAttribute("data-bulletStyle"),
- mapStyle: dom.getAttribute("data-mapStyle"),
+ bulletStyle: dom.getAttribute('data-bulletStyle'),
+ mapStyle: dom.getAttribute('data-mapStyle'),
fontColor: dom.style.color,
- fontSize: dom.style["font-size"],
- fontFamily: dom.style["font-family"],
- indent: dom.style["margin-left"]
+ fontSize: dom.style['font-size'],
+ fontFamily: dom.style['font-family'],
+ indent: dom.style['margin-left'],
};
- }
- }],
- toDOM(node: Node<any>) {
- const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
- const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : "";
- const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : "";
- const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : "";
- const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : "";
- if (node.attrs.mapStyle === "bullet") {
- return ['ul', {
- "data-mapStyle": node.attrs.mapStyle,
- "data-bulletStyle": node.attrs.bulletStyle,
- style: `${fsize} ${ffam} ${fcol} ${marg}`
- }, 0];
+ },
+ },
+ ],
+ toDOM(node: Node) {
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : '';
+ const fsize = node.attrs.fontSize ? `font-size: ${node.attrs.fontSize};` : '';
+ const ffam = node.attrs.fontFamily ? `font-family:${node.attrs.fontFamily};` : '';
+ const fcol = node.attrs.fontColor ? `color: ${node.attrs.fontColor};` : '';
+ const marg = node.attrs.indent ? `margin-left: ${node.attrs.indent};` : '';
+ if (node.attrs.mapStyle === 'bullet') {
+ return [
+ 'ul',
+ {
+ 'data-mapStyle': node.attrs.mapStyle,
+ 'data-bulletStyle': node.attrs.bulletStyle,
+ style: `${fsize} ${ffam} ${fcol} ${marg}`,
+ },
+ 0,
+ ];
}
- return node.attrs.visibility ?
- ['ol', {
- class: `${map}-ol`,
- "data-mapStyle": node.attrs.mapStyle,
- "data-bulletStyle": node.attrs.bulletStyle,
- style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`
- }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
- }
+ return node.attrs.visibility
+ ? [
+ 'ol',
+ {
+ class: `${map}-ol`,
+ 'data-mapStyle': node.attrs.mapStyle,
+ 'data-bulletStyle': node.attrs.bulletStyle,
+ style: `list-style: none; ${fsize} ${ffam} ${fcol} ${marg}`,
+ },
+ 0,
+ ]
+ : ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ },
},
list_item: {
...listItem,
attrs: {
bulletStyle: { default: 0 },
- mapStyle: { default: "decimal" }, // "decimal", "multi", "bullet"
- visibility: { default: true }
+ mapStyle: { default: 'decimal' }, // "decimal", "multi", "bullet"
+ visibility: { default: true },
},
content: '(paragraph|audiotag)+ | ((paragraph|audiotag)+ ordered_list)',
- parseDOM: [{
- tag: "li", getAttrs(dom: any) {
- return { mapStyle: dom.getAttribute("data-mapStyle"), bulletStyle: dom.getAttribute("data-bulletStyle") };
- }
- }],
+ parseDOM: [
+ {
+ tag: 'li',
+ getAttrs(dom: any) {
+ return { mapStyle: dom.getAttribute('data-mapStyle'), bulletStyle: dom.getAttribute('data-bulletStyle') };
+ },
+ },
+ ],
toDOM(node: any) {
- const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
- return ["li", { class: `${map}`, "data-mapStyle": node.attrs.mapStyle, "data-bulletStyle": node.attrs.bulletStyle }, node.attrs.visibility ? 0 :
- ["span", { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== "bullet" ? "inline-block" : "list-item"}; text-overflow: ellipsis; white-space: pre` },
- `${node.firstChild?.textContent}...`]];
- }
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : '';
+ return [
+ 'li',
+ { class: `${map}`, 'data-mapStyle': node.attrs.mapStyle, 'data-bulletStyle': node.attrs.bulletStyle },
+ node.attrs.visibility
+ ? 0
+ : [
+ 'span',
+ { style: `position: relative; width: 100%; height: 1.5em; overflow: hidden; display: ${node.attrs.mapStyle !== 'bullet' ? 'inline-block' : 'list-item'}; text-overflow: ellipsis; white-space: pre` },
+ `${node.firstChild?.textContent}...`,
+ ],
+ ];
+ },
},
-}; \ No newline at end of file
+};
diff --git a/src/client/views/nodes/formattedText/schema_rts.ts b/src/client/views/nodes/formattedText/schema_rts.ts
index 83561073c..d6e0e6002 100644
--- a/src/client/views/nodes/formattedText/schema_rts.ts
+++ b/src/client/views/nodes/formattedText/schema_rts.ts
@@ -1,8 +1,7 @@
-import { Schema, Slice } from "prosemirror-model";
-
-import { nodes } from "./nodes_rts";
-import { marks } from "./marks_rts";
+import { Schema, Slice } from 'prosemirror-model';
+import { nodes } from './nodes_rts';
+import { marks } from './marks_rts';
// :: Schema
// This schema rougly corresponds to the document schema used by
@@ -20,7 +19,9 @@ const fromJson = schema.nodeFromJSON;
schema.nodeFromJSON = (json: any) => {
const node = fromJson(json);
if (json.type === schema.nodes.summary.name) {
- node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
+ // bcz: this is a hacky way to convert the JSON that's serialized for a summary node into the Slice that the summary node wants at run-time.
+ // since attrs are readonly, assigning the text field like this violates the way prosemirror works, but I think we can get away with it.
+ (node.attrs.text as any) = Slice.fromJSON(schema, node.attrs.textslice);
}
return node;
-}; \ No newline at end of file
+};
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index 06932d145..a0a2dd4f8 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -1078,11 +1078,13 @@
.miniPres {
cursor: grab;
position: absolute;
- right: 10;
- top: 10;
+ top: 0;
+ left: 0;
opacity: 0.1;
transition: all 0.4s;
color: $white;
+ width: 100%;
+ height: 100%;
}
.miniPres:hover {
@@ -1101,7 +1103,6 @@
align-items: center;
display: flex;
position: absolute;
- right: 10px;
transition: all 0.2s;
.presPanel-button-text {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 8a0d03ae8..6e5eb3300 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,79 +1,93 @@
-import React = require("react");
-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 { ColorState, SketchPicker } from "react-color";
+import React = require('react');
+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 { ColorState, SketchPicker } from 'react-color';
import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, DocListCastAsync, FieldResult } from "../../../../fields/Doc";
-import { InkTool } from "../../../../fields/InkField";
-import { List } from "../../../../fields/List";
-import { PrefetchProxy } from "../../../../fields/Proxy";
-import { listSpec } from "../../../../fields/Schema";
-import { ScriptField } from "../../../../fields/ScriptField";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
-import { emptyFunction, returnFalse, returnOne, returnTrue } from '../../../../Utils';
-import { Docs } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
-import { DocumentManager } from "../../../util/DocumentManager";
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { undoBatch, UndoManager } from "../../../util/UndoManager";
-import { CollectionDockingView } from "../../collections/CollectionDockingView";
-import { CollectionView, CollectionViewType } 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 { Doc, DocListCast, DocListCastAsync, FieldResult } from '../../../../fields/Doc';
+import { InkTool } from '../../../../fields/InkField';
+import { List } from '../../../../fields/List';
+import { listSpec } from '../../../../fields/Schema';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { SettingsManager } from '../../../util/SettingsManager';
+import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { MarqueeViewBounds } from '../../collections/collectionFreeForm';
+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 { FieldView, FieldViewProps } from '../FieldView';
-import "./PresBox.scss";
-import { PresEffect, PresMovement, PresStatus } from "./PresEnums";
+import './PresBox.scss';
+import { PresEffect, PresMovement, PresStatus } from './PresEnums';
-export class PinProps {
+export interface PinProps {
audioRange?: boolean;
- unpin?: boolean;
setPosition?: boolean;
hidePresBox?: boolean;
+ pinWithView?: PinViewProps;
+ pinDocView?: boolean; // whether the current view specs of the document should be saved the pinned document
+ panelWidth?: number; // panel width and height of the document (used to compute the bounds of the pinned view area)
+ panelHeight?: number;
+}
+
+export interface PinViewProps {
+ bounds: MarqueeViewBounds;
+ scale: number;
}
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PresBox, fieldKey);
+ }
/**
* transitions & effects for documents
* @param renderDoc
* @param layoutDoc
*/
- static renderEffectsDoc(renderDoc: any, layoutDoc: Doc) {
+ static renderEffectsDoc(renderDoc: any, layoutDoc: Doc, presDoc: Doc) {
const effectProps = {
- left: layoutDoc.presEffectDirection === PresEffect.Left,
- right: layoutDoc.presEffectDirection === PresEffect.Right,
- top: layoutDoc.presEffectDirection === PresEffect.Top,
- bottom: layoutDoc.presEffectDirection === PresEffect.Bottom,
+ left: presDoc.presEffectDirection === PresEffect.Left,
+ right: presDoc.presEffectDirection === PresEffect.Right,
+ top: presDoc.presEffectDirection === PresEffect.Top,
+ bottom: presDoc.presEffectDirection === PresEffect.Bottom,
opposite: true,
- delay: layoutDoc.presTransition,
+ delay: presDoc.presTransition,
// when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc,
};
- switch (layoutDoc.presEffect) {
- case PresEffect.Zoom: return (<Zoom {...effectProps}>{renderDoc}</Zoom>);
- case PresEffect.Fade: return (<Fade {...effectProps}>{renderDoc}</Fade>);
- case PresEffect.Flip: return (<Flip {...effectProps}>{renderDoc}</Flip>);
- case PresEffect.Rotate: return (<Rotate {...effectProps}>{renderDoc}</Rotate>);
- case PresEffect.Bounce: return (<Bounce {...effectProps}>{renderDoc}</Bounce>);
- case PresEffect.Roll: return (<Roll {...effectProps}>{renderDoc}</Roll>);
- case PresEffect.Lightspeed: return (<LightSpeed {...effectProps}>{renderDoc}</LightSpeed>);
+ switch (presDoc.presEffect) {
+ case PresEffect.Zoom:
+ return <Zoom {...effectProps}>{renderDoc}</Zoom>;
+ case PresEffect.Fade:
+ return <Fade {...effectProps}>{renderDoc}</Fade>;
+ case PresEffect.Flip:
+ return <Flip {...effectProps}>{renderDoc}</Flip>;
+ case PresEffect.Rotate:
+ return <Rotate {...effectProps}>{renderDoc}</Rotate>;
+ case PresEffect.Bounce:
+ return <Bounce {...effectProps}>{renderDoc}</Bounce>;
+ case PresEffect.Roll:
+ return <Roll {...effectProps}>{renderDoc}</Roll>;
+ case PresEffect.Lightspeed:
+ return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
case PresEffect.None:
- default: return renderDoc;
+ default:
+ return renderDoc;
}
}
public static EffectsProvider(layoutDoc: Doc, renderDoc: any) {
- return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ?
- PresBox.renderEffectsDoc(renderDoc, layoutDoc)
- :
- renderDoc;
+ return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ? PresBox.renderEffectsDoc(renderDoc, layoutDoc, PresBox.Instance.childDocs[PresBox.Instance.itemIndex]) : renderDoc;
}
@observable public static Instance: PresBox;
@@ -90,13 +104,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _expandBoolean: boolean = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
+
+ @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
@observable private transitionTools: boolean = false;
@observable private newDocumentTools: boolean = false;
@observable private progressivizeTools: boolean = false;
@observable private openMovementDropdown: boolean = false;
@observable private openEffectDropdown: boolean = false;
@observable private presentTools: boolean = false;
- @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); }
+ @computed get isTreeOrStack() {
+ return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any);
+ }
+ @computed get isTree() {
+ return this.layoutDoc._viewType === CollectionViewType.Tree;
+ }
+ @computed get presFieldKey() {
+ return StrCast(this.layoutDoc.presFieldKey, 'data');
+ }
+ @computed get childDocs() {
+ return DocListCast(this.rootDoc[this.presFieldKey]);
+ }
+ @observable _treeViewMap: Map<Doc, number> = new Map();
+
@computed get tagDocs() {
const tagDocs: Doc[] = [];
for (const doc of this.childDocs) {
@@ -105,11 +134,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
return tagDocs;
}
- @computed get itemIndex() { return NumCast(this.rootDoc._itemIndex); }
- @computed get activeItem() { return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null); }
- @computed get targetDoc() { return Cast(this.activeItem?.presentationTargetDoc, Doc, null); }
+ @computed get itemIndex() {
+ return NumCast(this.rootDoc._itemIndex);
+ }
+ @computed get activeItem() {
+ return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null);
+ }
+ @computed get targetDoc() {
+ return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
+ }
@computed get scrollable(): boolean {
- //TODO: likely do NOT have to update this for note-taking view, but still worth putting here
if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
else return false;
}
@@ -117,21 +151,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
else return false;
}
- @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); }
constructor(props: any) {
super(props);
- if (Doc.UserDoc().activePresentation = this.rootDoc) runInAction(() => PresBox.Instance = this);
- if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations.
- Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
- title: "pres element template", type: DocumentType.PRESELEMENT, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"
- }));
- // this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement
- // this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent
- // the preselement docs from being part of multiple presentations since they would all have the same field, or we'd have to keep per-presentation data
- // stored on each pres element.
- (this.presElement as Doc).lookupField = ScriptField.MakeFunction("lookupPresBoxField(container, field, data)",
- { field: "string", data: Doc.name, container: Doc.name });
- }
+ if ((Doc.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this));
this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
}
@computed get selectedDocumentView() {
@@ -139,19 +161,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (this._selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc);
}
@computed get isPres(): boolean {
- document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
if (this.selectedDoc?.type === DocumentType.PRES) {
- document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
- document.addEventListener("keydown", PresBox.keyEventsWrapper, true);
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
+ document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
return true;
}
return false;
}
- @computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; }
+ @computed get selectedDoc() {
+ return this.selectedDocumentView?.rootDoc;
+ }
+ _unmounting = false;
@action
componentWillUnmount() {
- document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
+ this._unmounting = true;
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
this._presKeyEventsActive = false;
this.resetPresentation();
// Turn of progressivize editors
@@ -161,38 +187,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
componentDidMount() {
- this.rootDoc.presBox = this.rootDoc;
- this.rootDoc._forceRenderEngine = "timeline";
+ this._unmounting = false;
+ this.rootDoc._forceRenderEngine = 'timeline';
this.layoutDoc.presStatus = PresStatus.Edit;
this.layoutDoc._gridGap = 0;
this.layoutDoc._yMargin = 0;
this.turnOffEdit(true);
- DocListCastAsync((Doc.UserDoc().myTrails as Doc).data).then(pres =>
- !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.UserDoc().myTrails as Doc, "data", this.rootDoc));
- this._disposers.selection = reaction(() => SelectionManager.Views(),
- views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation());
+ DocListCastAsync(Doc.MyTrails.data).then(pres => !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.MyTrails, 'data', this.rootDoc));
+ this._disposers.selection = reaction(
+ () => SelectionManager.Views(),
+ views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()
+ );
}
@action
updateCurrentPresentation = (pres?: Doc) => {
- if (pres) Doc.UserDoc().activePresentation = pres;
- else Doc.UserDoc().activePresentation = this.rootDoc;
- document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
- document.addEventListener("keydown", PresBox.keyEventsWrapper, true);
+ if (pres) Doc.ActivePresentation = pres;
+ else Doc.ActivePresentation = this.rootDoc;
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
+ document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
this._presKeyEventsActive = true;
PresBox.Instance = this;
- }
+ };
// There are still other internal frames and should go through all frames before going to next slide
nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => {
- const currentFrame = Cast(targetDoc?._currentFrame, "number", null);
+ const currentFrame = Cast(targetDoc?._currentFrame, 'number', null);
const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
- targetDoc._viewTransition = "all 1s";
- setTimeout(() => targetDoc._viewTransition = undefined, 1010);
+ targetDoc._viewTransition = 'all 1s';
+ setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
this.nextKeyframe(targetDoc, activeItem);
if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc);
else targetDoc.keyFrameEditing = true;
- }
+ };
_mediaTimer!: [NodeJS.Timeout, Doc];
// 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
@@ -202,44 +229,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration);
}
- // if (targetDoc.type === DocumentType.AUDIO) {
- // if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- // targetDoc._triggerAudio = NumCast(activeItem.presStartTime);
- // this._mediaTimer = [setTimeout(() => targetDoc._audioStop = true, duration * 1000), targetDoc];
- // } else if (targetDoc.type === DocumentType.VID) {
- // targetDoc._triggerVideoStop = true;
- // setTimeout(() => targetDoc._currentTimecode = NumCast(activeItem.presStartTime), 10);
- // setTimeout(() => targetDoc._triggerVideo = true, 20);
- // this._mediaTimer = [setTimeout(() => targetDoc._triggerVideoStop = true, (duration * 1000) + 20), targetDoc];
- // }
- }
+ };
stopTempMedia = (targetDocField: FieldResult) => {
const targetDoc = Cast(targetDocField, Doc, null);
- if (targetDoc?.type === DocumentType.AUDIO) {
- if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- targetDoc._audioStop = true;
- } else if (targetDoc?.type === DocumentType.VID) {
- if (this._mediaTimer && this._mediaTimer[1] === targetDoc) clearTimeout(this._mediaTimer[0]);
- targetDoc._triggerVideoStop = true;
+ if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
+ const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
+ targMedia?.ComponentView?.Pause?.();
}
- }
+ };
- //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
- // No more frames in current doc and next slide is defined, therefore move to next slide
+ //TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
+ // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
+ // No more frames in current doc and next slide is defined, therefore move to next slide
nextSlide = (activeNext: Doc) => {
const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
+ console.info('nextSlide', activeNext.title, targetNext?.title);
let nextSelected = this.itemIndex + 1;
this.gotoDocument(nextSelected, this.activeItem);
for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
if (!this.childDocs[nextSelected].groupWithUp) {
break;
} else {
- console.log("Title: " + this.childDocs[nextSelected].title);
+ console.log('Title: ' + this.childDocs[nextSelected].title);
this.gotoDocument(nextSelected, this.activeItem, true);
}
}
- }
+ };
// Called when the user activates 'next' - to move to the next part of the pres. trail
@action
@@ -247,7 +263,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null);
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const lastFrame = Cast(targetDoc?.lastFrame, "number", null);
+ const lastFrame = Cast(targetDoc?.lastFrame, 'number', null);
const curFrame = NumCast(targetDoc?._currentFrame);
let internalFrames: boolean = false;
if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true;
@@ -255,13 +271,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Case 1: There are still other frames and should go through all frames before going to next slide
this.nextInternalFrame(targetDoc, activeItem);
} else if (this.childDocs[this.itemIndex + 1] !== undefined) {
- // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
+ // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
this.nextSlide(activeNext);
} else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
// Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
this.gotoDocument(0, this.activeItem);
}
- }
+ };
// Called when the user activates 'back' - to move to the previous part of the pres. trail
@action
@@ -270,7 +286,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
- const lastFrame = Cast(targetDoc.lastFrame, "number", null);
+ const lastFrame = Cast(targetDoc.lastFrame, 'number', null);
const curFrame = NumCast(targetDoc._currentFrame);
let prevSelected = this.itemIndex;
// Functionality for group with up
@@ -290,7 +306,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Case 3: Pres loop is on so it should go to the last slide
this.gotoDocument(this.childDocs.length - 1, activeItem);
}
- }
+ };
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
@@ -303,26 +319,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
}
- if (from?.mediaStop === "auto" && this.layoutDoc.presStatus !== PresStatus.Edit) {
+ if (from?.mediaStop === 'auto' && this.layoutDoc.presStatus !== PresStatus.Edit) {
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")) {
+ if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
this.startTempMedia(targetDoc, activeItem);
}
if (targetDoc) {
- targetDoc && runInAction(() => {
- if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0;
- else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500;
- });
- setTimeout(() => targetDoc.focusSpeed = 500, this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510);
+ Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
+ targetDoc &&
+ runInAction(() => {
+ if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0;
+ else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500;
+ });
+ setTimeout(() => (targetDoc.focusSpeed = 500), this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510);
}
if (targetDoc?.lastFrame !== undefined) {
targetDoc._currentFrame = 0;
}
if (!group) this._selectedArray.clear();
this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array
- if (this.layoutDoc._viewType === "stacking" && !group) this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list
+ this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list
this.onHideDocument(); //Handles hide after/before
}
});
@@ -331,40 +349,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
navigateToView = (targetDoc: Doc, activeItem: Doc) => {
clearTimeout(this._navTimer);
const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
- bestTarget && runInAction(() => {
- if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
- bestTarget._scrollTop = activeItem.presPinViewScroll;
- } else if (bestTarget.type === DocumentType.COMPARISON) {
- bestTarget._clipWidth = activeItem.presPinClipWidth;
- } else if (bestTarget.type === DocumentType.VID) {
- bestTarget._currentTimecode = activeItem.presPinTimecode;
+ if (bestTarget) console.log(bestTarget.title, bestTarget.type);
+ else console.log('no best target');
+ if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false);
+ };
+
+ // navigates to the bestTarget document by making sure it is on screen,
+ // then it applies the view specs stored in activeItem to
+ @action
+ static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) {
+ if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) {
+ bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
+ bestTarget._scrollTop = activeItem.presPinViewScroll;
+ } else if (bestTarget.type === DocumentType.COMPARISON) {
+ bestTarget._clipWidth = activeItem.presPinClipWidth;
+ } else if ([DocumentType.AUDIO, DocumentType.VID].includes(bestTarget.type as any)) {
+ bestTarget._currentTimecode = activeItem.presStartTime;
+ } else {
+ const contentBounds = Cast(activeItem.contentBounds, listSpec('number'));
+ bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
+ if (contentBounds) {
+ bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2;
+ bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2;
+ const dv = DocumentManager.Instance.getDocumentView(bestTarget);
+ if (dv) {
+ bestTarget._viewScale = Math.min(dv.props.PanelHeight() / (contentBounds[3] - contentBounds[1]), dv.props.PanelWidth() / (contentBounds[2] - contentBounds[0]));
+ }
} else {
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
bestTarget._panX = activeItem.presPinViewX;
bestTarget._panY = activeItem.presPinViewY;
bestTarget._viewScale = activeItem.presPinViewScale;
}
- this._navTimer = setTimeout(() => bestTarget._viewTransition = undefined, activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
- });
+ }
+ return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
}
/**
* This method makes sure that cursor navigates to the element that
- * has the option open and last in the group.
- * Design choice: If the next document is not in presCollection or
+ * has the option open and last in the group.
+ * Design choice: If the next document is not in presCollection or
* presCollection itself then if there is a presCollection it will add
* a new tab. If presCollection is undefined it will open the document
- * on the right.
+ * on the right.
*/
navigateToElement = async (curDoc: Doc) => {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const srcContext = Cast(targetDoc.context, Doc, null);
+ const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null);
const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc);
- const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext);
+ const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext);
this.turnOffEdit();
// Handles the setting of presCollection
if (includesDoc) {
@@ -388,7 +423,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
});
const openInTab = (doc: Doc, finished?: () => void) => {
- collectionDocView ? collectionDocView.props.addDocTab(doc, "") : this.props.addDocTab(doc, ":left");
+ collectionDocView ? collectionDocView.props.addDocTab(doc, '') : this.props.addDocTab(doc, '');
this.layoutDoc.presCollection = targetDoc;
// this still needs some fixing
setTimeout(resetSelection, 500);
@@ -401,21 +436,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// If openDocument is selected then it should open the document for the user
if (activeItem.openDocument) {
LightboxView.SetLightboxDoc(targetDoc);
+ // openInTab(targetDoc);
} else if (curDoc.presMovement === PresMovement.Pan && targetDoc) {
LightboxView.SetLightboxDoc(undefined);
- await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right
+ await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right
} else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) {
LightboxView.SetLightboxDoc(undefined);
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right
+ //awaiting jump so that new scale can be found, since jumping is async
+ await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right
}
// 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.
if (activeItem.presPinView) {
+ console.log(targetDoc.title);
+ console.log('presPinView in PresBox.tsx:420');
// if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target
this.navigateToView(targetDoc, activeItem);
}
- }
+ };
/**
* Uses the viewfinder to progressivize through the different views of a single collection.
@@ -425,10 +463,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
const srcContext = Cast(targetDoc?.context, Doc, null);
const docView = DocumentManager.Instance.getDocumentView(targetDoc);
- const vfLeft = this.checkList(targetDoc, activeItem["viewfinder-left-indexed"]);
- const vfWidth = this.checkList(targetDoc, activeItem["viewfinder-width-indexed"]);
- const vfTop = this.checkList(targetDoc, activeItem["viewfinder-top-indexed"]);
- const vfHeight = this.checkList(targetDoc, activeItem["viewfinder-height-indexed"]);
+ const vfLeft = this.checkList(targetDoc, activeItem['viewfinder-left-indexed']);
+ const vfWidth = this.checkList(targetDoc, activeItem['viewfinder-width-indexed']);
+ const vfTop = this.checkList(targetDoc, activeItem['viewfinder-top-indexed']);
+ const vfHeight = this.checkList(targetDoc, activeItem['viewfinder-height-indexed']);
// Case 1: document that is not a Golden Layout tab
if (srcContext) {
const srcDocView = DocumentManager.Instance.getDocumentView(srcContext);
@@ -439,8 +477,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const newPanX = NumCast(targetDoc.x) + NumCast(layoutdoc._width) / 2;
const newPanY = NumCast(targetDoc.y) + NumCast(layoutdoc._height) / 2;
const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
- srcContext._panX = newPanX + (vfLeft + (vfWidth / 2));
- srcContext._panY = newPanY + (vfTop + (vfHeight / 2));
+ srcContext._panX = newPanX + (vfLeft + vfWidth / 2);
+ srcContext._panY = newPanY + (vfTop + vfHeight / 2);
srcContext._viewScale = newScale;
}
}
@@ -449,8 +487,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const panelWidth: number = docView.props.PanelWidth();
const panelHeight: number = docView.props.PanelHeight();
const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
- targetDoc._panX = vfLeft + (vfWidth / 2);
- targetDoc._panY = vfTop + (vfWidth / 2);
+ targetDoc._panX = vfLeft + vfWidth / 2;
+ targetDoc._panY = vfTop + vfWidth / 2;
targetDoc._viewScale = newScale;
}
const resize = document.getElementById('resizable');
@@ -460,7 +498,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
resize.style.top = vfTop + 'px';
resize.style.left = vfLeft + 'px';
}
- }
+ };
/**
* For 'Hide Before' and 'Hide After' buttons making sure that
@@ -474,18 +512,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (tagDoc) tagDoc.opacity = 1;
const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
const curInd: number = itemIndexes.indexOf(index);
- if (tagDoc === this.layoutDoc.presCollection) { tagDoc.opacity = 1; }
- else {
- if (itemIndexes.length > 1 && curDoc.presHideBefore && curInd !== 0) { }
- else if (curDoc.presHideBefore) {
+ if (tagDoc === this.layoutDoc.presCollection) {
+ tagDoc.opacity = 1;
+ } else {
+ if (itemIndexes.length > 1 && curDoc.presHideBefore && curInd !== 0) {
+ } else if (curDoc.presHideBefore) {
if (index > this.itemIndex) {
tagDoc.opacity = 0;
} else if (!curDoc.presHideAfter) {
tagDoc.opacity = 1;
}
}
- if (itemIndexes.length > 1 && curDoc.presHideAfter && curInd !== (itemIndexes.length - 1)) { }
- else if (curDoc.presHideAfter) {
+ if (itemIndexes.length > 1 && curDoc.presHideAfter && curInd !== itemIndexes.length - 1) {
+ } else if (curDoc.presHideAfter) {
if (index < this.itemIndex) {
tagDoc.opacity = 0;
} else if (!curDoc.presHideBefore) {
@@ -494,9 +533,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
});
- }
-
-
+ };
//The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc
@action
@@ -505,13 +542,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let activeItem: Doc = this.activeItem;
let targetDoc: Doc = this.targetDoc;
let duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition);
- const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms));
- const load = async () => { // Wrap the loop into an async function for this to work
+ const timer = (ms: number) => new Promise(res => (this._presTimer = setTimeout(res, ms)));
+ const load = async () => {
+ // Wrap the loop into an async function for this to work
for (var i = startSlide; i < this.childDocs.length; i++) {
activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
targetDoc = Cast(activeItem.presentationTargetDoc, Doc, null);
duration = NumCast(activeItem.presDuration) + NumCast(activeItem.presTransition);
- if (duration < 100) { duration = 2500; }
+ if (duration < 100) {
+ duration = 2500;
+ }
if (NumCast(targetDoc.lastFrame) > 0) {
for (var f = 0; f < NumCast(targetDoc.lastFrame); f++) {
await timer(duration / NumCast(targetDoc.lastFrame));
@@ -519,7 +559,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
- await timer(duration); this.next(); // then the created Promise can be awaited
+ await timer(duration);
+ this.next(); // then the created Promise can be awaited
if (i === this.childDocs.length - 1) {
setTimeout(() => {
clearTimeout(this._presTimer);
@@ -533,7 +574,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.startPresentation(startSlide);
this.gotoDocument(startSlide, activeItem);
load();
- }
+ };
// The function pauses the auto presentation
@action
@@ -544,20 +585,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc.presLoop = false;
this.childDocs.forEach(this.stopTempMedia);
}
- }
+ };
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
resetPresentation = () => {
this.rootDoc._itemIndex = 0;
- this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null)).filter(doc => doc instanceof Doc).forEach(doc => {
- try {
- doc.opacity = 1;
- } catch (e) {
- console.log("Reset presentation error: ", e);
- }
- });
- }
+ this.childDocs
+ .map(doc => Cast(doc.presentationTargetDoc, Doc, null))
+ .filter(doc => doc instanceof Doc)
+ .forEach(doc => {
+ try {
+ doc.opacity = 1;
+ } catch (e) {
+ console.log('Reset presentation error: ', e);
+ }
+ });
+ };
// The function allows for viewing the pres path on toggle
@action togglePath = (srcContext: Doc, off?: boolean) => {
@@ -565,19 +609,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._pathBoolean = false;
srcContext.presPathView = false;
} else {
- runInAction(() => this._pathBoolean = !this._pathBoolean);
+ runInAction(() => (this._pathBoolean = !this._pathBoolean));
srcContext.presPathView = this._pathBoolean;
}
- }
+ };
// The function allows for expanding the view of pres on toggle
@action toggleExpandMode = () => {
- runInAction(() => this._expandBoolean = !this._expandBoolean);
+ runInAction(() => (this._expandBoolean = !this._expandBoolean));
this.rootDoc.expandBoolean = this._expandBoolean;
- this.childDocs.forEach((doc) => {
+ this.childDocs.forEach(doc => {
doc.presExpandInlineButton = this._expandBoolean;
});
- }
+ };
/**
* The function that starts the presentation at the given index, also checking if actions should be applied
@@ -595,48 +639,45 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
tagDoc.opacity = 0;
}
});
- }
+ };
/**
* The method called to open the presentation as a minimized view
*/
@action
- updateMinimize = () => {
- const docView = DocumentManager.Instance.getDocumentView(this.layoutDoc);
- if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
- console.log("case 1");
+ updateMinimize = async () => {
+ if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
this.layoutDoc.presStatus = PresStatus.Edit;
- Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
- CollectionDockingView.AddSplit(this.rootDoc, "right");
- } else if ((true || this.layoutDoc.context) && docView) {
- console.log("case 2");
+ Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ CollectionDockingView.AddSplit(this.rootDoc, 'right');
+ } else {
this.layoutDoc.presStatus = PresStatus.Edit;
clearTimeout(this._presTimer);
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250);
this.rootDoc.y = pt[1] + 10;
- this.rootDoc._height = 35;
- this.rootDoc._width = 250;
- docView.props.removeDocument?.(this.layoutDoc);
- Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
- } else {
- console.log("case 3");
- // TODO glr: fix this case
+ this.rootDoc._height = 30;
+ this.rootDoc._width = 248;
+ Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ this.props.removeDocument?.(this.layoutDoc);
}
- }
+ };
/**
* Called when the user changes the view type
* Either 'List' (stacking) or 'Slides' (carousel)
*/
- // @undoBatch
+ @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' : '');
// pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined);
+ [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
this.rootDoc._viewType = viewType;
- if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 0;
+ if (this.isTreeOrStack) {
+ this.layoutDoc._gridGap = 0;
+ }
});
/**
@@ -664,20 +705,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
});
-
setMovementName = action((movement: any, activeItem: Doc): string => {
let output: string = 'none';
switch (movement) {
- case PresMovement.Zoom: output = 'Pan & Zoom'; break; //Pan and zoom
- case PresMovement.Pan: output = 'Pan'; break; //Pan
- case PresMovement.Jump: output = 'Jump cut'; break; //Jump Cut
- case PresMovement.None: output = 'None'; break; //None
- default: output = 'Zoom'; activeItem.presMovement = 'zoom'; break; //default set as zoom
+ case PresMovement.Zoom:
+ output = 'Pan & Zoom';
+ break; //Pan and zoom
+ case PresMovement.Pan:
+ output = 'Pan';
+ break; //Pan
+ case PresMovement.Jump:
+ output = 'Jump cut';
+ break; //Jump Cut
+ case PresMovement.None:
+ output = 'None';
+ break; //None
+ default:
+ output = 'Zoom';
+ activeItem.presMovement = 'zoom';
+ break; //default set as zoom
}
return output;
});
- whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isChildActive = isActive));
+ whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive)));
// For dragging documents into the presentation trail
addDocumentFilter = (docs: Doc[]) => {
docs.forEach((doc, i) => {
@@ -685,8 +736,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (doc.type === DocumentType.LABEL) {
const audio = Cast(doc.annotationOn, Doc, null);
if (audio) {
- audio.mediaStart = "manual";
- audio.mediaStop = "manual";
+ audio.mediaStart = 'manual';
+ audio.mediaStop = 'manual';
audio.presStartTime = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */));
audio.presEndTime = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */));
audio.presDuration = audio.presStartTime - audio.presEndTime;
@@ -701,7 +752,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
setTimeout(() => this.removeDocument(doc), 0);
return false;
} else {
- if (!doc.presentationTargetDoc) doc.title = doc.title + " - Slide";
+ 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;
@@ -709,13 +760,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
});
return true;
- }
- childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement;
- removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
- getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
+ };
+ childLayoutTemplate = () => (!this.isTreeOrStack ? undefined : DocCast(Doc.UserDoc().presElement));
+ 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) => ((CurrentUserUtils.SelectedTool === InkTool.None && this.props.layerProvider?.(this.layoutDoc) !== false) &&
- (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+ 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;
/**
* For sorting the array so that the order is maintained when it is dropped.
@@ -723,7 +774,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
sortArray = (): Doc[] => {
return this.childDocs.filter(doc => this._selectedArray.has(doc));
- }
+ };
/**
* Method to get the list of selected items in the order in which they have been selected
@@ -732,9 +783,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const list = Array.from(this._selectedArray.keys()).map((doc: Doc, index: any) => {
const curDoc = Cast(doc, Doc, null);
const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
- if (curDoc && curDoc === this.activeItem) return <div key={index} className="selectedList-items"><b>{index + 1}. {curDoc.title}</b></div>;
- else if (tagDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>;
- else if (curDoc) return <div key={index} className="selectedList-items">{index + 1}. {curDoc.title}</div>;
+ if (curDoc && curDoc === this.activeItem)
+ return (
+ <div key={index} className="selectedList-items">
+ <b>
+ {index + 1}. {curDoc.title}
+ </b>
+ </div>
+ );
+ else if (tagDoc)
+ return (
+ <div key={index} className="selectedList-items">
+ {index + 1}. {curDoc.title}
+ </div>
+ );
+ else if (curDoc)
+ return (
+ <div key={index} className="selectedList-items">
+ {index + 1}. {curDoc.title}
+ </div>
+ );
});
return list;
}
@@ -743,7 +811,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
selectPres = () => {
const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
presDocView && SelectionManager.SelectView(presDocView, false);
- }
+ };
//Regular click
@action
@@ -753,12 +821,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0);
else this.updateCurrentPresentation(context);
- if (this.activeItem.setPosition &&
- this.activeItem.y !== undefined &&
- this.activeItem.x !== undefined &&
- this.targetDoc.x !== undefined &&
- this.targetDoc.y !== undefined) {
- const timer = (ms: number) => new Promise(res => this._presTimer = setTimeout(res, ms));
+ if (this.activeItem.setPosition && this.activeItem.y !== undefined && this.activeItem.x !== undefined && this.targetDoc.x !== undefined && this.targetDoc.y !== undefined) {
+ const timer = (ms: number) => new Promise(res => (this._presTimer = setTimeout(res, ms)));
const time = 10;
const ydiff = NumCast(this.activeItem.y) - NumCast(this.targetDoc.y);
const xdiff = NumCast(this.activeItem.x) - NumCast(this.targetDoc.x);
@@ -769,7 +833,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
await timer(0.1);
}
}
- }
+ };
//Command click
@action
@@ -784,13 +848,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.removeFromArray(this._dragArray, doc);
}
this.selectPres();
- }
+ };
removeFromArray = (arr: any[], val: any) => {
const index: number = arr.indexOf(val);
const ret: any[] = arr.splice(index, 1);
arr = ret;
- }
+ };
//Shift click
@action
@@ -805,28 +869,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
this.selectPres();
- }
+ };
//regular click
@action
- regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean) => {
+ regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true) => {
this._selectedArray.clear();
this._selectedArray.set(doc, undefined);
this._eleArray.splice(0, this._eleArray.length, ref);
this._dragArray.splice(0, this._dragArray.length, drag);
focus && this.selectElement(doc);
- this.selectPres();
- }
+ selectPres && this.selectPres();
+ };
modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: 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);
- }
+ };
static keyEventsWrapper = (e: KeyboardEvent) => {
PresBox.Instance.keyEvents(e);
- }
+ };
// Key for when the presentaiton is active
@action
@@ -834,57 +898,75 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (e.target instanceof HTMLInputElement) return;
let handled = false;
const anchorNode = document.activeElement as HTMLDivElement;
- if (anchorNode && anchorNode.className?.includes("lm_title")) return;
+ if (anchorNode && anchorNode.className?.includes('lm_title')) return;
switch (e.key) {
- case "Backspace":
- if (this.layoutDoc.presStatus === "edit") {
- undoBatch(action(() => {
- for (const doc of Array.from(this._selectedArray.keys())) {
- this.removeDocument(doc);
- }
- this._selectedArray.clear();
- this._eleArray.length = 0;
- this._dragArray.length = 0;
- }))();
+ case 'Backspace':
+ if (this.layoutDoc.presStatus === 'edit') {
+ undoBatch(
+ action(() => {
+ for (const doc of Array.from(this._selectedArray.keys())) {
+ this.removeDocument(doc);
+ }
+ this._selectedArray.clear();
+ this._eleArray.length = 0;
+ this._dragArray.length = 0;
+ })
+ )();
handled = true;
}
break;
- case "Escape":
- if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { this.updateMinimize(); }
- else if (this.layoutDoc.presStatus === "edit") { this._selectedArray.clear(); this._eleArray.length = this._dragArray.length = 0; }
- else this.layoutDoc.presStatus = "edit";
+ case 'Escape':
+ if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
+ this.updateMinimize();
+ } else if (this.layoutDoc.presStatus === 'edit') {
+ this._selectedArray.clear();
+ this._eleArray.length = this._dragArray.length = 0;
+ } else this.layoutDoc.presStatus = 'edit';
if (this._presTimer) clearTimeout(this._presTimer);
handled = true;
break;
- case "Down": case "ArrowDown":
- case "Right": case "ArrowRight":
- if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) { // TODO: update to work properly
+ case 'Down':
+ case 'ArrowDown':
+ case 'Right':
+ case 'ArrowRight':
+ if (e.shiftKey && this.itemIndex < this.childDocs.length - 1) {
+ // TODO: update to work properly
this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) + 1;
this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined);
} else {
this.next();
- if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; }
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
}
handled = true;
break;
- case "Up": case "ArrowUp":
- case "Left": case "ArrowLeft":
- if (e.shiftKey && this.itemIndex !== 0) { // TODO: update to work properly
+ case 'Up':
+ case 'ArrowUp':
+ case 'Left':
+ case 'ArrowLeft':
+ if (e.shiftKey && this.itemIndex !== 0) {
+ // TODO: update to work properly
this.rootDoc._itemIndex = NumCast(this.rootDoc._itemIndex) - 1;
this._selectedArray.set(this.childDocs[this.rootDoc._itemIndex], undefined);
} else {
this.back();
- if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; }
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
}
handled = true;
break;
- case "Spacebar": case " ":
+ case 'Spacebar':
+ case ' ':
if (this.layoutDoc.presStatus === PresStatus.Manual) this.startAutoPres(this.itemIndex);
else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer);
handled = true;
break;
- case "a":
- if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === "edit") {
+ case 'a':
+ if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === 'edit') {
this._selectedArray.clear();
this.childDocs.forEach(doc => this._selectedArray.set(doc, undefined));
handled = true;
@@ -896,10 +978,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
e.preventDefault();
}
- }
+ };
/**
- *
+ *
*/
@action
viewPaths = () => {
@@ -907,7 +989,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (srcContext) {
this.togglePath(srcContext);
}
- }
+ };
getAllIndexes = (arr: Doc[], val: Doc): number[] => {
const indexes = [];
@@ -915,7 +997,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
arr[i] === val && indexes.push(i);
}
return indexes;
- }
+ };
// Adds the index in the pres path graphically
@computed get order() {
@@ -923,104 +1005,109 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const docs: Doc[] = [];
const presCollection = Cast(this.rootDoc.presCollection, Doc, null);
const dv = DocumentManager.Instance.getDocumentView(presCollection);
- this.childDocs.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 width = NumCast(tagDoc._width) / 10;
- const height = Math.max(NumCast(tagDoc._height) / 10, 15);
- const edge = Math.max(width, height);
- const fontSize = edge * 0.8;
- const gap = 2;
- if (presCollection === srcContext) {
- // Case A: Document is contained within the collection
- if (docs.includes(tagDoc)) {
- const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length;
- docs.push(tagDoc);
- order.push(
- <div className="pathOrder"
- key={tagDoc.id + 'pres' + index}
- style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - (edge / 2)), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }}
- onClick={() => this.selectElement(doc)}>
- <div className="pathOrder-frame">{index + 1}</div>
- </div>);
- } else {
+ this.childDocs
+ .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 width = NumCast(tagDoc._width) / 10;
+ const height = Math.max(NumCast(tagDoc._height) / 10, 15);
+ const edge = Math.max(width, height);
+ const fontSize = edge * 0.8;
+ const gap = 2;
+ if (presCollection === srcContext) {
+ // Case A: Document is contained within the collection
+ if (docs.includes(tagDoc)) {
+ const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length;
+ docs.push(tagDoc);
+ order.push(
+ <div
+ className="pathOrder"
+ key={tagDoc.id + 'pres' + index}
+ style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - edge / 2), left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }}
+ onClick={() => this.selectElement(doc)}>
+ <div className="pathOrder-frame">{index + 1}</div>
+ </div>
+ );
+ } else {
+ docs.push(tagDoc);
+ order.push(
+ <div
+ className="pathOrder"
+ key={tagDoc.id + 'pres' + index}
+ style={{ top: NumCast(tagDoc.y) - edge / 2, left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }}
+ onClick={() => this.selectElement(doc)}>
+ <div className="pathOrder-frame">{index + 1}</div>
+ </div>
+ );
+ }
+ } else if (doc.presPinView && presCollection === tagDoc && dv) {
+ // Case B: Document is presPinView and is presCollection
+ const scale: number = 1 / NumCast(doc.presPinViewScale);
+ 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;
docs.push(tagDoc);
order.push(
- <div className="pathOrder"
- key={tagDoc.id + 'pres' + index}
- style={{ top: NumCast(tagDoc.y) - (edge / 2), left: NumCast(tagDoc.x) - (edge / 2), width: edge, height: edge, fontSize: fontSize }}
- onClick={() => this.selectElement(doc)}>
- <div className="pathOrder-frame">{index + 1}</div>
- </div>);
+ <>
+ <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top: yLoc - indEdge / 2, left: xLoc - indEdge / 2, width: indEdge, height: indEdge, fontSize: indFontSize }} onClick={() => this.selectElement(doc)}>
+ <div className="pathOrder-frame">{index + 1}</div>
+ </div>
+ <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div>
+ </>
+ );
}
- } else if (doc.presPinView && presCollection === tagDoc && dv) {
- // Case B: Document is presPinView and is presCollection
- const scale: number = 1 / NumCast(doc.presPinViewScale);
- 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);
- docs.push(tagDoc);
- order.push(
- <>
- <div className="pathOrder"
- key={tagDoc.id + 'pres' + index}
- style={{ top: yLoc - (indEdge / 2), left: xLoc - (indEdge / 2), width: indEdge, height: indEdge, fontSize: indFontSize }}
- onClick={() => this.selectElement(doc)}
- >
- <div className="pathOrder-frame">{index + 1}</div>
- </div>
- <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div>
- </>);
- }
- });
+ });
return order;
}
/**
* 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 _fitToBox as true so the
+ * Design choice: When this is called it sets _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)
*/
@computed get paths() {
- let pathPoints = "";
+ let pathPoints = '';
const presCollection = Cast(this.rootDoc.presCollection, Doc, null);
this.childDocs.forEach((doc, index) => {
const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
const srcContext = Cast(tagDoc?.context, Doc, null);
if (tagDoc && presCollection === srcContext) {
- const n1x = NumCast(tagDoc.x) + (NumCast(tagDoc._width) / 2);
- const n1y = NumCast(tagDoc.y) + (NumCast(tagDoc._height) / 2);
- if (index = 0) pathPoints = n1x + "," + n1y;
- else pathPoints = pathPoints + " " + n1x + "," + n1y;
+ const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2;
+ const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2;
+ if ((index = 0)) pathPoints = n1x + ',' + n1y;
+ else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
} else if (doc.presPinView && presCollection === tagDoc) {
const n1x = NumCast(doc.presPinViewX);
const n1y = NumCast(doc.presPinViewY);
- if (index = 0) pathPoints = n1x + "," + n1y;
- else pathPoints = pathPoints + " " + n1x + "," + n1y;
+ if ((index = 0)) pathPoints = n1x + ',' + n1y;
+ else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
}
});
- return (<polyline
- points={pathPoints}
- style={{
- opacity: 1,
- stroke: "#69a6db",
- strokeWidth: 5,
- strokeDasharray: '10 5',
- boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)',
- }}
- fill="none"
- markerStart="url(#markerArrow)"
- markerMid="url(#markerSquare)"
- markerEnd="url(#markerSquareFilled)"
- />);
+ return (
+ <polyline
+ points={pathPoints}
+ style={{
+ opacity: 1,
+ stroke: '#69a6db',
+ strokeWidth: 5,
+ strokeDasharray: '10 5',
+ boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)',
+ }}
+ fill="none"
+ markerStart="url(#markerArrow)"
+ markerMid="url(#markerSquare)"
+ markerEnd="url(#markerSquareFilled)"
+ />
+ );
}
// Converts seconds to ms and updates presTransition
@@ -1029,8 +1116,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
if (timeInMS > 10000) timeInMS = 10000;
- Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS);
- }
+ Array.from(this._selectedArray.keys()).forEach(doc => (doc.presTransition = timeInMS));
+ };
// Converts seconds to ms and updates presTransition
setZoom = (number: String, change?: number) => {
@@ -1038,8 +1125,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) scale += change;
if (scale < 0.01) scale = 0.01;
if (scale > 1.5) scale = 1.5;
- Array.from(this._selectedArray.keys()).forEach((doc) => doc.presZoom = scale);
- }
+ Array.from(this._selectedArray.keys()).forEach(doc => (doc.presZoom = scale));
+ };
// Converts seconds to ms and updates presDuration
setDurationTime = (number: String, change?: number) => {
@@ -1047,8 +1134,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
if (timeInMS > 20000) timeInMS = 20000;
- Array.from(this._selectedArray.keys()).forEach((doc) => doc.presDuration = timeInMS);
- }
+ Array.from(this._selectedArray.keys()).forEach(doc => (doc.presDuration = timeInMS));
+ };
/**
* When the movement dropdown is changes
@@ -1056,7 +1143,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
updateMovement = action((movement: any, all?: boolean) => {
const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys());
- array.forEach((doc) => {
+ array.forEach(doc => {
switch (movement) {
case PresMovement.Zoom: //Pan and zoom
doc.presMovement = PresMovement.Zoom;
@@ -1068,7 +1155,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc.presJump = true;
doc.presMovement = PresMovement.Jump;
break;
- case PresMovement.None: default:
+ case PresMovement.None:
+ default:
doc.presMovement = PresMovement.None;
break;
}
@@ -1079,31 +1167,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
updateHideBefore = (activeItem: Doc) => {
activeItem.presHideBefore = !activeItem.presHideBefore;
- Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideBefore = activeItem.presHideBefore);
- }
+ Array.from(this._selectedArray.keys()).forEach(doc => (doc.presHideBefore = activeItem.presHideBefore));
+ };
@undoBatch
@action
updateHideAfter = (activeItem: Doc) => {
activeItem.presHideAfter = !activeItem.presHideAfter;
- Array.from(this._selectedArray.keys()).forEach((doc) => doc.presHideAfter = activeItem.presHideAfter);
- }
+ Array.from(this._selectedArray.keys()).forEach(doc => (doc.presHideAfter = activeItem.presHideAfter));
+ };
@undoBatch
@action
updateOpenDoc = (activeItem: Doc) => {
activeItem.openDocument = !activeItem.openDocument;
- Array.from(this._selectedArray.keys()).forEach((doc) => {
+ Array.from(this._selectedArray.keys()).forEach(doc => {
doc.openDocument = activeItem.openDocument;
});
- }
+ };
@undoBatch
@action
updateEffectDirection = (effect: any, all?: boolean) => {
const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys());
- array.forEach((doc) => {
- const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
+ array.forEach(doc => {
+ const tagDoc = doc; // Cast(doc.presentationTargetDoc, Doc, null);
switch (effect) {
case PresEffect.Left:
tagDoc.presEffectDirection = PresEffect.Left;
@@ -1117,19 +1205,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case PresEffect.Bottom:
tagDoc.presEffectDirection = PresEffect.Bottom;
break;
- case PresEffect.Center: default:
+ case PresEffect.Center:
+ default:
tagDoc.presEffectDirection = PresEffect.Center;
break;
}
});
- }
+ };
@undoBatch
@action
updateEffect = (effect: any, all?: boolean) => {
const array: any[] = all ? this.childDocs : Array.from(this._selectedArray.keys());
- array.forEach((doc) => {
- const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
+ array.forEach(doc => {
+ const tagDoc = doc; //Cast(doc.presentationTargetDoc, Doc, null);
switch (effect) {
case PresEffect.Bounce:
tagDoc.presEffect = PresEffect.Bounce;
@@ -1146,19 +1235,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case PresEffect.Rotate:
tagDoc.presEffect = PresEffect.Rotate;
break;
- case PresEffect.None: default:
+ case PresEffect.None:
+ default:
tagDoc.presEffect = PresEffect.None;
break;
}
});
- }
+ };
_batch: UndoManager.Batch | undefined = undefined;
@computed get transitionDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection);
+ const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection;
const isPinWithView: boolean = BoolCast(activeItem.presPinView);
if (activeItem && targetDoc) {
const type = targetDoc.type;
@@ -1166,168 +1256,326 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75;
let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
- const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None';
+ const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None';
activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom';
return (
- <div className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = false; this.openEffectDropdown = false; })}>
+ <div
+ className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
+ onPointerDown={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
+ onClick={action(e => {
+ e.stopPropagation();
+ this.openMovementDropdown = false;
+ this.openEffectDropdown = false;
+ })}>
<div className="ribbon-box">
Movement
- {isPresCollection || (isPresCollection && isPinWithView) ?
+ {isPresCollection || (isPresCollection && isPinWithView) ? (
<div className="ribbon-property" style={{ marginLeft: 0, height: 25, textAlign: 'left', paddingLeft: 5, paddingRight: 5, fontSize: 10 }}>
- {this.scrollable ? "Scroll to pinned view" : !isPinWithView ? "No movement" : "Pan & Zoom to pinned view"}
+ {this.scrollable ? 'Scroll to pinned view' : !isPinWithView ? 'No movement' : 'Pan & Zoom to pinned view'}
</div>
- :
- <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openMovementDropdown = !this.openMovementDropdown; })} style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ ) : (
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this.openMovementDropdown = !this.openMovementDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
{this.setMovementName(activeItem.presMovement, activeItem)}
- <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} />
- <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? "grid" : "none" }}>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>None</div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>Pan {"&"} Zoom</div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}>Pan</div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}>Jump cut</div>
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? 'grid' : 'none' }}>
+ <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>
+ None
+ </div>
+ <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>
+ Pan {'&'} Zoom
+ </div>
+ <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}>
+ Pan
+ </div>
+ <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}>
+ Jump cut
+ </div>
</div>
</div>
- }
- <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
+ )}
+ <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" value={zoom} onChange={action(e => this.setZoom(e.target.value))} />%
</div>
<div className="ribbon-propertyUpDown">
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
- <FontAwesomeIcon icon={"caret-up"} />
+ <FontAwesomeIcon icon={'caret-up'} />
</div>
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
- <FontAwesomeIcon icon={"caret-down"} />
+ <FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- <input type="range" step="1" min="0" max="150" value={zoom}
- className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}
+ <input
+ type="range"
+ step="1"
+ min="0"
+ max="150"
+ value={zoom}
+ className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}
id="toolbar-slider"
- onPointerDown={() => this._batch = UndoManager.StartBatch("presZoom")}
+ onPointerDown={() => (this._batch = UndoManager.StartBatch('presZoom'))}
onPointerUp={() => this._batch?.end()}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
this.setZoom(e.target.value);
- }} />
- <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
+ }}
+ />
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Movement Speed</div>
<div className="ribbon-property">
- <input className="presBox-input"
- type="number" value={transitionSpeed}
- onChange={action((e) => this.setTransitionTime(e.target.value))} /> s
+ <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setTransitionTime(e.target.value))} /> s
</div>
<div className="ribbon-propertyUpDown">
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}>
- <FontAwesomeIcon icon={"caret-up"} />
+ <FontAwesomeIcon icon={'caret-up'} />
</div>
<div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}>
- <FontAwesomeIcon icon={"caret-down"} />
+ <FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- <input type="range" step="0.1" min="0.1" max="10" value={transitionSpeed}
- className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}
+ <input
+ type="range"
+ step="0.1"
+ min="0.1"
+ max="10"
+ value={transitionSpeed}
+ className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}
id="toolbar-slider"
- onPointerDown={() => this._batch = UndoManager.StartBatch("presTransition")}
+ onPointerDown={() => (this._batch = UndoManager.StartBatch('presTransition'))}
onPointerUp={() => this._batch?.end()}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
this.setTransitionTime(e.target.value);
- }} />
- <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}>
+ }}
+ />
+ <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
<div className="slider-text">Fast</div>
<div className="slider-text">Medium</div>
<div className="slider-text">Slow</div>
</div>
</div>
<div className="ribbon-box">
- Visibility {"&"} Duration
+ Visibility {'&'} Duration
<div className="ribbon-doubleButton">
- {isPresCollection ? (null) : <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>}
- {isPresCollection ? (null) : <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.openDocument ? Colors.LIGHT_BLUE : "" }} onClick={() => this.updateOpenDoc(activeItem)}>Lightbox</div></Tooltip>
+ {isPresCollection ? null : (
+ <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>
+ )}
+ {isPresCollection ? null : (
+ <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.openDocument ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
+ Lightbox
+ </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}
- onChange={action((e) => this.setDurationTime(e.target.value))} /> s
+ {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>
- <div className="ribbon-propertyUpDown">
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}>
- <FontAwesomeIcon icon={"caret-up"} />
+ <input
+ type="range"
+ step="0.1"
+ min="0.1"
+ max="20"
+ value={duration}
+ style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'block' }}
+ className={'toolbar-slider'}
+ id="duration-slider"
+ onPointerDown={() => {
+ this._batch = UndoManager.StartBatch('presDuration');
+ }}
+ onPointerUp={() => {
+ if (this._batch) this._batch.end();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ this.setDurationTime(e.target.value);
+ }}
+ />
+ <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>
+ {isPresCollection ? null : (
+ <div className="ribbon-box">
+ Effects
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this.openEffectDropdown = !this.openEffectDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ {effect.toString()}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+ <div
+ className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.None || !this.activeItem.presEffect ? 'active' : ''}`}
+ onPointerDown={e => e.stopPropagation()}
+ onClick={() => this.updateEffect(PresEffect.None)}>
+ None
+ </div>
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Fade ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>
+ Fade In
+ </div>
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Flip ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>
+ Flip
+ </div>
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Rotate ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>
+ Rotate
+ </div>
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Bounce ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>
+ Bounce
</div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}>
- <FontAwesomeIcon icon={"caret-down"} />
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Roll ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>
+ Roll
</div>
</div>
</div>
- <input type="range" step="0.1" min="0.1" max="20" value={duration}
- style={{ display: targetDoc.type === DocumentType.AUDIO ? "none" : "block" }}
- className={"toolbar-slider"} id="duration-slider"
- onPointerDown={() => { this._batch = UndoManager.StartBatch("presDuration"); }}
- onPointerUp={() => { if (this._batch) this._batch.end(); }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => { e.stopPropagation(); this.setDurationTime(e.target.value); }}
- />
- <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>
- {isPresCollection ? (null) : <div className="ribbon-box">
- Effects
- <div className="presBox-dropdown" onClick={action(e => { e.stopPropagation(); this.openEffectDropdown = !this.openEffectDropdown; })} style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
- {effect}
- <FontAwesomeIcon className='presBox-dropdownIcon' style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={"angle-down"} />
- <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? "grid" : "none" }} onPointerDown={e => e.stopPropagation()}>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.None || !targetDoc.presEffect ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.None)}>None</div>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Fade ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>Fade In</div>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Flip ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>Flip</div>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Rotate ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>Rotate</div>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Bounce ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>Bounce</div>
- <div className={`presBox-dropdownOption ${targetDoc.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div>
+ <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}>
+ <div className="presBox-subheading">Effect direction</div>
+ <div className="ribbon-property">{this.effectDirection}</div>
</div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}>
- <div className="presBox-subheading" >Effect direction</div>
- <div className="ribbon-property">
- {this.effectDirection}
+ <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}>
+ <Tooltip title={<div className="dash-tooltip">{'Enter from left'}</div>}>
+ <div
+ style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
+ onClick={() => this.updateEffectDirection(PresEffect.Left)}>
+ <FontAwesomeIcon icon={'angle-right'} />
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Enter from right'}</div>}>
+ <div
+ style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
+ onClick={() => this.updateEffectDirection(PresEffect.Right)}>
+ <FontAwesomeIcon icon={'angle-left'} />
+ </div>
+ </Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Enter from top'}</div>
+ </>
+ }>
+ <div
+ style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
+ onClick={() => this.updateEffectDirection(PresEffect.Top)}>
+ <FontAwesomeIcon icon={'angle-down'} />
+ </div>
+ </Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Enter from bottom'}</div>
+ </>
+ }>
+ <div
+ style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
+ onClick={() => this.updateEffectDirection(PresEffect.Bottom)}>
+ <FontAwesomeIcon icon={'angle-up'} />
+ </div>
+ </Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Enter from center'}</div>
+ </>
+ }>
+ <div
+ style={{
+ gridColumn: 2,
+ gridRow: 2,
+ width: 10,
+ height: 10,
+ alignSelf: 'center',
+ justifySelf: 'center',
+ border: this.activeItem.presEffectDirection === PresEffect.Center || !this.activeItem.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : 'solid 2px black',
+ borderRadius: '100%',
+ cursor: 'pointer',
+ }}
+ onClick={() => this.updateEffectDirection(PresEffect.Center)}></div>
+ </Tooltip>
</div>
</div>
- <div className="effectDirection" style={{ display: effect === 'None' ? "none" : "grid", width: 40 }}>
- <Tooltip title={<><div className="dash-tooltip">{"Enter from left"}</div></>}><div style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Left)}><FontAwesomeIcon icon={"angle-right"} /></div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Enter from right"}</div></>}><div style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Right)}><FontAwesomeIcon icon={"angle-left"} /></div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Enter from top"}</div></>}><div style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Top)}><FontAwesomeIcon icon={"angle-down"} /></div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Enter from bottom"}</div></>}><div style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: targetDoc.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : "black", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Bottom)}><FontAwesomeIcon icon={"angle-up"} /></div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Enter from center"}</div></>}><div style={{ gridColumn: 2, gridRow: 2, width: 10, height: 10, alignSelf: 'center', justifySelf: 'center', border: targetDoc.presEffectDirection === PresEffect.Center || !targetDoc.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : "solid 2px black", borderRadius: "100%", cursor: "pointer" }} onClick={() => this.updateEffectDirection(PresEffect.Center)}></div></Tooltip>
- </div>
- </div>}
+ )}
<div className="ribbon-final-box">
<div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}>
Apply to all
</div>
</div>
- </div >
+ </div>
);
}
}
@computed get effectDirection(): string {
let effect = '';
- switch (this.targetDoc.presEffectDirection) {
- case 'left': effect = "Enter from left"; break;
- case 'right': effect = "Enter from right"; break;
- case 'top': effect = "Enter from top"; break;
- case 'bottom': effect = "Enter from bottom"; break;
- default: effect = "Enter from center"; break;
+ switch (this.activeItem.presEffectDirection) {
+ case 'left':
+ effect = 'Enter from left';
+ break;
+ case 'right':
+ effect = 'Enter from right';
+ break;
+ case 'top':
+ effect = 'Enter from top';
+ break;
+ case 'bottom':
+ effect = 'Enter from bottom';
+ break;
+ default:
+ effect = 'Enter from center';
+ break;
}
return effect;
}
@@ -1338,9 +1586,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
this.updateMovement(activeItem.presMovement, true);
- this.updateEffect(targetDoc.presEffect, true);
- this.updateEffectDirection(targetDoc.presEffectDirection, true);
- array.forEach((doc) => {
+ this.updateEffect(activeItem.presEffect, true);
+ this.updateEffectDirection(activeItem.presEffectDirection, true);
+ array.forEach(doc => {
const curDoc = Cast(doc, Doc, null);
const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
if (tagDoc && targetDoc) {
@@ -1350,61 +1598,86 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
curDoc.presHideAfter = activeItem.presHideAfter;
}
});
- }
+ };
@computed get presPinViewOptionsDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 16, filter: 'invert(1)' }} />;
+ const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 16, filter: 'invert(1)' }} />;
return (
<>
- {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : (null)}
+ {this.panable || this.scrollable || this.targetDoc.type === DocumentType.COMPARISON ? 'Pinned view' : null}
<div className="ribbon-doubleButton">
- <Tooltip title={<><div className="dash-tooltip">{activeItem.presPinView ? "Turn off pin with view" : "Turn on pin with view"}</div></>}><div className="ribbon-toggle" style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : "" }}
- onClick={() => {
- activeItem.presPinView = !activeItem.presPinView;
- targetDoc.presPinView = activeItem.presPinView;
- if (activeItem.presPinView) {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinView = true;
- activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeItem.presPinTimecode = targetDoc._currentTimecode;
- } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinView = true;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const width = targetDoc._clipWidth;
- activeItem.presPinClipWidth = width;
- activeItem.presPinView = true;
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{activeItem.presPinView ? 'Turn off pin with view' : 'Turn on pin with view'}</div>
+ </>
+ }>
+ <div
+ className="ribbon-toggle"
+ style={{ width: 20, padding: 0, backgroundColor: activeItem.presPinView ? Colors.LIGHT_BLUE : '' }}
+ onClick={() => {
+ activeItem.presPinView = !activeItem.presPinView;
+ targetDoc.presPinView = activeItem.presPinView;
+ if (activeItem.presPinView) {
+ if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.RTF || targetDoc.type === DocumentType.WEB || targetDoc._viewType === CollectionViewType.Stacking) {
+ const scroll = targetDoc._scrollTop;
+ activeItem.presPinView = true;
+ activeItem.presPinViewScroll = scroll;
+ } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
+ activeItem.presStartTime = targetDoc._currentTimecode;
+ activeItem.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1;
+ } else if ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG) {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeItem.presPinView = true;
+ activeItem.presPinViewX = x;
+ activeItem.presPinViewY = y;
+ activeItem.presPinViewScale = scale;
+ } else if (targetDoc.type === DocumentType.COMPARISON) {
+ const width = targetDoc._clipWidth;
+ activeItem.presPinClipWidth = width;
+ activeItem.presPinView = true;
+ }
}
- }
- }}>{presPinWithViewIcon}</div></Tooltip>
- {activeItem.presPinView ? <Tooltip title={<><div className="dash-tooltip">{"Update the pinned view with the view of the selected document"}</div></>}><div className="ribbon-button"
- onClick={() => {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeItem.presPinTimecode = targetDoc._currentTimecode;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- } else {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
- }
- }}>Update</div></Tooltip> : (null)}
+ }}>
+ {presPinWithViewIcon}
+ </div>
+ </Tooltip>
+ {activeItem.presPinView ? (
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Update the pinned view with the view of the selected document'}</div>
+ </>
+ }>
+ <div
+ className="ribbon-button"
+ onClick={() => {
+ if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
+ const scroll = targetDoc._scrollTop;
+ activeItem.presPinViewScroll = scroll;
+ } else if ([DocumentType.AUDIO, DocumentType.VID].includes(targetDoc.type as any)) {
+ activeItem.presStartTime = targetDoc._currentTimecode;
+ activeItem.presStartTime = NumCast(targetDoc._currentTimecode) + 0.1;
+ } else if (targetDoc.type === DocumentType.COMPARISON) {
+ const clipWidth = targetDoc._clipWidth;
+ activeItem.presPinClipWidth = clipWidth;
+ } else {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeItem.presPinViewX = x;
+ activeItem.presPinViewY = y;
+ activeItem.presPinViewScale = scale;
+ }
+ }}>
+ Update
+ </div>
+ </Tooltip>
+ ) : null}
</div>
</>
);
@@ -1415,35 +1688,58 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
return (
<>
- {this.panable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Pan X</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number" value={NumCast(activeItem.presPinViewX)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewX = Number(val); })} />
+ {this.panable ? (
+ <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
+ <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
+ <div className="presBox-subheading">Pan X</div>
+ <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
+ <input
+ className="presBox-input"
+ style={{ textAlign: 'left', width: 50 }}
+ type="number"
+ value={NumCast(activeItem.presPinViewX)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ const val = e.target.value;
+ activeItem.presPinViewX = Number(val);
+ })}
+ />
+ </div>
</div>
- </div>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Pan Y</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number" value={NumCast(activeItem.presPinViewY)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewY = Number(val); })} />
+ <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
+ <div className="presBox-subheading">Pan Y</div>
+ <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
+ <input
+ className="presBox-input"
+ style={{ textAlign: 'left', width: 50 }}
+ type="number"
+ value={NumCast(activeItem.presPinViewY)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ const val = e.target.value;
+ activeItem.presPinViewY = Number(val);
+ })}
+ />
+ </div>
</div>
- </div>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Scale</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number" value={NumCast(activeItem.presPinViewScale)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScale = Number(val); })} />
+ <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
+ <div className="presBox-subheading">Scale</div>
+ <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
+ <input
+ className="presBox-input"
+ style={{ textAlign: 'left', width: 50 }}
+ type="number"
+ value={NumCast(activeItem.presPinViewScale)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ const val = e.target.value;
+ activeItem.presPinViewScale = Number(val);
+ })}
+ />
+ </div>
</div>
</div>
- </div> : (null)}
+ ) : null}
</>
);
}
@@ -1453,17 +1749,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
return (
<>
- {this.scrollable ? <div style={{ display: activeItem.presPinView ? "block" : "none" }}>
- <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
- <div className="presBox-subheading">Scroll</div>
- <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
- <input className="presBox-input"
- style={{ textAlign: 'left', width: 50 }}
- type="number" value={NumCast(activeItem.presPinViewScroll)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { const val = e.target.value; activeItem.presPinViewScroll = Number(val); })} />
+ {this.scrollable ? (
+ <div style={{ display: activeItem.presPinView ? 'block' : 'none' }}>
+ <div className="ribbon-doubleButton" style={{ marginRight: 10 }}>
+ <div className="presBox-subheading">Scroll</div>
+ <div className="ribbon-property" style={{ paddingRight: 0, paddingLeft: 0 }}>
+ <input
+ className="presBox-input"
+ style={{ textAlign: 'left', width: 50 }}
+ type="number"
+ value={NumCast(activeItem.presPinViewScroll)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ const val = e.target.value;
+ activeItem.presPinViewScroll = Number(val);
+ })}
+ />
+ </div>
</div>
</div>
- </div> : (null)}
+ ) : null}
</>
);
}
@@ -1473,38 +1778,45 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const list = this.childDocs.map((doc, i) => {
if (i > this.itemIndex) {
return (
- <option>{i + 1}. {doc.title}</option>
+ <option>
+ {i + 1}. {StrCast(doc.title)}
+ </option>
);
}
});
- return (
- list
- );
+ return list;
}
@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);
const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10);
const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
- const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + ". " + this.childDocs[mediaStopDocInd - 1].title : "";
+ const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + '. ' + this.childDocs[mediaStopDocInd - 1].title : '';
if (activeItem && targetDoc) {
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" >
+ 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"
+ <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ className="presBox-input"
style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }}
- type="number" value={NumCast(activeItem.presStartTime)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })}
+ type="number"
+ value={NumCast(activeItem.presStartTime)}
+ onKeyDown={e => e.stopPropagation()}
+ onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+ activeItem.presStartTime = Number(e.target.value);
+ })}
/>
</div>
</div>
@@ -1520,23 +1832,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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"
+ <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ className="presBox-input"
+ onKeyDown={e => e.stopPropagation()}
style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }}
- type="number" value={NumCast(activeItem.presEndTime)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presEndTime = Number(e.target.value); })}
+ type="number"
+ value={NumCast(activeItem.presEndTime)}
+ 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="0" max={duration / 10} value={NumCast(activeItem.presEndTime)}
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presEndTime)}
style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${"end"}`}
+ className={`toolbar-slider ${'end'}`}
id="toolbar-slider"
onPointerDown={() => {
- this._batch = UndoManager.StartBatch("presEndTime");
- const endBlock = document.getElementById("endTime");
+ this._batch = UndoManager.StartBatch('presEndTime');
+ const endBlock = document.getElementById('endTime');
if (endBlock) {
endBlock.style.color = Colors.LIGHT_GRAY;
endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
@@ -1544,7 +1866,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}}
onPointerUp={() => {
this._batch?.end();
- const endBlock = document.getElementById("endTime");
+ const endBlock = document.getElementById('endTime');
if (endBlock) {
endBlock.style.color = Colors.BLACK;
endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
@@ -1553,14 +1875,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
activeItem.presEndTime = Number(e.target.value);
- }} />
- <input type="range" step="0.1" min="0" max={duration / 10} value={NumCast(activeItem.presStartTime)}
+ }}
+ />
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presStartTime)}
style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${"start"}`}
+ className={`toolbar-slider ${'start'}`}
id="toolbar-slider"
onPointerDown={() => {
- this._batch = UndoManager.StartBatch("presStartTime");
- const startBlock = document.getElementById("startTime");
+ this._batch = UndoManager.StartBatch('presStartTime');
+ const startBlock = document.getElementById('startTime');
if (startBlock) {
startBlock.style.color = Colors.LIGHT_GRAY;
startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
@@ -1568,7 +1896,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}}
onPointerUp={() => {
this._batch?.end();
- const startBlock = document.getElementById("startTime");
+ const startBlock = document.getElementById('startTime');
if (startBlock) {
startBlock.style.color = Colors.BLACK;
startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
@@ -1577,12 +1905,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
activeItem.presStartTime = Number(e.target.value);
- }} />
+ }}
+ />
</div>
- <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}>
- <div className="slider-text">0 s</div>
+ <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
+ <div className="slider-text">{clipStart} s</div>
<div className="slider-text"></div>
- <div className="slider-text">{duration / 10} s</div>
+ <div className="slider-text">{clipEnd} s</div>
</div>
</div>
<div className="ribbon-final-box">
@@ -1590,38 +1919,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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"}
- />
+ <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"}
- />
+ <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"}
- />
+ <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"}
- />
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
<div>On slide change</div>
</div>
{/* <div className="checkbox-container">
@@ -1645,7 +1958,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
</div>
- </div >
+ </div>
);
}
}
@@ -1653,84 +1966,137 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get newDocumentToolbarDropdown() {
return (
<div>
- <div className={'presBox-toolbar-dropdown'} style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === "edit" ? "inline-flex" : "none" }} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div
+ className={'presBox-toolbar-dropdown'}
+ style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }}
+ onClick={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
+ onPointerDown={e => e.stopPropagation()}>
<div className="layout-container" style={{ height: 'max-content' }}>
- <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'blank'; this.createNewSlide(this.layout); })} />
- <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'title'; this.createNewSlide(this.layout); })}>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'blank';
+ this.createNewSlide(this.layout);
+ })}
+ />
+ <div
+ className="layout"
+ style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'title';
+ this.createNewSlide(this.layout);
+ })}>
<div className="title">Title</div>
<div className="subtitle">Subtitle</div>
</div>
- <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'header'; this.createNewSlide(this.layout); })}>
- <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'header';
+ this.createNewSlide(this.layout);
+ })}>
+ <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
+ Section header
+ </div>
</div>
- <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => { this.layout = 'content'; this.createNewSlide(this.layout); })}>
- <div className="title" style={{ alignSelf: 'center' }}>Title</div>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'content';
+ this.createNewSlide(this.layout);
+ })}>
+ <div className="title" style={{ alignSelf: 'center' }}>
+ Title
+ </div>
<div className="content">Text goes here</div>
</div>
</div>
</div>
- </div >
+ </div>
);
}
@observable openLayouts: boolean = false;
@observable addFreeform: boolean = true;
- @observable layout: string = "";
- @observable title: string = "";
+ @observable layout: string = '';
+ @observable title: string = '';
@computed get newDocumentDropdown() {
return (
<div>
- <div className={"presBox-ribbon"} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
<div className="ribbon-box">
Slide Title: <br></br>
- <input className="ribbon-textInput" placeholder="..." type="text" name="fname"
- onChange={(e) => {
+ <input
+ className="ribbon-textInput"
+ placeholder="..."
+ type="text"
+ name="fname"
+ onChange={e => {
e.stopPropagation();
e.preventDefault();
- runInAction(() => this.title = e.target.value);
- }}>
- </input>
+ runInAction(() => (this.title = e.target.value));
+ }}></input>
</div>
<div className="ribbon-box">
Choose type:
<div className="ribbon-doubleButton">
- <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? "" : Colors.LIGHT_BLUE }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Text</div>
- <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : "" }} onClick={action(() => this.addFreeform = !this.addFreeform)}>Freeform</div>
+ <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+ Text
+ </div>
+ <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+ Freeform
+ </div>
</div>
</div>
- <div className="ribbon-box" style={{ display: this.addFreeform ? "grid" : "none" }}>
+ <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}>
Preset layouts:
<div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}>
- <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'blank')} />
- <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'title')}>
+ <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} />
+ <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}>
<div className="title">Title</div>
<div className="subtitle">Subtitle</div>
</div>
- <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'header')}>
- <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>Section header</div>
+ <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}>
+ <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
+ Section header
+ </div>
</div>
- <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'content')}>
- <div className="title" style={{ alignSelf: 'center' }}>Title</div>
+ <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}>
+ <div className="title" style={{ alignSelf: 'center' }}>
+ Title
+ </div>
<div className="content">Text goes here</div>
</div>
- <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => this.layout = 'twoColumns')}>
- <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>Title</div>
- <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>Column one text</div>
- <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>Column two text</div>
+ <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}>
+ <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>
+ Title
+ </div>
+ <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>
+ Column one text
+ </div>
+ <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>
+ Column two text
+ </div>
</div>
</div>
- <div className="open-layout" onClick={action(() => this.openLayouts = !this.openLayouts)}>
- <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"caret-down"} size={"lg"} />
+ <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}>
+ <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} />
</div>
</div>
<div className="ribbon-final-box">
- <div className={this.title !== "" && (this.addFreeform && this.layout !== "" || !this.addFreeform) ? "ribbon-final-button-hidden" : "ribbon-final-button"} onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}>
+ <div
+ className={this.title !== '' && ((this.addFreeform && this.layout !== '') || !this.addFreeform) ? 'ribbon-final-button-hidden' : 'ribbon-final-button'}
+ onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}>
Create New Slide
</div>
</div>
</div>
- </div >
+ </div>
);
}
@@ -1738,7 +2104,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let doc = undefined;
if (layout) doc = this.createTemplate(layout);
if (freeform && layout) doc = this.createTemplate(layout, title);
- if (!freeform && !layout) doc = Docs.Create.TextDocument("", { _nativeWidth: 400, _width: 225, title: title });
+ if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title });
if (doc) {
const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
const data = Cast(presCollection?.data, listSpec(Doc));
@@ -1748,10 +2114,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
TabDocView.PinDoc(doc);
this.gotoDocument(this.childDocs.length, this.activeItem);
} else {
- this.props.addDocTab(doc, "add:right");
+ this.props.addDocTab(doc, 'add:right');
}
}
- }
+ };
createTemplate = (layout: string, input?: string) => {
const activeItem: Doc = this.activeItem;
@@ -1763,43 +2129,59 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
y = NumCast(targetDoc.y) + NumCast(targetDoc._height) + 20;
}
let doc = undefined;
- const title = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 58, _fontSize: "24pt", });
- const subtitle = Docs.Create.TextDocument("Click to change subtitle", { title: "Slide subtitle", _width: 380, _height: 50, x: 10, y: 118, _fontSize: "16pt" });
- const header = Docs.Create.TextDocument("Click to change header", { title: "Slide header", _width: 380, _height: 65, x: 10, y: 80, _fontSize: "20pt" });
- const contentTitle = Docs.Create.TextDocument("Click to change title", { title: "Slide title", _width: 380, _height: 60, x: 10, y: 10, _fontSize: "24pt" });
- const content = Docs.Create.TextDocument("Click to change text", { title: "Slide text", _width: 380, _height: 145, x: 10, y: 70, _fontSize: "14pt" });
- const content1 = Docs.Create.TextDocument("Click to change text", { title: "Column 1", _width: 185, _height: 140, x: 10, y: 80, _fontSize: "14pt" });
- const content2 = Docs.Create.TextDocument("Click to change text", { title: "Column 2", _width: 185, _height: 140, x: 205, y: 80, _fontSize: "14pt" });
+ const title = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
+ const subtitle = Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
+ const header = Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
+ const contentTitle = Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
+ const content = Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
+ const content1 = Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
+ const content2 = Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
switch (layout) {
case 'blank':
- doc = Docs.Create.FreeformDocument([], { title: input ? input : "Blank slide", _width: 400, _height: 225, x: x, y: y });
+ doc = Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x: x, y: y });
break;
case 'title':
- doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : "Title slide", _width: 400, _height: 225, _fitToBox: true, x: x, y: y });
+ doc = Docs.Create.FreeformDocument([title, subtitle], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
break;
case 'header':
- doc = Docs.Create.FreeformDocument([header], { title: input ? input : "Section header", _width: 400, _height: 225, _fitToBox: true, x: x, y: y });
+ doc = Docs.Create.FreeformDocument([header], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
break;
case 'content':
- doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : "Title and content", _width: 400, _height: 225, _fitToBox: true, x: x, y: y });
+ doc = Docs.Create.FreeformDocument([contentTitle, content], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
break;
case 'twoColumns':
- doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : "Title and two columns", _width: 400, _height: 225, _fitToBox: true, x: x, y: y });
+ doc = Docs.Create.FreeformDocument([contentTitle, content1, content2], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x: x, y: y });
break;
default:
break;
}
return doc;
- }
+ };
// Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view)
@computed get presentDropdown() {
return (
- <div className={`dropdown-play ${this.presentTools ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.updateMinimize(); this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }))}>
+ <div className={`dropdown-play ${this.presentTools ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div
+ className="dropdown-play-button"
+ onClick={undoBatch(
+ action(() => {
+ this.updateMinimize();
+ this.turnOffEdit(true);
+ this.gotoDocument(this.itemIndex, this.activeItem);
+ })
+ )}>
Mini-player
</div>
- <div className="dropdown-play-button" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "manual"; this.turnOffEdit(true); this.gotoDocument(this.itemIndex, this.activeItem); }))}>
+ <div
+ className="dropdown-play-button"
+ onClick={undoBatch(
+ action(() => {
+ this.layoutDoc.presStatus = 'manual';
+ this.turnOffEdit(true);
+ this.gotoDocument(this.itemIndex, this.activeItem);
+ })
+ )}>
Sidebar player
</div>
</div>
@@ -1810,7 +2192,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
- const currentFrame = Cast(tagDoc._currentFrame, "number", null);
+ const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
if (currentFrame === undefined) {
tagDoc._currentFrame = 0;
// CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
@@ -1820,19 +2202,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
- }
+ };
@action
prevKeyframe = (tagDoc: Doc, actItem: Doc): void => {
const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
- const currentFrame = Cast(tagDoc._currentFrame, "number", null);
+ const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
if (currentFrame === undefined) {
tagDoc._currentFrame = 0;
// CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
}
CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
- }
+ };
/**
* Returns the collection type as a string for headers
@@ -1843,15 +2225,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let type: string = '';
if (activeItem) {
switch (targetDoc.type) {
- case DocumentType.PDF: type = "PDF"; break;
- case DocumentType.RTF: type = "Text node"; break;
- case DocumentType.COL: type = "Collection"; break;
- case DocumentType.AUDIO: type = "Audio"; break;
- case DocumentType.VID: type = "Video"; break;
- case DocumentType.IMG: type = "Image"; break;
- case DocumentType.WEB: type = "Web page"; break;
- case DocumentType.MAP: type = "Map"; break;
- default: type = "Other node"; break;
+ case DocumentType.PDF:
+ type = 'PDF';
+ break;
+ case DocumentType.RTF:
+ type = 'Text node';
+ break;
+ case DocumentType.COL:
+ type = 'Collection';
+ break;
+ case DocumentType.AUDIO:
+ type = 'Audio';
+ break;
+ case DocumentType.VID:
+ type = 'Video';
+ break;
+ case DocumentType.IMG:
+ type = 'Image';
+ break;
+ case DocumentType.WEB:
+ type = 'Web page';
+ break;
+ case DocumentType.MAP:
+ type = 'Map';
+ break;
+ default:
+ type = 'Other node';
+ break;
}
}
return type;
@@ -1860,66 +2260,122 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable private openActiveColorPicker: boolean = false;
@observable private openViewedColorPicker: boolean = false;
-
-
@computed get progressivizeDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
if (activeItem && targetDoc) {
- const activeFontColor = targetDoc["pres-text-color"] ? StrCast(targetDoc["pres-text-color"]) : "Black";
- const viewedFontColor = targetDoc["pres-text-viewed-color"] ? StrCast(targetDoc["pres-text-viewed-color"]) : "Black";
+ const activeFontColor = targetDoc['pres-text-color'] ? StrCast(targetDoc['pres-text-color']) : 'Black';
+ const viewedFontColor = targetDoc['pres-text-viewed-color'] ? StrCast(targetDoc['pres-text-viewed-color']) : 'Black';
return (
<div>
- <div className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === "edit" ? "active" : ""}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div
+ className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`}
+ onClick={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
+ onPointerDown={e => e.stopPropagation()}>
<div className="ribbon-box">
{this.stringType} selected
- <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeChild}>Contents</div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editProgressivize}>Edit</div>
+ <div
+ className="ribbon-doubleButton"
+ style={{
+ borderTop: 'solid 1px darkgrey',
+ display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
+ }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}>
+ Contents
+ </div>
+ <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}>
+ Edit
+ </div>
</div>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}>
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Active text color</div>
- <div className="ribbon-colorBox" style={{ backgroundColor: activeFontColor, height: 15, width: 15 }} onClick={action(() => { this.openActiveColorPicker = !this.openActiveColorPicker; })}>
- </div>
+ <div
+ className="ribbon-colorBox"
+ style={{ backgroundColor: activeFontColor, height: 15, width: 15 }}
+ onClick={action(() => {
+ this.openActiveColorPicker = !this.openActiveColorPicker;
+ })}></div>
</div>
{this.activeColorPicker}
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? "inline-flex" : "none" }}>
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Viewed font color</div>
- <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => this.openViewedColorPicker = !this.openViewedColorPicker)}>
- </div>
+ <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div>
</div>
{this.viewedColorPicker}
- <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? "inline-flex" : "none" }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeZoom}>Zoom</div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editZoomProgressivize}>Edit</div>
+ <div
+ className="ribbon-doubleButton"
+ style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}>
+ Zoom
+ </div>
+ <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}>
+ Edit
+ </div>
</div>
- <div className="ribbon-doubleButton" style={{ borderTop: 'solid 1px darkgrey', display: targetDoc._viewType === "stacking" || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? "inline-flex" : "none" }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.progressivizeScroll}>Scroll</div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : "" }} onClick={this.editScrollProgressivize}>Edit</div>
+ <div
+ className="ribbon-doubleButton"
+ style={{
+ borderTop: 'solid 1px darkgrey',
+ display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
+ }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}>
+ Scroll
+ </div>
+ <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}>
+ Edit
+ </div>
</div>
</div>
<div className="ribbon-final-box">
Frames
<div className="ribbon-doubleButton">
<div className="ribbon-frameSelector">
- <div key="back" title="back frame" className="backKeyframe" onClick={e => { e.stopPropagation(); this.prevKeyframe(targetDoc, activeItem); }}>
- <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
+ <div
+ key="back"
+ title="back frame"
+ className="backKeyframe"
+ onClick={e => {
+ e.stopPropagation();
+ this.prevKeyframe(targetDoc, activeItem);
+ }}>
+ <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
</div>
- <div key="num" title="toggle view all" className="numKeyframe" style={{ color: targetDoc.keyFrameEditing ? "white" : "black", backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
- onClick={action(() => targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing)} >
+ <div
+ key="num"
+ title="toggle view all"
+ className="numKeyframe"
+ style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
+ onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}>
{NumCast(targetDoc._currentFrame)}
</div>
- <div key="fwd" title="forward frame" className="fwdKeyframe" onClick={e => { e.stopPropagation(); this.nextKeyframe(targetDoc, activeItem); }}>
- <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
+ <div
+ key="fwd"
+ title="forward frame"
+ className="fwdKeyframe"
+ onClick={e => {
+ e.stopPropagation();
+ this.nextKeyframe(targetDoc, activeItem);
+ }}>
+ <FontAwesomeIcon icon={'caret-right'} size={'lg'} />
</div>
</div>
- <Tooltip title={<><div className="dash-tooltip">{"Last frame"}</div></>}><div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div></Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Last frame'}</div>
+ </>
+ }>
+ <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div>
+ </Tooltip>
</div>
<div className="ribbon-frameList">
{this.frameListHeader}
{this.frameList}
</div>
- <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(" TODO: play frames")}>Play</div>
+ <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}>
+ Play
+ </div>
</div>
</div>
</div>
@@ -1933,37 +2389,41 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
const val = String(color.hex);
- targetDoc["pres-text-color"] = val;
+ targetDoc['pres-text-color'] = val;
return true;
- }
+ };
@undoBatch
@action
switchPresented = (color: ColorState) => {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
const val = String(color.hex);
- targetDoc["pres-text-viewed-color"] = val;
+ targetDoc['pres-text-viewed-color'] = val;
return true;
- }
+ };
@computed get activeColorPicker() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- 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(targetDoc["pres-text-color"])} />;
+ 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(targetDoc['pres-text-color'])}
+ />
+ );
}
@computed get viewedColorPicker() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- 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(targetDoc["pres-text-viewed-color"])} />;
+ 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(targetDoc['pres-text-viewed-color'])}
+ />
+ );
}
@action
@@ -1974,7 +2434,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (srcContext) this.togglePath(srcContext, true);
}
// Turn off the progressivize editors for each document
- this.childDocs.forEach((doc) => {
+ this.childDocs.forEach(doc => {
doc.editSnapZoomProgressivize = false;
doc.editZoomProgressivize = false;
const targetDoc = Cast(doc.presentationTargetDoc, Doc, null);
@@ -1983,7 +2443,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// targetDoc.editScrollProgressivize = false;
}
});
- }
+ };
//Toggle whether the user edits or not
@action
@@ -1991,14 +2451,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
if (!targetDoc.editZoomProgressivize) {
- if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true; targetDoc.zoomProgressivize = true;
+ if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true;
+ targetDoc.zoomProgressivize = true;
targetDoc.editZoomProgressivize = true;
activeItem.editZoomProgressivize = true;
} else {
targetDoc.editZoomProgressivize = false;
activeItem.editZoomProgressivize = false;
}
- }
+ };
//Toggle whether the user edits or not
@action
@@ -2006,12 +2467,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
if (!targetDoc.editScrollProgressivize) {
- if (!targetDoc.scrollProgressivize) { targetDoc.scrollProgressivize = true; activeItem.scrollProgressivize = true; }
+ if (!targetDoc.scrollProgressivize) {
+ targetDoc.scrollProgressivize = true;
+ activeItem.scrollProgressivize = true;
+ }
targetDoc.editScrollProgressivize = true;
} else {
targetDoc.editScrollProgressivize = false;
}
- }
+ };
//Progressivize Zoom
@action
@@ -2027,7 +2491,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
targetDoc._currentFrame = 0;
targetDoc.lastFrame = 0;
}
- }
+ };
//Progressivize Zoom
@action
@@ -2043,7 +2507,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
targetDoc._currentFrame = 0;
targetDoc.lastFrame = 0;
}
- }
+ };
//Progressivize Child Docs
@action
@@ -2052,12 +2516,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
targetDoc._currentFrame = targetDoc.lastFrame;
if (!targetDoc.editProgressivize) {
- if (!activeItem.presProgressivize) { activeItem.presProgressivize = true; targetDoc.presProgressivize = true; }
+ if (!activeItem.presProgressivize) {
+ activeItem.presProgressivize = true;
+ targetDoc.presProgressivize = true;
+ }
targetDoc.editProgressivize = true;
} else {
targetDoc.editProgressivize = false;
}
- }
+ };
@action
progressivizeChild = (e: React.MouseEvent) => {
@@ -2079,40 +2546,48 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
targetDoc._currentFrame = 0;
targetDoc.keyFrameEditing = true;
}
- }
+ };
@action
checkMovementLists = (doc: Doc, xlist: any, ylist: any) => {
const x: List<number> = xlist;
const y: List<number> = ylist;
const tags: JSX.Element[] = [];
- let pathPoints = ""; //List of all of the pathpoints that need to be added
+ let pathPoints = ''; //List of all of the pathpoints that need to be added
for (let i = 0; i < x.length - 1; i++) {
if (y[i] || x[i]) {
- if (i === 0) pathPoints = (x[i] - 11) + "," + (y[i] + 33);
- else pathPoints = pathPoints + " " + (x[i] - 11) + "," + (y[i] + 33);
- tags.push(<div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>{i}</div>);
+ if (i === 0) pathPoints = x[i] - 11 + ',' + (y[i] + 33);
+ else pathPoints = pathPoints + ' ' + (x[i] - 11) + ',' + (y[i] + 33);
+ tags.push(
+ <div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>
+ {i}
+ </div>
+ );
}
}
- tags.push(<svg style={{ overflow: 'visible', position: 'absolute' }}><polyline
- points={pathPoints}
- style={{
- position: 'absolute',
- opacity: 1,
- stroke: "#000000",
- strokeWidth: 2,
- strokeDasharray: '10 5',
- }}
- fill="none"
- /></svg>);
+ tags.push(
+ <svg style={{ overflow: 'visible', position: 'absolute' }}>
+ <polyline
+ points={pathPoints}
+ style={{
+ position: 'absolute',
+ opacity: 1,
+ stroke: '#000000',
+ strokeWidth: 2,
+ strokeDasharray: '10 5',
+ }}
+ fill="none"
+ />
+ </svg>
+ );
return tags;
- }
+ };
@observable
toggleDisplayMovement = (doc: Doc) => {
if (doc.displayMovement) doc.displayMovement = false;
else doc.displayMovement = true;
- }
+ };
@action
checkList = (doc: Doc, list: any): number => {
@@ -2124,22 +2599,54 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1];
return x[NumCast(doc._currentFrame)];
} else return 100;
- }
+ };
@computed get progressivizeChildDocs() {
const targetDoc: Doc = this.targetDoc;
const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
const tags: JSX.Element[] = [];
docs.forEach((doc, index) => {
- if (doc["x-indexed"] && doc["y-indexed"]) {
- tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? "block" : "none" }}>{this.checkMovementLists(doc, doc["x-indexed"], doc["y-indexed"])}</div>);
+ if (doc['x-indexed'] && doc['y-indexed']) {
+ tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? 'block' : 'none' }}>{this.checkMovementLists(doc, doc['x-indexed'], doc['y-indexed'])}</div>);
}
tags.push(
- <div className="progressivizeButton" key={index} onPointerLeave={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0; }} onPointerOver={() => { if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5; }} onClick={e => { this.toggleDisplayMovement(doc); e.stopPropagation(); }} style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : "#c8c8c8", top: NumCast(doc.y), left: NumCast(doc.x) }}>
- <div className="progressivizeButton-prev"><FontAwesomeIcon icon={"caret-left"} size={"lg"} onClick={e => { e.stopPropagation(); this.prevAppearFrame(doc, index); }} /></div>
- <div className="progressivizeButton-frame">{doc.appearFrame}</div>
- <div className="progressivizeButton-next"><FontAwesomeIcon icon={"caret-right"} size={"lg"} onClick={e => { e.stopPropagation(); this.nextAppearFrame(doc, index); }} /></div>
- </div>);
+ <div
+ className="progressivizeButton"
+ key={index}
+ onPointerLeave={() => {
+ if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0;
+ }}
+ onPointerOver={() => {
+ if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5;
+ }}
+ onClick={e => {
+ this.toggleDisplayMovement(doc);
+ e.stopPropagation();
+ }}
+ style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : '#c8c8c8', top: NumCast(doc.y), left: NumCast(doc.x) }}>
+ <div className="progressivizeButton-prev">
+ <FontAwesomeIcon
+ icon={'caret-left'}
+ size={'lg'}
+ onClick={e => {
+ e.stopPropagation();
+ this.prevAppearFrame(doc, index);
+ }}
+ />
+ </div>
+ <div className="progressivizeButton-frame">{NumCast(doc.appearFrame)}</div>
+ <div className="progressivizeButton-next">
+ <FontAwesomeIcon
+ icon={'caret-right'}
+ size={'lg'}
+ onClick={e => {
+ e.stopPropagation();
+ this.nextAppearFrame(doc, index);
+ }}
+ />
+ </div>
+ </div>
+ );
});
return tags;
}
@@ -2148,25 +2655,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
nextAppearFrame = (doc: Doc, i: number): void => {
// const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
// const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null);
- const appearFrame = Cast(doc.appearFrame, "number", null);
+ const appearFrame = Cast(doc.appearFrame, 'number', null);
if (appearFrame === undefined) {
doc.appearFrame = 0;
}
doc.appearFrame = appearFrame + 1;
- this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame));
- }
+ this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
+ };
@action
prevAppearFrame = (doc: Doc, i: number): void => {
// const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
// const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null);
- const appearFrame = Cast(doc.appearFrame, "number", null);
+ const appearFrame = Cast(doc.appearFrame, 'number', null);
if (appearFrame === undefined) {
doc.appearFrame = 0;
}
doc.appearFrame = Math.max(0, appearFrame - 1);
- this.updateOpacityList(doc["opacity-indexed"], NumCast(doc.appearFrame));
- }
+ this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
+ };
@action
updateOpacityList = (list: any, frame: number) => {
@@ -2191,10 +2698,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
list = x;
}
- }
+ };
@computed get moreInfoDropdown() {
- return (<div></div>);
+ return <div></div>;
}
@computed
@@ -2205,33 +2712,41 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
toggleProperties = () => {
- if (CurrentUserUtils.propertiesWidth > 0) {
- CurrentUserUtils.propertiesWidth = 0;
+ if (SettingsManager.propertiesWidth > 0) {
+ SettingsManager.propertiesWidth = 0;
} else {
- CurrentUserUtils.propertiesWidth = 250;
+ SettingsManager.propertiesWidth = 250;
}
- }
+ };
@computed get toolbar() {
- const propIcon = CurrentUserUtils.propertiesWidth > 0 ? "angle-double-right" : "angle-double-left";
- const propTitle = CurrentUserUtils.propertiesWidth > 0 ? "Close Presentation Panel" : "Open Presentation Panel";
+ 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 isMini: boolean = this.toolbarWidth <= 100;
- const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation);
+ const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation;
const activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
- return (mode === CollectionViewType.Carousel3D) ? (null) : (
+ return mode === CollectionViewType.Carousel3D ? 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"} />
<FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
</div></Tooltip> */}
- <Tooltip title={<><div className="dash-tooltip">{"View paths"}</div></>}>
- <div style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? "100%" : undefined }} className={"toolbar-button"} onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}>
- <FontAwesomeIcon icon={"exchange-alt"} />
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'View paths'}</div>
+ </>
+ }>
+ <div
+ style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
+ className={'toolbar-button'}
+ onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}>
+ <FontAwesomeIcon icon={'exchange-alt'} />
</div>
</Tooltip>
- {isMini ? (null) :
+ {isMini ? null : (
<>
<div className="toolbar-divider" />
{/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}>
@@ -2242,18 +2757,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
<div className="toolbar-divider" /> */}
- <Tooltip title={<><div className="dash-tooltip">{presKeyEvents ? "Keys are active" : "Keys are not active - click anywhere on the presentation trail to activate keys"}</div></>}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>
+ </>
+ }>
<div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
- <FontAwesomeIcon className={"toolbar-thumbtack"} icon={"keyboard"} style={{ color: presKeyEvents ? activeColor : inactiveColor }} />
+ <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: presKeyEvents ? activeColor : inactiveColor }} />
</div>
</Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{propTitle}</div></>}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{propTitle}</div>
+ </>
+ }>
<div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}>
- <FontAwesomeIcon className={"toolbar-thumbtack"} icon={propIcon} style={{ color: CurrentUserUtils.propertiesWidth > 0 ? activeColor : inactiveColor }} />
+ <FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} />
</div>
</Tooltip>
</>
- }
+ )}
</div>
);
}
@@ -2267,34 +2792,45 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
return (
- <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}>
- {isMini || Doc.UserDoc().noviceMode ? (null) : <select className="presBox-viewPicker"
- style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }}
- onPointerDown={e => e.stopPropagation()}
- onChange={this.viewChanged}
- value={mode}>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option>
- </select>}
+ <div className="presBox-buttons" style={{ 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={e => e.stopPropagation()} value={CollectionViewType.Stacking}>
+ List
+ </option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Tree}>
+ Tree
+ </option>
+ {Doc.noviceMode ? null : (
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>
+ 3D Carousel
+ </option>
+ )}
+ </select>
+ )}
<div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}>
- <span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}>
- <div className="presBox-button-left"
+ <span className={`presBox-button ${this.layoutDoc.presStatus === 'edit' ? 'present' : ''}`}>
+ <div
+ className="presBox-button-left"
onClick={undoBatch(() => {
if (this.childDocs.length) {
- this.layoutDoc.presStatus = "manual";
+ this.layoutDoc.presStatus = 'manual';
this.gotoDocument(this.itemIndex, this.activeItem);
}
})}>
- <FontAwesomeIcon icon={"play-circle"} />
- <div style={{ display: this.props.PanelWidth() > 200 ? "inline-flex" : "none" }}>&nbsp; Present</div>
+ <FontAwesomeIcon icon={'play-circle'} />
+ <div style={{ display: this.props.PanelWidth() > 200 ? 'inline-flex' : 'none' }}>&nbsp; Present</div>
</div>
- {(mode === CollectionViewType.Carousel3D || isMini) ? (null) : <div className={`presBox-button-right ${this.presentTools ? "active" : ""}`}
- onClick={(action(() => {
- if (this.childDocs.length) this.presentTools = !this.presentTools;
- }))}>
- <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={"angle-down"} />
- {this.presentDropdown}
- </div>}
+ {mode === CollectionViewType.Carousel3D || isMini ? null : (
+ <div
+ className={`presBox-button-right ${this.presentTools ? 'active' : ''}`}
+ onClick={action(() => {
+ if (this.childDocs.length) this.presentTools = !this.presentTools;
+ })}>
+ <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} />
+ {this.presentDropdown}
+ </div>
+ )}
</span>
{this.playButtons}
</div>
@@ -2306,7 +2842,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
getList = (list: any): List<number> => {
const x: List<number> = list;
return x;
- }
+ };
@action
updateList = (list: any): List<number> => {
@@ -2315,7 +2851,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
x.length + 1;
x[x.length - 1] = NumCast(targetDoc._scrollY);
return x;
- }
+ };
@action
newFrame = () => {
@@ -2324,7 +2860,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const type: string = StrCast(targetDoc.type);
if (!activeItem.frameList) activeItem.frameList = new List<number>();
switch (type) {
- case (DocumentType.PDF || DocumentType.RTF || DocumentType.WEB):
+ case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
this.updateList(activeItem.frameList);
break;
case DocumentType.COL:
@@ -2332,20 +2868,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
default:
break;
}
- }
+ };
@computed get frameListHeader() {
- return (<div className="frameList-header">
- &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : (null)}
- <div className={"frameList-headerButtons"}>
- <Tooltip title={<><div className="dash-tooltip">{"Add frame by example"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); this.newFrame(); }}>
- <FontAwesomeIcon icon={"plus"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Edit in collection"}</div></>}><div className={"headerButton"} onClick={e => { e.stopPropagation(); console.log('New frame'); }}>
- <FontAwesomeIcon icon={"edit"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
+ return (
+ <div className="frameList-header">
+ &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
+ <div className={'frameList-headerButtons'}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Add frame by example'}</div>
+ </>
+ }>
+ <div
+ className={'headerButton'}
+ onClick={e => {
+ e.stopPropagation();
+ this.newFrame();
+ }}>
+ <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Edit in collection'}</div>
+ </>
+ }>
+ <div
+ className={'headerButton'}
+ onClick={e => {
+ e.stopPropagation();
+ console.log('New frame');
+ }}>
+ <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ </div>
</div>
- </div>);
+ );
}
@computed get frameList() {
@@ -2353,66 +2915,175 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
const frameList: List<number> = this.getList(activeItem.frameList);
if (frameList) {
- const frameItems = frameList.map((value) =>
- <div className="framList-item">
-
- </div>
- );
- return (
-
- <div className="frameList-container">
- {frameItems}
- </div>
- );
- } else return (null);
-
+ const frameItems = frameList.map(value => <div className="framList-item"></div>);
+ return <div className="frameList-container">{frameItems}</div>;
+ } else return null;
}
@computed get playButtonFrames() {
const targetDoc: Doc = this.targetDoc;
return (
<>
- {this.targetDoc ? <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? "inline-flex" : "none" }}>
- <div>{targetDoc._currentFrame}</div>
- <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
- <div>{targetDoc.lastFrame}</div>
- </div> : null}
+ {this.targetDoc ? (
+ <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
+ <div>{NumCast(targetDoc._currentFrame)}</div>
+ <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
+ <div>{NumCast(targetDoc.lastFrame)}</div>
+ </div>
+ ) : null}
</>
);
}
@computed get playButtons() {
- const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1);
- const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0);
+ const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+ const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
// 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" }}>
- <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip>
- <div className="presPanel-divider"></div>
- <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div>
- <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? "pause" : "play"} /></div></Tooltip>
- <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div>
- <div className="presPanel-divider"></div>
- <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}><b>1</b></div></Tooltip>
- <div
- className="presPanel-button-text"
- onClick={() => this.gotoDocument(0, this.activeItem)}
- style={{ display: this.props.PanelWidth() > 250 ? "inline-flex" : "none" }}>
- Slide {this.itemIndex + 1} / {this.childDocs.length}
- {this.playButtonFrames}
+ return (
+ <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Loop'}</div>
+ </>
+ }>
+ <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}>
+ <FontAwesomeIcon icon={'redo-alt'} />
+ </div>
+ </Tooltip>
+ <div className="presPanel-divider"></div>
+ <div
+ className="presPanel-button"
+ style={{ opacity: presStart ? 0.4 : 1 }}
+ onClick={() => {
+ this.back();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ }}>
+ <FontAwesomeIcon icon={'arrow-left'} />
+ </div>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>
+ </>
+ }>
+ <div className="presPanel-button" onClick={this.startOrPause}>
+ <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
+ </div>
+ </Tooltip>
+ <div
+ className="presPanel-button"
+ style={{ opacity: presEnd ? 0.4 : 1 }}
+ onClick={() => {
+ this.next();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ }}>
+ <FontAwesomeIcon icon={'arrow-right'} />
+ </div>
+ <div className="presPanel-divider"></div>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Click to return to 1st slide'}</div>
+ </>
+ }>
+ <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}>
+ <b>1</b>
+ </div>
+ </Tooltip>
+ <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
+ Slide {this.itemIndex + 1} / {this.childDocs.length}
+ {this.playButtonFrames}
+ </div>
+ <div className="presPanel-divider"></div>
+ {this.props.PanelWidth() > 250 ? (
+ <div
+ className="presPanel-button-text"
+ onClick={undoBatch(
+ action(() => {
+ this.layoutDoc.presStatus = 'edit';
+ clearTimeout(this._presTimer);
+ })
+ )}>
+ EXIT
+ </div>
+ ) : (
+ <div className="presPanel-button" onClick={undoBatch(action(() => (this.layoutDoc.presStatus = 'edit')))}>
+ <FontAwesomeIcon icon={'times'} />
+ </div>
+ )}
</div>
- <div className="presPanel-divider"></div>
- {this.props.PanelWidth() > 250 ? <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.layoutDoc.presStatus = "edit"; clearTimeout(this._presTimer); }))}>EXIT</div>
- : <div className="presPanel-button" onClick={undoBatch(action(() => this.layoutDoc.presStatus = "edit"))}>
- <FontAwesomeIcon icon={"times"} />
- </div>}
- </div>);
+ );
}
@action
startOrPause = () => {
if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
else this.pauseAutoPres();
- }
+ };
+
+ @action
+ prevClicked = (e: PointerEvent) => {
+ this.back();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ };
+
+ @action
+ nextClicked = (e: PointerEvent) => {
+ this.next();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ };
+ @undoBatch
+ @action
+ exitClicked = (e: PointerEvent) => {
+ this.updateMinimize();
+ this.layoutDoc.presStatus = PresStatus.Edit;
+ clearTimeout(this._presTimer);
+ };
+
+ @action
+ startMarqueeCreateSlide = () => {
+ PresBox.startMarquee = true;
+ };
+
+ AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ var indexNum = 0;
+ for (let i = 0; i < index.length; i++) {
+ indexNum += index[i] * 10 ** -i;
+ }
+ if (this._treeViewMap.get(treeViewDoc) !== indexNum) {
+ this._treeViewMap.set(treeViewDoc, indexNum);
+ const sorted = this.sort(this._treeViewMap);
+ const curList = DocListCast(this.dataDoc[this.presFieldKey]);
+ if (sorted.length !== curList.length || sorted.some((doc, ind) => doc !== curList[ind])) {
+ this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs
+ }
+ }
+ return this.childDocs;
+ };
+
+ RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ 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() {
// calling this method for keyEvents
@@ -2420,60 +3091,113 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// needed to ensure that the childDocs are loaded for looking up fields
this.childDocs.slice();
const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
- const presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.UserDoc().activePresentation);
- const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1);
- const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0);
- return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ?
- <div className="miniPres">
- <div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
- <Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip>
+ const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation;
+ const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+ const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
+ return DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc) ? (
+ <div className="miniPres" onClick={e => e.stopPropagation()}>
+ <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Loop'}</div>
+ </>
+ }>
+ <div
+ className="presPanel-button"
+ style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }}
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}>
+ <FontAwesomeIcon icon={'redo-alt'} />
+ </div>
+ </Tooltip>
<div className="presPanel-divider"></div>
- <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onClick={() => { this.back(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-left"} /></div>
- <Tooltip title={<><div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? "Pause" : "Autoplay"}</div></>}><div className="presPanel-button" onClick={this.startOrPause}><FontAwesomeIcon icon={this.layoutDoc.presStatus === "auto" ? "pause" : "play"} /></div></Tooltip>
- <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onClick={() => { this.next(); if (this._presTimer) { clearTimeout(this._presTimer); this.layoutDoc.presStatus = PresStatus.Manual; } }}><FontAwesomeIcon icon={"arrow-right"} /></div>
+ <div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}>
+ <FontAwesomeIcon icon={'arrow-left'} />
+ </div>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>
+ </>
+ }>
+ <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.startOrPause, false, false)}>
+ <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} />
+ </div>
+ </Tooltip>
+ <div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}>
+ <FontAwesomeIcon icon={'arrow-right'} />
+ </div>
<div className="presPanel-divider"></div>
- <Tooltip title={<><div className="dash-tooltip">{"Click to return to 1st slide"}</div></>}><div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}><b>1</b></div></Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Click to return to 1st slide'}</div>
+ </>
+ }>
+ <div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}>
+ <b>1</b>
+ </div>
+ </Tooltip>
<div className="presPanel-button-text">
Slide {this.itemIndex + 1} / {this.childDocs.length}
{this.playButtonFrames}
</div>
- <div className="presPanel-divider"></div>
- <div className="presPanel-button-text" onClick={undoBatch(action(() => { this.updateMinimize(); this.layoutDoc.presStatus = PresStatus.Edit; clearTimeout(this._presTimer); }))}>EXIT</div>
+ <div className="presPanel-divider" />
+ <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
+ EXIT
+ </div>
</div>
</div>
- :
- <div className="presBox-cont" style={{ minWidth: CurrentUserUtils.OverlayDocs.includes(this.layoutDoc) ? 240 : undefined }} >
+ ) : (
+ <div className="presBox-cont" style={{ minWidth: DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}>
{this.topPanel}
{this.toolbar}
{this.newDocumentToolbarDropdown}
<div className="presBox-listCont">
- {mode !== CollectionViewType.Invalid ?
- <CollectionView {...this.props}
- ContainingCollectionDoc={this.props.Document}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={this.panelHeight}
- childIgnoreNativeSize={true}
- moveDocument={returnFalse}
- childFitWidth={returnTrue}
- childOpacity={returnOne}
- childLayoutTemplate={this.childLayoutTemplate}
- filterAddDocument={this.addDocumentFilter}
- removeDocument={returnFalse}
- dontRegisterView={true}
- focus={this.selectElement}
- ScreenToLocalTransform={this.getTransform}
- />
- : (null)
+ <div className="Slide" style={{ height: `calc(100% - 30px)` }}>
+ {mode !== CollectionViewType.Invalid ? (
+ <CollectionView
+ {...this.props}
+ ContainingCollectionDoc={this.props.Document}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.panelHeight}
+ childIgnoreNativeSize={true}
+ moveDocument={returnFalse}
+ childFitWidth={returnTrue}
+ childOpacity={returnOne}
+ childLayoutTemplate={this.childLayoutTemplate}
+ filterAddDocument={this.addDocumentFilter}
+ removeDocument={returnFalse}
+ dontRegisterView={true}
+ focus={this.selectElement}
+ scriptContext={this}
+ ScreenToLocalTransform={this.getTransform}
+ AddToMap={this.AddToMap}
+ RemFromMap={this.RemFromMap}
+ hierarchyIndex={[]}
+ />
+ ) : null}
+ </div>
+
+ {
+ // if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack
+ <Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}>
+ <button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}>
+ + NEW SLIDE
+ </button>
+ </Tooltip>
}
</div>
- </div>;
+ </div>
+ );
}
}
-ScriptingGlobals.add(function lookupPresBoxField(container: Doc, field: string, data: Doc) {
- if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data);
- if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 35 : 31;
- if (field === 'presStatus') return container.presStatus;
- if (field === '_itemIndex') return container._itemIndex;
- if (field === 'presBox') return container;
- return undefined;
-}); \ No newline at end of file
+
+ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
+ const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
+ const openInTab = (doc: Doc, finished?: () => void) => {
+ CollectionDockingView.AddSplit(doc, 'right');
+ finished?.();
+ };
+ DocumentManager.Instance.jumpToDocument(bestTarget, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, () => PresBox.navigateToDoc(bestTarget, activeItem, true), undefined, true, NumCast(activeItem.presZoom));
+});
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 1ad4b820e..969f034a8 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -5,155 +5,157 @@ $slide-background: #d5dce2;
$slide-active: #5B9FDD;
.presItem-container {
- cursor: grab;
- display: grid;
- grid-template-columns: 20px auto;
- font-family: Roboto;
- letter-spacing: normal;
- position: relative;
- pointer-events: all;
- width: 100%;
- height: 100%;
- font-weight: 400;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- align-items: center;
+ cursor: grab;
+ display: flex;
+ grid-template-columns: 20px auto;
+ font-family: Roboto;
+ letter-spacing: normal;
+ position: relative;
+ pointer-events: all;
+ width: 100%;
+ height: 100%;
+ font-weight: 400;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ align-items: center;
- .presItem-number {
- margin-top: 3.5px;
- font-size: 12px;
- font-weight: 700;
- text-align: center;
- justify-self: center;
- align-self: flex-start;
- position: relative;
- display: inline-block;
- overflow: hidden;
- }
+ // .presItem-number {
+ // margin-top: 3.5px;
+ // font-size: 12px;
+ // font-weight: 700;
+ // text-align: center;
+ // justify-self: center;
+ // align-self: flex-start;
+ // position: relative;
+ // display: inline-block;
+ // overflow: hidden;
+ // }
}
.presItem-slide {
- position: relative;
- background-color: #d5dce2;
- border-radius: 5px;
- height: calc(100% - 7px);
- width: calc(100% - 15px);
- display: grid;
- grid-template-rows: 16px 10px auto;
- grid-template-columns: max-content max-content max-content max-content auto;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ border-bottom: .5px solid grey;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ grid-template-rows: 16px 10px auto;
+ grid-template-columns: max-content max-content max-content max-content auto;
- .presItem-name {
- min-width: 20px;
- z-index: 300;
- top: 2px;
- align-self: center;
- font-size: 11px;
- font-family: Roboto;
- font-weight: 500;
- position: relative;
- padding-left: 10px;
- padding-right: 10px;
- letter-spacing: normal;
- width: max-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: pre;
- }
-
- .presItem-docName {
- min-width: 20px;
- z-index: 300;
- align-self: center;
- font-size: 9px;
- font-family: Roboto;
- font-weight: 300;
- position: relative;
- padding-left: 10px;
- padding-right: 10px;
- letter-spacing: normal;
- width: max-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: pre;
- grid-row: 2;
- grid-column: 1/6;
- }
-
- .presItem-time {
- align-self: center;
- position: relative;
- padding-right: 10px;
- top: 1px;
- font-size: 10;
- font-weight: 300;
- font-family: Roboto;
- z-index: 300;
- letter-spacing: normal;
- }
-
- .presItem-embedded {
- overflow: hidden;
- grid-row: 3;
- grid-column: 1/8;
- position: relative;
- display: flex;
- width: auto;
- justify-content: center;
- margin: auto;
- margin-bottom: 2px;
- border-bottom-left-radius: 5px;
- border-bottom-right-radius: 5px;
- }
-
- .presItem-embeddedMask {
- width: 100%;
- height: 100%;
- position: absolute;
- border-radius: 3px;
- top: 0;
- left: 0;
- z-index: 1;
- overflow: hidden;
- }
-
-
- .presItem-slideButtons {
- display: flex;
- grid-column: 7;
- grid-row: 1/3;
- width: max-content;
- justify-self: right;
- justify-content: flex-end;
-
- .slideButton {
- cursor: pointer;
+ .presItem-name {
+ display: flex;
+ min-width: 20px;
+ z-index: 300;
+ top: 2px;
+ align-self: center;
+ font-size: 11px;
+ font-family: Roboto;
+ font-weight: 500;
position: relative;
- border-radius: 100px;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ }
+
+ .presItem-docName {
+ min-width: 20px;
z-index: 300;
- width: 18px;
- height: 18px;
- display: flex;
- font-size: 12px;
- justify-self: center;
align-self: center;
- background-color: rgba(0, 0, 0, 0.5);
- color: white;
+ font-size: 9px;
+ font-family: Roboto;
+ font-weight: 300;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ grid-row: 2;
+ grid-column: 1/6;
+ }
+
+ .presItem-time {
+ align-self: center;
+ position: relative;
+ padding-right: 10px;
+ top: 1px;
+ font-size: 10;
+ font-weight: 300;
+ font-family: Roboto;
+ z-index: 300;
+ letter-spacing: normal;
+ }
+
+ .presItem-embedded {
+ overflow: hidden;
+ grid-row: 3;
+ grid-column: 1/8;
+ position: relative;
+ display: flex;
+ width: auto;
justify-content: center;
- align-items: center;
- transition: 0.2s;
- margin-right: 3px;
- }
-
- .slideButton:hover {
- background-color: rgba(0, 0, 0, 1);
- transform: scale(1.2);
- }
- }
+ margin: auto;
+ margin-bottom: 2px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
+
+ .presItem-embeddedMask {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ border-radius: 3px;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ overflow: hidden;
+ }
+
+
+ .presItem-slideButtons {
+ display: flex;
+ grid-column: 7;
+ grid-row: 1/3;
+ width: max-content;
+ justify-self: right;
+ justify-content: flex-end;
+
+ .slideButton {
+ cursor: pointer;
+ position: relative;
+ border-radius: 100px;
+ z-index: 300;
+ width: 18px;
+ height: 18px;
+ display: flex;
+ font-size: 12px;
+ justify-self: center;
+ align-self: center;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ align-items: center;
+ transition: 0.2s;
+ margin-right: 3px;
+ }
+
+ .slideButton:hover {
+ background-color: rgba(0, 0, 0, 1);
+ transform: scale(1.2);
+ }
+ }
}
// .presItem-slide:hover {
@@ -192,44 +194,116 @@ $slide-active: #5B9FDD;
// }
.presItem-slide.active {
- box-shadow: 0 0 0px 1.5px $dark-blue;
+ box-shadow: 0 0 0px 2.5px $dark-blue;
}
-.presItem-multiDrag {
- font-family: Roboto;
- font-weight: 600;
- color: white;
- text-align: center;
- justify-content: center;
- align-content: center;
- width: 100px;
- height: 30px;
+.presItem-slide.group {
+ border-radius: 5px;
+}
+
+.presItem-slide.activeGroup {
+ border-radius: 5px;
+ box-shadow: 0 0 0px 2.5px $dark-blue;
+}
+
+.presItem-groupSlideContainer {
position: absolute;
- background-color: $dark-blue;
- z-index: 4000;
- border-radius: 10px;
- box-shadow: black 0.4vw 0.4vw 0.8vw;
- line-height: 30px;
+ /* grid-row: 3; */
+ /* grid-column: 1/8; */
+ top: 28;
+ display: block;
+ background: #92adb9;
+ width: 100%;
+ height: 10px;
+ border-radius: 0px 0px 5px 5px;
+ height: calc(100% - 28px);
+ display: grid;
+ grid-template-rows: auto auto auto;
+ grid-template-columns: 100%;
+ justify-items: left;
+ align-items: center;
}
-.presItem-miniSlide {
- font-weight: 700;
- font-size: 12;
- grid-column: 1/8;
- align-self: center;
- justify-self: center;
+.presItem-groupSlide {
+ position: relative;
background-color: #d5dce2;
- width: 26px;
- text-align: center;
- height: 26px;
- line-height: 28px;
- border-radius: 100%;
+ border-radius: 5px;
+ height: calc(100% - 7px);
+ width: calc(100% - 20px);
+ left: 15px;
+ /* height: 20px; */
+ /* width: calc(100% - 19px); */
+ display: flex;
+ grid-template-rows: 16px 10px auto;
+ grid-template-columns: max-content max-content max-content max-content auto;
+}
+
+.presItem-multiDrag {
+ font-family: Roboto;
+ font-weight: 600;
+ color: white;
+ text-align: center;
+ justify-content: center;
+ align-content: center;
+ width: 100px;
+ height: 30px;
+ position: absolute;
+ background-color: $dark-blue;
+ z-index: 4000;
+ border-radius: 10px;
+ box-shadow: black 0.4vw 0.4vw 0.8vw;
+ line-height: 30px;
+}
+
+.presItem-miniSlide {
+ font-weight: 700;
+ font-size: 12;
+ grid-column: 1/8;
+ align-self: center;
+ justify-self: center;
+ background-color: #d5dce2;
+ width: 26px;
+ text-align: center;
+ height: 26px;
+ line-height: 28px;
+ border-radius: 100%;
}
.presItem-miniSlide.active {
- box-shadow: 0 0 0px 1.5px $dark-blue;
+ box-shadow: 0 0 0px 1.5px $dark-blue;
}
-// .presItem-slide:hover {
-// background: $slide-hover;
-// } \ No newline at end of file
+.expandButton {
+ cursor: pointer;
+ position: absolute;
+ border-radius: 100px;
+ bottom: 0;
+ left: -18;
+ z-index: 300;
+ width: 15px;
+ height: 15px;
+ display: flex;
+ font-size: 12px;
+ justify-self: center;
+ align-self: center;
+ background-color: #92adb9;
+ color: white;
+ justify-content: center;
+ align-items: center;
+ transition: 0.2s;
+ margin-right: 3px;
+}
+
+.expandButton:hover {
+ background-color: rgba(0, 0, 0, 1);
+ transform: scale(1.2);
+}
+
+.presItem-groupNum {
+ color: #d5dce2;
+ position: absolute;
+ left: -15px;
+ top: 1;
+ font-weight: 600;
+ font-size: 12;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5f252c3e9..0cf15d297 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,50 +1,66 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { DataSym, Doc, Opt } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-import { Cast, NumCast, StrCast } from "../../../../fields/Types";
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
-import { DocUtils } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
-import { DocumentManager } from "../../../util/DocumentManager";
-import { DragManager } from "../../../util/DragManager";
-import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+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 { 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 { ViewBoxBaseComponent } from '../../DocComponent';
-import { EditableView } from "../../EditableView";
-import { Colors } from "../../global/globalEnums";
-import { DocumentView, DocumentViewProps } from "../../nodes/DocumentView";
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { DocumentView, DocumentViewProps } from '../../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../../nodes/FieldView';
-import { StyleProp } from "../../StyleProvider";
-import { PresBox } from "./PresBox";
-import "./PresElementBox.scss";
-import { PresMovement } from "./PresEnums";
-import React = require("react");
+import { StyleProp } from '../../StyleProvider';
+import { PresBox } from './PresBox';
+import './PresElementBox.scss';
+import { PresMovement } from './PresEnums';
+import React = require('react');
/**
* This class models the view a document added to presentation will have in the presentation.
* It involves some functionality for its buttons and options.
*/
@observer
export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PresElementBox, fieldKey);
+ }
_heightDisposer: IReactionDisposer | undefined;
@observable _dragging = false;
- // these fields are conditionally computed fields on the layout document that take this document as a parameter
- @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // 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 collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
- @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
- @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
- @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
- @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
+ // 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);
+ } // 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 collapsedHeight() {
+ return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(this.presBox._viewType as any) ? 35 : 31;
+ } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+ @computed get presStatus() {
+ return this.presBox.presStatus;
+ }
+ @computed get presBox() {
+ return (this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc)!;
+ }
+ @computed get targetDoc() {
+ return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc;
+ }
componentDidMount() {
this.layoutDoc.hideLinkButton = true;
- this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
- params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ this._heightDisposer = reaction(
+ () => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+ params => (this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0)),
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
this._heightDisposer?.();
@@ -58,28 +74,27 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
presExpandDocumentClick = () => {
this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
- }
+ };
embedHeight = (): number => 97;
// embedWidth = () => this.props.PanelWidth();
// embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
embedWidth = (): number => this.props.PanelWidth() - 35;
- styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+ styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (property === StyleProp.Opacity) return 1;
return this.props.styleProvider?.(doc, props, property);
- }
+ };
/**
* The function that is responsible for rendering a preview or not for this
* presentation element.
*/
@computed get renderEmbeddedInline() {
- return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+ return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
<div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
<DocumentView
- Document={this.targetDoc}
- DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+ Document={this.rootDoc}
+ DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
styleProvider={this.styleProvider}
- layerProvider={this.props.layerProvider}
docViewPath={returnEmptyDoclist}
rootSelected={returnTrue}
addDocument={returnFalse}
@@ -87,6 +102,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
isContentActive={this.props.isContentActive}
addDocTab={returnFalse}
pinToPres={returnFalse}
+ fitContentsToBox={returnTrue}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
ScreenToLocalTransform={Transform.Identity}
@@ -103,29 +119,62 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
hideLinkButton={true}
/>
<div className="presItem-embeddedMask" />
- </div>;
+ </div>
+ );
}
+ @computed get renderGroupSlides() {
+ const childDocs = DocListCast(this.targetDoc.data);
+ const groupSlides = childDocs.map((doc: Doc, ind: number) => (
+ <div
+ className="presItem-groupSlide"
+ onClick={e => {
+ console.log('Clicked on slide with index: ', ind);
+ e.stopPropagation();
+ e.preventDefault();
+ PresBox.Instance.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>
+ {/* style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }} */}
+ <div className="presItem-name">
+ <EditableView
+ ref={this._titleRef}
+ editing={undefined}
+ contents={doc.title}
+ overflow={'ellipsis'}
+ GetValue={() => StrCast(doc.title)}
+ SetValue={(value: string) => {
+ doc.title = !value.trim().length ? '-untitled-' : value;
+ return true;
+ }}
+ />
+ </div>
+ </div>
+ ));
+ return groupSlides;
+ }
@computed get duration() {
let durationInS: number;
- if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) { durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime); durationInS = Math.round(durationInS * 10) / 10; }
- else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
+ if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) {
+ durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime);
+ durationInS = Math.round(durationInS * 10) / 10;
+ } else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
else durationInS = 2;
- return "D: " + durationInS + "s";
+ return 'D: ' + durationInS + 's';
}
@computed get transition() {
let transitionInS: number;
if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
else transitionInS = 0.5;
- return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
+ return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? null : 'M: ' + transitionInS + 's';
}
private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
private _titleRef: React.RefObject<EditableView> = React.createRef();
-
@action
headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
const element = e.target as any;
@@ -133,59 +182,71 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.preventDefault();
if (element && !(e.ctrlKey || e.metaKey)) {
if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
- PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+ PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
} else {
- setupMoveUpEvents(this, e, ((e: PointerEvent) => {
- PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
- return this.startDrag(e);
- }), emptyFunction, emptyFunction);
+ setupMoveUpEvents(
+ this,
+ e,
+ (e: PointerEvent) => {
+ PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
+ return this.startDrag(e);
+ },
+ emptyFunction,
+ emptyFunction
+ );
}
}
- }
-
- headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
- }
+ };
/**
* Function to drag and drop the pres element to a diferent location
*/
- //TODO: this is what you need to look into
startDrag = (e: PointerEvent) => {
const miniView: boolean = this.toolbarWidth <= 100;
const activeItem = this.rootDoc;
const dragArray = PresBox.Instance._dragArray;
const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
+ if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
+ dragData.dropAction = 'move';
+ dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc;
+ dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument;
const dragItem: HTMLElement[] = [];
if (dragArray.length === 1) {
- const doc = dragArray[0];
- doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
- dragItem.push(doc);
+ const doc = this._itemRef.current || dragArray[0];
+ if (doc) {
+ doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide';
+ dragItem.push(doc);
+ }
} else if (dragArray.length >= 1) {
const doc = document.createElement('div');
- doc.className = "presItem-multiDrag";
- doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
+ doc.className = 'presItem-multiDrag';
+ doc.innerText = 'Move ' + PresBox.Instance._selectedArray.size + ' slides';
doc.style.position = 'absolute';
- doc.style.top = (e.clientY) + 'px';
- doc.style.left = (e.clientX - 50) + 'px';
+ doc.style.top = e.clientY + 'px';
+ doc.style.left = e.clientX - 50 + 'px';
dragItem.push(doc);
}
// const dropEvent = () => runInAction(() => this._dragging = false);
if (activeItem) {
- DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
+ DragManager.StartDocumentDrag(
+ dragItem.map(ele => ele),
+ dragData,
+ e.clientX,
+ e.clientY,
+ undefined
+ );
// runInAction(() => this._dragging = true);
return true;
}
return false;
- }
+ };
onPointerOver = (e: any) => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- }
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointermove', this.onPointerMove);
+ };
onPointerMove = (e: PointerEvent) => {
const slide = this._itemRef.current!;
@@ -195,77 +256,181 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (slide && dragIsPresItem) {
const rect = slide.getBoundingClientRect();
- const y = e.clientY - rect.top; //y position within the element.
+ const y = e.clientY - rect.top; //y position within the element.
const height = slide.clientHeight;
const halfLine = height / 2;
if (y <= halfLine) {
slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
- slide.style.borderBottom = "0px";
+ slide.style.borderBottom = '0px';
} else if (y > halfLine) {
- slide.style.borderTop = "0px";
+ slide.style.borderTop = '0px';
slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
}
}
- document.removeEventListener("pointermove", this.onPointerMove);
- }
+ document.removeEventListener('pointermove', this.onPointerMove);
+ };
onPointerLeave = (e: any) => {
- this._itemRef.current!.style.borderTop = "0px";
- this._itemRef.current!.style.borderBottom = "0px";
- document.removeEventListener("pointermove", this.onPointerMove);
- }
+ this._itemRef.current!.style.borderTop = '0px';
+ this._itemRef.current!.style.borderBottom = '0px';
+ document.removeEventListener('pointermove', this.onPointerMove);
+ };
@action
toggleProperties = () => {
- if (CurrentUserUtils.propertiesWidth < 5) {
- action(() => (CurrentUserUtils.propertiesWidth = 250));
+ if (SettingsManager.propertiesWidth < 5) {
+ action(() => (SettingsManager.propertiesWidth = 250));
}
- }
+ };
@undoBatch
removeItem = action((e: React.MouseEvent) => {
+ e.stopPropagation();
this.props.removeDocument?.(this.rootDoc);
if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
PresBox.Instance._selectedArray.delete(this.rootDoc);
}
- e.stopPropagation();
+ this.removeAllRecordingInOverlay();
});
// set the value/title of the individual pres element
@undoBatch
@action
onSetValue = (value: string) => {
- this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
+ this.rootDoc.title = !value.trim().length ? '-untitled-' : value;
return true;
- }
+ };
/**
* Method called for updating the view of the currently selected document
- *
- * @param targetDoc
- * @param activeItem
+ *
+ * @param targetDoc
+ * @param activeItem
*/
@undoBatch
@action
updateView = (targetDoc: Doc, activeItem: Doc) => {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeItem.presPinTimecode = targetDoc._currentTimecode;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- } else {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
+ switch (targetDoc.type) {
+ case DocumentType.PDF:
+ case DocumentType.WEB:
+ case DocumentType.RTF:
+ const scroll = targetDoc._scrollTop;
+ activeItem.presPinViewScroll = scroll;
+ break;
+ case DocumentType.VID:
+ case DocumentType.AUDIO:
+ activeItem.presStartTime = targetDoc._currentTimecode;
+ break;
+ case DocumentType.COMPARISON:
+ const clipWidth = targetDoc._clipWidth;
+ activeItem.presPinClipWidth = clipWidth;
+ break;
+ default:
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeItem.presPinViewX = x;
+ activeItem.presPinViewY = y;
+ activeItem.presPinViewScale = scale;
}
+ };
+
+ @computed get recordingIsInOverlay() {
+ let isInOverlay = false;
+ DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
+ if (doc.slides === this.rootDoc) {
+ isInOverlay = true;
+ // return
+ }
+ });
+ return isInOverlay;
}
+ // a previously recorded video will have timecode defined
+ static videoIsRecorded = (activeItem: Doc) => {
+ const casted = Cast(activeItem.recording, Doc, null);
+ return casted && 'currentTimecode' in casted;
+ };
+
+ removeAllRecordingInOverlay = () => {
+ DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
+ if (doc.slides === this.rootDoc) {
+ Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ }
+ });
+ };
+
+ static removeEveryExistingRecordingInOverlay = () => {
+ // Remove every recording that already exists in overlay view
+ DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
+ // if it's a recording video, don't remove from overlay (user can lose data)
+ if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return;
+
+ if (doc.slides !== null) {
+ Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ }
+ });
+ };
+
+ @undoBatch
+ @action
+ hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => {
+ e.stopPropagation();
+ this.removeAllRecordingInOverlay();
+
+ // if (iconClick) PresElementBox.showVideo = false;
+ };
+
+ @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));
+ }
+ };
+
+ @undoBatch
+ @action
+ startRecording = (activeItem: Doc) => {
+ console.log('start recording', 'activeItem', activeItem);
+ if (PresElementBox.videoIsRecorded(activeItem)) {
+ // 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));
+ } else {
+ // Remove every recording that already exists in overlay view
+ // this is a design decision to clear to focus in on the recoding mode
+ PresElementBox.removeEveryExistingRecordingInOverlay();
+
+ // if we dont have any recording
+ const recording = Docs.Create.WebCamDocument('', {
+ _width: 384,
+ _height: 216,
+ hideDocumentButtonBar: true,
+ hideDecorationTitle: true,
+ hideOpenButton: true,
+ // hideDeleteButton: true,
+ cloneFieldFilter: new List<string>(['system']),
+ });
+
+ // attach the recording to the slide, and attach the slide to the recording
+ recording.slides = activeItem;
+ activeItem.recording = recording;
+
+ // make recording box appear in the bottom right corner of the screen
+ recording.x = window.innerWidth - recording[WidthSym]() - 20;
+ recording.y = window.innerHeight - recording[HeightSym]() - 20;
+ Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording);
+ }
+ };
+
@computed
get toolbarWidth(): number {
const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
@@ -275,29 +440,31 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return width;
}
- // TODO: this is what you were looking for
@computed get mainItem() {
- const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
+ const isSelected: boolean = PresBox.Instance?._selectedArray.has(this.rootDoc);
const toolbarWidth: number = this.toolbarWidth;
const showMore: boolean = this.toolbarWidth >= 300;
const miniView: boolean = this.toolbarWidth <= 110;
const presBox: Doc = this.presBox; //presBox
const presBoxColor: string = StrCast(presBox._backgroundColor);
- const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
+ const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
const targetDoc: Doc = this.targetDoc;
const activeItem: Doc = this.rootDoc;
+
return (
- <div className={`presItem-container`}
+ <div
+ className={`presItem-container`}
key={this.props.Document[Id] + this.indexInPres}
ref={this._itemRef}
style={{
- backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
- opacity: this._dragging ? 0.3 : 1
+ backgroundColor: presColorBool ? (isSelected ? 'rgba(250,250,250,0.3)' : 'transparent') : isSelected ? Colors.LIGHT_BLUE : 'transparent',
+ opacity: this._dragging ? 0.3 : 1,
}}
onClick={e => {
e.stopPropagation();
e.preventDefault();
PresBox.Instance.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();
@@ -305,73 +472,126 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
})}
onPointerOver={this.onPointerOver}
onPointerLeave={this.onPointerLeave}
- onPointerDown={this.headerDown}
- onPointerUp={this.headerUp}
- >
- {miniView ?
- // when width is LESS than 110 px
- <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
- {`${this.indexInPres + 1}.`}
- </div>
- :
- // when width is MORE than 110 px
- <div className="presItem-number">
- {`${this.indexInPres + 1}.`}
- </div>}
- {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
- style={{
- backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
- }}>
- <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
- <EditableView
- ref={this._titleRef}
- 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>
- <div className={"presItem-slideButtons"}>
- <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
- <div className="slideButton"
- onClick={() => this.updateView(targetDoc, activeItem)}
- style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
- </Tooltip>
- {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
- <div className="slideButton"
- onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
- style={{
- zIndex: 1000 - this.indexInPres,
- fontWeight: 700,
- backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
- height: activeItem.groupWithUp ? 53 : 18,
- transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
- }}>
- <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
- <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
+ onPointerDown={this.headerDown}>
+ {/* {miniView ?
+ // when width is LESS than 110 px
+ <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
+ {`${this.indexInPres + 1}.`}
+ </div>
+ :
+ // when width is MORE than 110 px
+ <div className="presItem-number">
+ {`${this.indexInPres + 1}.`}
+ </div>} */}
+ {/* <div className="presItem-number">
+ {`${this.indexInPres + 1}.`}
+ </div> */}
+ {miniView ? null : (
+ <div
+ ref={miniView ? null : this._dragRef}
+ className={`presItem-slide ${isSelected ? 'active' : ''}`}
+ style={{
+ backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+ boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
+ }}>
+ <div className="presItem-name" style={{ maxWidth: showMore ? toolbarWidth - 195 : toolbarWidth - 105, 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>
+ {/* <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> */}
+ <div className={'presItem-slideButtons'}>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Update view'}</div>
+ </>
+ }>
+ <div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}>
+ V
+ </div>
+ </Tooltip>
+
+ {this.recordingIsInOverlay ? (
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Hide Recording'}</div>
+ </>
+ }>
+ <div className="slideButton" onClick={e => this.hideRecording(e, true)} style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={'video-slash'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ ) : (
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>
+ </>
+ }>
+ <div
+ className="slideButton"
+ onClick={e => {
+ e.stopPropagation();
+ this.startRecording(activeItem);
+ }}
+ style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={'video'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ )}
+
+ {/* {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
+ <div className="slideButton"
+ onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
+ style={{
+ zIndex: 1000 - this.indexInPres,
+ fontWeight: 700,
+ backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
+ height: activeItem.groupWithUp ? 53 : 18,
+ transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
+ }}>
+ <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
+ <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </div>
+ </Tooltip>} */}
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>
+ </>
+ }>
+ <div
+ className={'slideButton'}
+ onClick={e => {
+ e.stopPropagation();
+ this.presExpandDocumentClick();
+ }}>
+ <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Remove from presentation'}</div>
+ </>
+ }>
+ <div className={'slideButton'} onClick={this.removeItem}>
+ <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
</div>
- </div>
- </Tooltip>}
- <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
- <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
- className={"slideButton"}
- onClick={this.removeItem}>
- <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
+ </Tooltip>
+ </div>
+ {/* <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> */}
+ {this.renderEmbeddedInline}
</div>
- <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
- {this.renderEmbeddedInline}
- </div>}
- </div >);
+ )}
+ </div>
+ );
}
render() {
- return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
+ return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? null : this.mainItem;
}
-} \ No newline at end of file
+}