aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes')
-rw-r--r--src/client/views/nodes/AudioBox.scss320
-rw-r--r--src/client/views/nodes/AudioBox.tsx986
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx16
-rw-r--r--src/client/views/nodes/ColorBox.tsx3
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx10
-rw-r--r--src/client/views/nodes/DataViz.scss0
-rw-r--r--src/client/views/nodes/DataViz.tsx21
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx16
-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.tsx61
-rw-r--r--src/client/views/nodes/DocumentView.scss7
-rw-r--r--src/client/views/nodes/DocumentView.tsx275
-rw-r--r--src/client/views/nodes/EquationBox.scss2
-rw-r--r--src/client/views/nodes/FieldView.tsx12
-rw-r--r--src/client/views/nodes/FilterBox.tsx95
-rw-r--r--src/client/views/nodes/ImageBox.scss16
-rw-r--r--src/client/views/nodes/ImageBox.tsx98
-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.tsx1
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx1
-rw-r--r--src/client/views/nodes/LinkDocPreview.scss6
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx43
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx26
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx18
-rw-r--r--src/client/views/nodes/PDFBox.tsx163
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.scss26
-rw-r--r--src/client/views/nodes/RecordingBox/ProgressBar.tsx45
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx61
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss207
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx282
-rw-r--r--src/client/views/nodes/RecordingBox/index.ts2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx429
-rw-r--r--src/client/views/nodes/VideoBox.scss159
-rw-r--r--src/client/views/nodes/VideoBox.tsx855
-rw-r--r--src/client/views/nodes/WebBox.scss20
-rw-r--r--src/client/views/nodes/WebBox.tsx293
-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.scss67
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx200
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx3
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx1
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx95
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx225
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx10
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts72
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx36
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts23
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts48
-rw-r--r--src/client/views/nodes/trails/PresBox.scss7
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx414
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss412
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx412
60 files changed, 4496 insertions, 2339 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..c42c2306a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,135 +1,122 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- action,
- computed,
- IReactionDisposer,
- observable,
- reaction,
- runInAction
-} from "mobx";
+import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { DateField } from "../../../fields/DateField";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
+import { Doc, DocListCast } from "../../../fields/Doc";
import { ComputedField } from "../../../fields/ScriptField";
-import { Cast, NumCast } from "../../../fields/Types";
+import { Cast, DateCast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
-import { emptyFunction, formatTime } from "../../../Utils";
+import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } 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 { 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 { Colors } from "../global/globalEnums";
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
import "./AudioBox.scss";
import { FieldView, FieldViewProps } from "./FieldView";
-import { LinkDocPreview } from "./LinkDocPreview";
+
+/**
+ * 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 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;
- });
- @computed get recordingStart() {
- return Cast(
- this.dataDoc[this.props.fieldKey + "-recordingStart"],
- DateField
- )?.date.getTime();
- }
- @computed get duration() {
- return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
- }
- @computed get trimDuration() {
- return this._trimming && this._trimEnd ? this.duration : this._trimEnd - this._trimStart;
- }
- @computed get anchorDocs() {
- return DocListCast(this.dataDoc[this.annotationKey]);
- }
- @computed get links() {
- return DocListCast(this.dataDoc.links);
- }
- @computed get pauseTime() {
- return this._pauseEnd - this._pauseStart;
- } // total time paused to update the correct time
- @computed get heightPercent() {
- return AudioBox.heightPercent;
- }
-
- constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
- super(props);
- AudioBox.Instance = this;
-
- if (this.duration === undefined) {
- runInAction(
- () =>
- (this.Document[this.fieldKey + "-duration"] = this.Document.duration)
- );
+ _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 DateCast(this.dataDoc[this.fieldKey + "-recordingStart"])?.date.getTime(); }
+ @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 mediaState() { return this.dataDoc.mediaState as media_state; }
+ @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;
+ }
+ set mediaState(value) { this.dataDoc.mediaState = value; }
+
+ @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();
+ }
+
+ @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.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) ||
+ this.timeline?.anchorStart(la2) ||
+ this.timeline?.anchorStart(la1) ||
0;
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
@@ -139,154 +126,98 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
getAnchor = () => {
- return (
- CollectionStackedTimeline.createAnchor(
- 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)
- ) || 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
+ return CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.annotationKey,
+ "_timecodeToShow" /* audioStart */,
+ "_timecodeToHide" /* audioEnd */,
+ this._ele?.currentTime ||
+ Cast(this.props.Document._currentTimecode, "number", null) ||
+ (this.mediaState === media_state.Recording
+ ? (Date.now() - (this.recordingStart || 0)) / 1000
+ : undefined)
+ ) || this.rootDoc;
+ }
+
+
+ // 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);
- if (Number.isNaN(this._ele?.duration)) {
+ 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;
}
}
}
@@ -295,49 +226,59 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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),
+ 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.dontAutoPlayFollowedLinks ? "" : "Don't") +
- " play when link is selected",
- event: () =>
- (this.layoutDoc.dontAutoPlayFollowedLinks =
- !this.layoutDoc.dontAutoPlayFollowedLinks),
+ 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.autoPlayAnchors ? "Don't auto play" : "Auto play") +
- " anchors onClick",
- event: () =>
- (this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors),
+ 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" : "Auto") + " play anchors onClick",
+ event: e => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors,
icon: "expand-arrows-alt",
});
ContextMenu.Instance?.addItem({
@@ -347,213 +288,331 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
});
}
- // 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) {
+ Record = (e: React.PointerEvent) => {
+ e.button === 0 && !e.ctrlKey && setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => {
this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
- e.stopPropagation();
- }
+ }), 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"
- );
- this.props.addDocument?.(newDoc);
- e.stopPropagation();
+ setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => {
+ const newDoc = CurrentUserUtils.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(CurrentUserUtils.MyOverlayDocs?.data).includes(this.rootDoc)) {
+ newDoc.x = this.rootDoc.x;
+ newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ }), false);
}
- // 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("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) => {
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)
+
+ 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 = () => {
+ 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);
+ }
+ }));
}
- // hides trim controls and displays new clip
+
+ // for zoom slider, sets timeline waveform zoom
+ zoom = (zoom: number) => {
+ this.timeline?.setZoom(zoom);
+ }
+
+ // for volume slider sets volume
@action
- finishTrim = () => {
- if (this.mediaState === "playing") {
- this.Pause();
+ setVolume = (volume: number) => {
+ if (this._ele) {
+ this._volume = volume;
+ this._ele.volume = volume;
+ if (this._muted) {
+ this.toggleMute();
+ }
}
- 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));
}
+ // toggles audio muted
@action
- setStartTrim = (newStart: number) => {
- this._trimStart = newStart;
+ toggleMute = () => {
+ if (this._ele) {
+ this._muted = !this._muted;
+ this._ele.muted = this._muted;
+ }
}
- @action
- setEndTrim = (newEnd: number) => {
- this._trimEnd = newEnd;
+
+ 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);
+ }
}
- 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);
- }
-
- 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;
+
+ // 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>;
+ }
+
+ // 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>;
+ }
+
+ // 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"}
mediaPath={this.path}
renderDepth={this.props.renderDepth + 1}
startTag={"_timecodeToShow" /* audioStart */}
endTag={"_timecodeToHide" /* audioEnd */}
- focus={DocUtils.DefaultFocus}
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,142 +624,33 @@ 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>
- );
+ return <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..bedc97575 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,21 @@ 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),
+ 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..6d05fca5e 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -22,7 +22,6 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@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 => {
@@ -49,7 +48,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }} >
<SketchPicker
- onChange={c => CurrentUserUtils.SelectedTool === InkTool.None && ColorBox.switchColor(c)}
+ onChange={c => CurrentUserUtils.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']}
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.scss b/src/client/views/nodes/DataViz.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/DataViz.scss
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
new file mode 100644
index 000000000..d9541dba0
--- /dev/null
+++ b/src/client/views/nodes/DataViz.tsx
@@ -0,0 +1,21 @@
+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>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 005133eb0..371d85a32 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -16,14 +16,15 @@ 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 "./DataViz";
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";
@@ -31,16 +32,17 @@ 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 { PresBox } from "./trails/PresBox";
+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");
-import { MapBox } from "./MapBox/MapBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -112,7 +114,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
scaling?: () => number,
- setHeight: (height: number) => void,
+ setHeight?: (height: number) => void,
layoutKey: string,
}> {
@computed get layout(): string {
@@ -142,7 +144,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
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",
@@ -225,10 +226,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
components={{
FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, LabelBox, EquationBox, SliderBox, FieldView,
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
+ PDFBox, VideoBox, AudioBox, RecordingBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox,
- ScreenshotBox,
- HTMLtag, ComparisonBox
+ ScreenshotBox, DataVizBox, HTMLtag, ComparisonBox
}}
bindings={bindings}
jsx={layoutFrame}
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..78d35ab99 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -2,25 +2,21 @@ 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 { Doc, Opt } from "../../../fields/Doc";
+import { 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 { 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 './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
import { TaskCompletionBox } from "./TaskCompletedBox";
import React = require("react");
-import { Transform } from "../../util/Transform";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -46,8 +42,6 @@ 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
onLinkButtonMoved = (e: PointerEvent) => {
if (this.props.InMenu && this.props.StartLink) {
@@ -72,35 +66,10 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
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);
- });
+ DocumentView.showBackLinks(this.props.View.rootDoc);
}
- else DocumentLinksButton.LinkEditorDocView = this.props.View;
- }));
+ }), undefined, undefined,
+ action(() => DocumentLinksButton.LinkEditorDocView = this.props.View));
}
@undoBatch
@@ -132,8 +101,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
DocumentLinksButton.StartLinkView = this.props.View;
}
//action(() => Doc.BrushDoc(this.props.View.Document));
- } else if (!this.props.InMenu) {
- DocumentLinksButton.LinkEditorDocView = this.props.View;
}
}
@@ -191,7 +158,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
} 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);
+ const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink },
+ DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : undefined, undefined, undefined, true);
LinkManager.currentLink = linkDoc;
@@ -275,9 +243,9 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
</div>
:
- <div className="documentLinksButton-menu" ref={this._linkButton}>
+ <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"}
+ <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" />
@@ -286,7 +254,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
}
{
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}>
+ <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>
:
@@ -305,7 +274,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
//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" >
+ <div className="documentLinksButton-wrapper"
+ style={{
+ transform: this.props.InMenu ? undefined :
+ `scale(${(this.props.ContentScaling?.() || 1) * this.props.View.screenToLocalTransform().Scale})`
+ }} >
{
(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) ||
(!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ?
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4565f8504..6a1bfa406 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,8 +4,13 @@
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;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8682a43e6..b80d4579a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -2,7 +2,7 @@ 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 { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, StrListCast, WidthSym } from "../../../fields/Doc";
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
@@ -14,7 +14,7 @@ import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from "../../.
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, returnTrue, returnFalse, returnVal, simulateMouseClick, Utils } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
@@ -37,7 +37,7 @@ import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
import { InkingStroke } from "../InkingStroke";
import { LightboxView } from "../LightboxView";
-import { StyleLayers, StyleProp } from "../StyleProvider";
+import { StyleProp } from "../StyleProvider";
import { CollectionFreeFormDocumentView } from "./CollectionFreeFormDocumentView";
import { DocumentContentsView } from "./DocumentContentsView";
import { DocumentLinksButton } from './DocumentLinksButton';
@@ -49,6 +49,8 @@ import { RadialMenu } from './RadialMenu';
import { ScriptingBox } from "./ScriptingBox";
import { PresBox } from './trails/PresBox';
import React = require("react");
+import { DocServer } from "../../DocServer";
+import { FieldViewProps } from "./FieldView";
const { Howl } = require('howler');
interface Window {
@@ -79,10 +81,11 @@ 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.
+ 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
@@ -102,20 +105,28 @@ export interface DocComponentView {
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;
@@ -140,7 +151,7 @@ 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;
@@ -148,12 +159,16 @@ export interface DocumentViewSharedProps {
originalBackgroundColor?: string;
isNoteTakingView?: 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
+ 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
@@ -161,6 +176,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
radialMenu?: String[];
LayoutTemplateString?: string;
dontCenter?: "x" | "y" | "xy";
+ dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling
ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal
NativeWidth?: () => number;
NativeHeight?: () => number;
@@ -170,8 +186,11 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
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 +203,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;
@@ -209,7 +229,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@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 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); }
@@ -223,7 +243,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@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 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); }
@@ -239,6 +259,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?.();
@@ -308,7 +329,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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();
+ if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) e.stopPropagation();
this.removeMoveListeners();
this.addMoveListeners();
this.removeEndListeners();
@@ -322,7 +343,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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(CurrentUserUtils.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)) {
@@ -420,6 +441,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
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);
+ 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;
@@ -467,7 +490,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
});
// 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 focusSpeed = this._componentView?.scrollFocus?.(anchor, !options?.instant || !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;
this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
...options, afterFocus: (didFocus: boolean) =>
@@ -480,10 +503,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
(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);
+ 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
@@ -499,12 +524,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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?.())
+ 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,
@@ -517,9 +542,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}, console.log).result?.select === true ? this.props.select(false) : "";
const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click");
if (this.onDoubleClickHandler) {
+ runInAction(() => this._pendingDoubleClick = true);
this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350);
} else clickFunc();
- } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+ } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
this.allLinks.length && LinkManager.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
@@ -537,8 +563,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
});
onPointerDown = (e: React.PointerEvent): void => {
+ if (this.rootDoc.type === DocumentType.INK && CurrentUserUtils.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(CurrentUserUtils.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
@@ -552,10 +579,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// 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) &&
+ !this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
!e.ctrlKey &&
(e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
- !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ !DocListCast(CurrentUserUtils.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();
@@ -571,9 +599,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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(CurrentUserUtils.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(CurrentUserUtils.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);
@@ -605,12 +633,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
@undoBatch @action
- toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => {
+ 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;
@@ -665,7 +697,7 @@ 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]);
}
}
}
@@ -676,7 +708,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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");
+ DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to:portal from");
}
this.Document.followLinkLocation = "inPlace";
this.Document.followLinkZoom = true;
@@ -685,7 +717,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@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);
@@ -735,13 +767,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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({
+ !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._yMargin = 10;
alias._height = 35;
alias._width = 100;
alias.syncLayoutFieldWithTitle = true;
@@ -753,8 +784,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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" });
+ 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 : [];
@@ -765,11 +796,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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...");
@@ -792,7 +823,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
const funcs: ContextMenuProps[] = [];
- if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) {
+ 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 });
@@ -802,8 +833,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const more = cm.findByDescription("More...");
const moreItems = more && "subitems" in more ? more.subitems : [];
if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" });
- if (!Doc.UserDoc().noviceMode) {
+ (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" });
@@ -819,13 +850,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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" });
}
+ !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 : [];
- !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" });
+ 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" });
}
@@ -844,15 +876,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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;
- }
- }
+ 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 ||
+ CurrentUserUtils.ActiveTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.rootSelected() ||
this.props.Document.forceActive ||
@@ -861,8 +889,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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;
+ }
@computed get contents() {
TraceMobx();
const audioView = !this.layoutDoc._showAudio ? (null) :
@@ -871,13 +903,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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"),
+ 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: "absolute" }} src={this.thumb} // + '?d=' + (new Date()).getTime()}
+ <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);
@@ -887,10 +923,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
{...this.props}
docViewPath={this.props.viewPath}
thumbShown={this.thumbShown}
+ isHovering={this.isHovering}
setContentView={this.setContentView}
scaling={this.contentScaling}
PanelHeight={this.panelHeight}
- setHeight={this.setHeight}
+ setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
isContentActive={this.isContentActive}
ScreenToLocalTransform={this.screenToLocal}
rootSelected={this.rootSelected}
@@ -898,10 +935,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
focus={this.focus}
layoutKey={this.finalLayoutKey} />
{this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
- {this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) :
+ {(!this.props.isSelected() && !this._isHovering) || 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]} />
+ Offset={[this.topMost ? 0 : !this.props.isSelected() ? - 15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? - 15 : -30]} />
}
{audioView}
</div>;
@@ -943,11 +980,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@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) =>
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView {...this.props}
+ isContentActive={returnFalse}
Document={link}
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
@@ -1041,7 +1079,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
/>
</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 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",
@@ -1079,24 +1118,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
{captionView}
</div>;
}
+ 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 isButton = this.props.Document.type === DocumentType.FONTICON;
- if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
+ 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),
- 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"}`,
+ transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`,
}}>
{this.innards}
@@ -1106,9 +1148,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
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
@@ -1119,14 +1161,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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}
+ return <div className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont}
onContextMenu={this.onContextMenu}
onKeyDown={this.onKeyDown}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
- onPointerEnter={e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document)}
- onPointerLeave={e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document)}
+ 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",
@@ -1164,8 +1207,24 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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;
+ 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; }
@@ -1181,13 +1240,17 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@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));
+ 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));
+ 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 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() {
@@ -1201,20 +1264,21 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@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 Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? 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.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
+ @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());
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();
@@ -1226,15 +1290,17 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
}
- public iconify() {
+ public iconify(finished?: () => void) {
+ this.ComponentView?.updateIcon?.();
const layoutKey = Cast(this.Document.layoutKey, "string", null);
if (layoutKey !== "layout_icon") {
- this.switchViews(true, "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);
+ this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
this.Document.deiconifyLayout = undefined;
+ this.props.bringToFront(this.rootDoc);
}
}
@undoBatch
@@ -1243,13 +1309,16 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Doc.setNativeView(this.props.Document);
custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
}
- switchViews = action((custom: boolean, view: string) => {
+ switchViews = action((custom: boolean, view: string, finished?: () => void) => {
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);
+ 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);
@@ -1263,10 +1332,12 @@ export class DocumentView extends React.Component<DocumentViewProps> {
PanelHeight = () => this.panelHeight;
ContentScale = () => 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,14 +1356,16 @@ 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={{
+ transition: this.props.dataTransition,
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()}%`,
+ 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()}%`),
}}>
@@ -1308,14 +1381,42 @@ export class DocumentView extends React.Component<DocumentViewProps> {
ContentScaling={this.ContentScale}
ScreenToLocalTransform={this.screenToLocalTransform}
focus={this.props.focus || emptyFunction}
- bringToFront={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);
+});
+
+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;
}); \ No newline at end of file
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 6c9d53d10..c6a497831 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -1,3 +1,5 @@
+@import "../global/globalCssVariables.scss";
+
.equationBox-cont {
transform-origin: top left;
} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 943b9f153..67cf18d8b 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -2,10 +2,12 @@ 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 { Doc, Field, FieldResult, Opt } from "../../../fields/Doc";
import { List } from "../../../fields/List";
import { WebField } from "../../../fields/URLField";
-import { DocumentViewSharedProps } from "./DocumentView";
+import { DocumentView, DocumentViewSharedProps } from "./DocumentView";
+import { ScriptField } from "../../../fields/ScriptField";
+import { RecordingBox } from "./RecordingBox";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -22,10 +24,12 @@ export interface FieldViewProps extends DocumentViewSharedProps {
isDocumentActive?: () => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
scaling?: () => number;
- setHeight: (height: number) => void;
+ setHeight?: (height: number) => void;
+ 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;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index ba65acee0..17b57cb3b 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -8,7 +8,7 @@ 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 { Cast, NumCast, StrCast, DocCast } from "../../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
@@ -35,7 +35,7 @@ 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 = CurrentUserUtils.createFilterDoc();
}
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterBox, fieldKey); }
@@ -84,7 +84,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
return "data";
}
@computed static get targetDocChildren() {
- return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || CurrentUserUtils.ActiveDashboard.data);
+ return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || CurrentUserUtils.ActiveDashboard?.data);
}
@observable _loaded = false;
@@ -96,17 +96,15 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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() {
@@ -117,7 +115,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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();
}
@@ -169,23 +167,25 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
+ }
}
}
}
@@ -196,6 +196,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
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);
@@ -208,7 +209,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
@@ -218,8 +219,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
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
+ 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")`;
@@ -240,6 +242,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.GetProto(newFacet)[newFacetField + "-maxThumb"] = extendedMaxVal;
const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`;
newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" });
+ newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
} else {
newFacet = new Doc();
newFacet.system = true;
@@ -267,7 +270,7 @@ 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);
}
/**
@@ -322,7 +325,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
* 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;
}
@@ -373,7 +376,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
<div className="filterBox-select-bool">
- <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc.currentFilter as Doc)?.filterBoolean)}>
+ <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>
@@ -386,6 +389,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
options={options}
isMulti={false}
onChange={val => this.facetClick((val as UserOptions).value)}
+ onKeyDown={e => e.stopPropagation()}
value={null}
closeMenuOnSelect={true}
/>
@@ -402,6 +406,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
docFilters={returnEmptyFilter}
docRangeFilters={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
+ childDocumentsActive={returnTrue}
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
ContainingCollectionView={this.props.ContainingCollectionView}
PanelWidth={this.props.PanelWidth}
@@ -420,10 +425,10 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
whenChildContentsActiveChanged={returnFalse}
treeViewHideTitle={true}
focus={returnFalse}
+ onCheckedClick={this.scriptField}
treeViewHideHeaderFields={false}
dontRegisterView={true}
styleProvider={this.FilterStyleProvider}
- layerProvider={this.props.layerProvider}
docViewPath={this.props.docViewPath}
scriptContext={this.props.scriptContext}
moveDocument={returnFalse}
@@ -480,6 +485,32 @@ ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader:
}
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) * .1));
+ const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * .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]);
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..60eb48114 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 { 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,6 +14,8 @@ 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 { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DragManager } from '../../util/DragManager';
@@ -45,9 +47,9 @@ const uploadIcons = {
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();
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
@observable _curSuffix = "";
@observable _uploadIcon = uploadIcons.idle;
@@ -61,7 +63,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
getAnchor = () => {
- const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations);
+ const anchor = this._getAnchor?.(this._savedAnnotations);
anchor && this.addDocument(anchor);
return anchor ?? this.rootDoc;
}
@@ -71,14 +73,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._disposers.sizer = reaction(() => (
{
forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
- scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0],
+ 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",
+ ({ 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) {
+ if (true || !this.layoutDoc._height) {
this.layoutDoc._height = width * nativeSize.nativeHeight / nativeSize.nativeWidth;
}
},
@@ -128,6 +130,48 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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.scaling?.() || 1);
+ cropping._height = anchh * (this.props.scaling?.() || 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) {
@@ -135,7 +179,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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) {
+ 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...");
@@ -246,8 +290,8 @@ 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 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 };
}
@@ -282,17 +326,18 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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}
+ <img key="paths"
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>}
+ {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()}
@@ -300,17 +345,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</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,7 +357,7 @@ 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)) {
+ 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(CurrentUserUtils.ActiveTool)) {
setupMoveUpEvents(this, e, action(e => {
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
@@ -332,9 +367,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
@action
finishMarquee = () => {
+ this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this._marqueeing = undefined;
this.props.select(false);
}
+ savedAnnotations = () => this._savedAnnotations;
render() {
TraceMobx();
@@ -344,7 +381,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
style={{
width: this.props.PanelWidth() ? undefined : `100%`,
height: this.props.PanelWidth() ? undefined : `100%`,
- pointerEvents: this.props.layerProvider?.(this.layoutDoc) === false ? "none" : undefined,
+ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined,
borderRadius
}} >
<CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
@@ -374,9 +411,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
docView={this.props.docViewPath().slice(-1)[0]}
addDocument={this.addDocument}
finishMarquee={this.finishMarquee}
- savedAnnotations={this._savedAnnotations}
+ 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..7fd289a97 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -89,7 +89,6 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
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");
}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index a9d33f161..ccac66996 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -54,6 +54,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
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)}>
diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss
index 06ae466f0..3febbcecb 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,14 @@
border-bottom: 8px solid white;
border-right: 8px solid white;
z-index: 2004;
+
.linkDocPreview-inner {
background-color: white;
width: 100%;
height: 100%;
pointer-events: none;
+ display: flex;
+ flex-direction: column;
.linkDocPreview-info {
height: 37px;
@@ -21,6 +24,7 @@
.linkDocPreview-buttonBar {
float: right;
}
+
.linkDocPreview-title {
padding-right: 4px;
float: left;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 2e29c0656..6c7e174f7 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -15,6 +15,7 @@ import { DocumentView, DocumentViewSharedProps } from "./DocumentView";
import './LinkDocPreview.scss';
import React = require("react");
import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -46,7 +47,7 @@ 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) {
+ 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;
@@ -83,10 +84,17 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
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._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 = "";
}
}));
@@ -99,7 +107,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
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) => {
@@ -107,7 +121,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
LinkDocPreview.Clear();
LinkManager.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");
}
}
width = () => {
@@ -127,7 +141,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@computed get previewHeader() {
return !this._linkDoc || !this._targetDoc || !this._linkSrc ? (null) :
<div className="linkDocPreview-info" ref={this._infoRef}>
- <div className="linkDocPreview-title">
+ <div className="linkDocPreview-title" style={{ pointerEvents: "all" }}
+ onPointerDown={e => {
+ DragManager.StartDocumentDrag([this._infoRef.current!],
+ new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY);
+ e.stopPropagation();
+ LinkDocPreview.Clear();
+ }}>
{StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + "..." : this._targetDoc.title}
<p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
</div>
@@ -152,17 +172,16 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
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">
+ <div className="linkDocPreview-preview-wrapper" style={{ maxHeight: this._toolTipText ? "100%" : undefined, overflow: "auto" }}>
{this._toolTipText ? this._toolTipText :
<DocumentView ref={(r) => {
- const targetanchor = LinkManager.getOppositeAnchor(this._linkDoc!, this._linkSrc!);
+ 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}
@@ -178,10 +197,12 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
ContainingCollectionDoc={undefined}
ContainingCollectionView={undefined}
renderDepth={-1}
+ suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
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}
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..0b7264d79 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -259,7 +259,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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
@@ -274,7 +274,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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();
@@ -284,7 +284,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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();
}
@@ -471,7 +471,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
onMarqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool)) {
setupMoveUpEvents(this, e, action(e => {
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
@@ -489,13 +489,17 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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} />)}
+ fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />)}
</div>;
}
@@ -537,7 +541,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
infoWidth = () => this.props.PanelWidth() / 5;
infoHeight = () => this.props.PanelHeight() / 5;
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
-
+ savedAnnotations = () => this._savedAnnotations;
render() {
const renderAnnotations = (docFilters?: () => string[]) => (null);
// bcz: commmented this out. Otherwise, any documents that are rendered with an InfoWindow of a marker
@@ -566,7 +570,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// moveDocument={this.moveDocument}
// addDocument={this.sidebarAddDocument}
// childPointerEvents={true}
- // pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ // pointerEvents={CurrentUserUtils.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
return <div className="mapBox" ref={this._ref}>
{/*console.log(apiKey)*/}
{/* <LoadScript
@@ -594,7 +598,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<Autocomplete
onLoad={this.setSearchBox}
onPlaceChanged={this.handlePlaceChanged}>
- <input ref={this.inputRef} className="searchbox" type="text" placeholder="Search anywhere:" />
+ <input className="mapBox-input" ref={this.inputRef} type="text" placeholder="Enter location" />
</Autocomplete>
{this.renderMarkers()}
@@ -618,7 +622,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
addDocument={this.addDocumentWrapper}
docView={this.props.docViewPath().lastElement()}
finishMarquee={this.finishMarquee}
- savedAnnotations={this._savedAnnotations}
+ savedAnnotations={this.savedAnnotations}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current} />}
</div>
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 5e5f6cd74..7e4d23912 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, 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 { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -46,11 +46,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
_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">
@@ -74,13 +72,13 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
rootSelected={returnFalse}
childHideResizeHandles={returnTrue}
childHideDecorationTitle={returnTrue}
- childFitWidth={doc => doc.type === DocumentType.RTF}
+ childFitWidth={this.childFitWidth}
// 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)}
+ removeDocument={this.removeDoc}
+ addDocument={this.addDoc}
renderDepth={this.props.renderDepth + 1}
viewType={CollectionViewType.Stacking}
- pointerEvents="all"
+ pointerEvents={returnAll}
/>
</div>
<hr />
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 9807cee7c..4eb07fd8d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,24 +3,30 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
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 { 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 { 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 { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
import { Colors } from '../global/globalEnums';
+import { CreateImage } from "../nodes/WebBoxRenderer";
import { AnchorMenu } from '../pdf/AnchorMenu';
import { PDFViewer } from "../pdf/PDFViewer";
import { SidebarAnnos } from '../SidebarAnnos';
import { FieldView, FieldViewProps } from './FieldView';
+import { ImageBox } from './ImageBox';
import "./PDFBox.scss";
+import { VideoBox } from './VideoBox';
import React = require("react");
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -52,6 +58,116 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
+ 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.scaling?.() || 1);
+ const anchh = cropping[HeightSym]() * (this.props.scaling?.() || 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);
@@ -72,7 +188,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
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)),
y: NumCast(this.layoutDoc._scrollTop),
@@ -104,8 +220,14 @@ 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 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
@@ -137,6 +259,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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
+ const batch = UndoManager.StartBatch("sidebar");
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"]);
@@ -148,7 +271,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
- }, emptyFunction, () => this.toggleSidebar());
+ }, () => batch.end(), () => this.toggleSidebar());
}
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
@@ -181,13 +304,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</>;
const searchTitle = `${!this._searching ? "Open" : "Close"} Search Bar`;
const curPage = NumCast(this.Document._curPage) || 1;
- return !this.props.isContentActive() ? (null) :
+ 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)} />
+ 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>
@@ -209,6 +335,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<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)}
+ onKeyDown={e => e.stopPropagation()}
onClick={action(() => this._pageControls = !this._pageControls)} />
{this._pageControls ? pageBtns : (null)}
</div>
@@ -224,12 +351,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</div>;
}
sidebarWidth = () => !this.SidebarShown ? 0 :
- this._previewWidth ? PDFBox.openSidebarWidth :
- (NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth() / NumCast(this.layoutDoc.nativeWidth)
+ 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: "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" });
}
@@ -261,12 +389,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}}>
<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)`,
+ 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
+ top: 0,
}}>
<PDFViewer {...this.props}
rootDoc={this.rootDoc}
@@ -282,7 +410,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- startupLive={true}
+ crop={this.crop}
ContentScaling={returnOne}
/>
</div>
@@ -324,4 +452,5 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
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..a493b0b89
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.scss
@@ -0,0 +1,26 @@
+
+.progressbar {
+ position: absolute;
+ display: flex;
+ justify-content: flex-start;
+ bottom: 10px;
+ width: 80%;
+ height: 5px;
+ 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;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/RecordingBox/ProgressBar.tsx b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
new file mode 100644
index 000000000..82d5e1f04
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/ProgressBar.tsx
@@ -0,0 +1,45 @@
+import * as React from 'react';
+import { useEffect } from "react"
+import "./ProgressBar.scss"
+
+interface ProgressBarProps {
+ progress: number,
+ marks: number[],
+}
+
+export function ProgressBar(props: ProgressBarProps) {
+
+ // const handleClick = (e: React.MouseEvent) => {
+ // let progressbar = document.getElementById('progressbar')!
+ // let bounds = progressbar!.getBoundingClientRect();
+ // let x = e.clientX - bounds.left;
+ // let percent = x / progressbar.clientWidth * 100
+
+ // for (let i = 0; i < props.marks.length; i++) {
+ // let start = i == 0 ? 0 : props.marks[i-1];
+ // if (percent > start && percent < props.marks[i]) {
+ // props.playSegment(i)
+ // // console.log(i)
+ // // console.log(percent)
+ // // console.log(props.marks[i])
+ // break
+ // }
+ // }
+ // }
+
+ return (
+ <div className="progressbar" id="progressbar">
+ <div
+ className="progressbar done"
+ style={{ width: `${props.progress}%` }}
+ // onClick={handleClick}
+ ></div>
+ {props.marks.map((mark) => {
+ return <div
+ className="progressbar mark"
+ style={{ width: `${mark}%` }}
+ ></div>
+ })}
+ </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..10393624b
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -0,0 +1,61 @@
+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 { RecordingApi } from "../../../util/RecordingApi";
+import { Doc, FieldsSym } 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() {
+ console.log("set native width and height")
+ Doc.SetNativeWidth(this.dataDoc, 1280);
+ Doc.SetNativeHeight(this.dataDoc, 720);
+ }
+
+ @observable result: Upload.FileInformation | undefined = undefined
+ @observable videoDuration: number | undefined = undefined
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration
+ }
+
+ @action
+ setResult = (info: Upload.FileInformation, trackScreen: boolean) => {
+ 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.agnostic.client);
+ this.dataDoc[this.fieldKey + "-recorded"] = true;
+ // stringify the presenation and store it
+ if (trackScreen) {
+ this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear());
+ }
+ }
+
+ render() {
+ return <div className="recordingBox" ref={this._ref}>
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={Doc.GetProto(this.rootDoc)[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..9b2f6d070
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -0,0 +1,207 @@
+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: grey;
+}
+
+.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: 30px;
+ 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: 30px;
+ // 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;
+ justify-content: center;
+ width: 100%;
+
+}
+
+.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-items: center;
+ position: absolute;
+ 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;
+ }
+
+ }
+} \ 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..b95335792
--- /dev/null
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -0,0 +1,282 @@
+import * as React from 'react';
+import "./RecordingView.scss";
+import { ReactElement, useCallback, 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 { RecordingApi } from '../../../util/RecordingApi';
+import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+
+interface MediaSegment {
+ videoChunks: any[],
+ endTime: number
+}
+
+interface IRecordingViewProps {
+ setResult: (info: Upload.FileInformation, trackScreen: boolean) => 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);
+
+ const [videos, setVideos] = useState<MediaSegment[]>([]);
+ const videoRecorder = useRef<MediaRecorder | null>(null);
+ const videoElementRef = useRef<HTMLVideoElement | null>(null);
+
+ const [finished, setFinished] = useState<boolean>(false)
+ const [trackScreen, setTrackScreen] = useState<boolean>(true)
+
+
+
+ const DEFAULT_MEDIA_CONSTRAINTS = {
+ video: {
+ width: 1280,
+ height: 720,
+ },
+ audio: {
+ echoCancellation: true,
+ noiseSuppression: true,
+ sampleRate: 44100
+ }
+ }
+
+ useEffect(() => {
+
+ if (finished) {
+ props.setDuration(recordingTimer * 100)
+ let allVideoChunks: any = []
+ videos.forEach((vid) => {
+ console.log(vid.videoChunks)
+ allVideoChunks = allVideoChunks.concat(vid.videoChunks)
+ })
+
+ const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
+
+ Networking.UploadFilesToServer(videoFile)
+ .then((data) => {
+ const result = data[0].result
+ if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+ props.setResult(result, trackScreen)
+ } else {
+ alert("video conversion failed");
+ }
+ })
+
+ }
+
+
+ }, [finished])
+
+ useEffect(() => {
+ // check if the browser supports media devices on first load
+ if (!navigator.mediaDevices) {
+ console.log('This browser does not support getUserMedia.')
+ }
+ console.log('This device has the correct media devices.')
+ }, [])
+
+ useEffect(() => {
+ // get access to the video element on every render
+ videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement;
+ })
+
+ 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 () => {
+ const stream = await startShowingStream();
+ videoRecorder.current = new MediaRecorder(stream)
+
+ // 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);
+ trackScreen && RecordingApi.Instance.start();
+ }
+
+ videoRecorder.current.onstop = () => {
+ // if we have a last portion
+ if (videoChunks.length > 1) {
+ // append the current portion to the video pieces
+ setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+ }
+
+ // reset the temporary chunks
+ videoChunks = []
+ setRecording(false);
+ setFinished(true);
+ trackScreen && RecordingApi.Instance.pause();
+ }
+
+ // recording paused
+ videoRecorder.current.onpause = (event: any) => {
+ // append the current portion to the video pieces
+ setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
+
+ // reset the temporary chunks
+ videoChunks = []
+ setRecording(false);
+ trackScreen && RecordingApi.Instance.pause();
+ }
+
+ videoRecorder.current.onresume = async (event: any) => {
+ await startShowingStream();
+ setRecording(true);
+ trackScreen && RecordingApi.Instance.resume();
+ }
+
+ videoRecorder.current.start(200)
+ }
+
+
+ const stop = () => {
+ if (videoRecorder.current) {
+ if (videoRecorder.current.state !== "inactive") {
+ videoRecorder.current.stop();
+ // recorder.current.stream.getTracks().forEach((track: any) => track.stop())
+ }
+ }
+ }
+
+ const pause = () => {
+ if (videoRecorder.current) {
+ if (videoRecorder.current.state === "recording") {
+ videoRecorder.current.pause();
+ }
+ }
+ }
+
+ const startOrResume = (e: React.PointerEvent) => {
+ // the code to start or resume does not get triggered if we start dragging the button
+ setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
+ if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+ record();
+ } else if (videoRecorder.current.state === "paused") {
+ videoRecorder.current.resume();
+ }
+ return true; // cancels propagation to documentView to avoid selecting it.
+ }, false, false);
+ }
+
+ const clearPrevious = () => {
+ const numVideos = videos.length
+ setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+ setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime)
+ setVideos(videos.filter((_, idx) => idx !== numVideos - 1));
+ }
+
+ const handleOnTimeUpdate = () => {
+ if (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}
+ />
+ <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={startOrResume} />
+ }
+ </div>
+
+ {!recording && (videos.length > 0 ?
+
+ <div className="options-wrapper video-edit-wrapper">
+ {/* <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons" }}>
+ <MdBackspace onClick={clearPrevious} />
+ </IconContext.Provider> */}
+ <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
+ <FaCheckCircle onClick={stop} />
+ </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>
+
+ <ProgressBar
+ progress={progress}
+ marks={videos.map((elt) => elt.endTime / MAXTIME * 100)}
+ // playSegment={playSegment}
+ />
+ </div>
+ </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..0c7d7dc31 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -26,7 +26,7 @@ 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,225 +107,226 @@ declare class MediaRecorder {
@observer
export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- 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(); }
+ 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(); }
- constructor(props: any) {
- super(props);
- if (this.rootDoc.videoWall) {
- this.rootDoc.nativeWidth = undefined;
- this.rootDoc.nativeHeight = undefined;
- this.layoutDoc.popOff = 0;
- this.layoutDoc.popOut = 1;
- } else {
- this.setupDictation();
- }
- }
- 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;
- }
+ constructor(props: any) {
+ super(props);
+ if (this.rootDoc.videoWall) {
+ this.rootDoc.nativeWidth = undefined;
+ this.rootDoc.nativeHeight = undefined;
+ this.layoutDoc.popOff = 0;
+ this.layoutDoc.popOut = 1;
+ } else {
+ this.setupDictation();
+ }
+ }
+ 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;
+ }
- videoLoad = () => {
- const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
- this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
- }
- }
+ videoLoad = () => {
+ const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
+ const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+ const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+ if (!nativeWidth || !nativeHeight) {
+ if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
+ Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
+ this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
+ }
+ }
- componentDidMount() {
- this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
- this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
- // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
- // ({ width, height }) => {
- // if (this._camera) {
- // const angle = -Math.abs(1 - width / height);
- // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
- // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
- // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
- // (this._camera as any).updateProjectionMatrix();
- // }
- // });
- }
- componentWillUnmount() {
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
- }
+ componentDidMount() {
+ this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
+ this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+ // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
+ // ({ width, height }) => {
+ // if (this._camera) {
+ // const angle = -Math.abs(1 - width / height);
+ // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
+ // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
+ // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+ // (this._camera as any).updateProjectionMatrix();
+ // }
+ // });
+ }
+ componentWillUnmount() {
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ 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" });
- }
+ 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" });
+ }
- @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>;
- }
+ @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>;
+ }
- // _numScreens = 5;
- // _camera: Camera | undefined;
- // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
- // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
- @computed get threed() {
- // if (this.rootDoc.videoWall) {
- // const screens: any[] = [];
- // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
- // let count = 0;
- // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
- // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
- // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
- // this._camera = props.camera;
- // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
- // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
- // }}>
- // {/* <ambientLight />*/}
- // <pointLight position={[10, 10, 10]} intensity={1} />
- // {screens}
- // </ Canvas>;
- // }
- return (null);
- }
- toggleRecording = async () => {
- if (!this._screenCapture) {
- this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
- const aud_chunks: any = [];
- this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
- 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._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.ondataavailable = (e: any) => vid_chunks.push(e.data);
- this._videoRec.onstop = async (e: any) => {
- 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.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");
- };
- this._audioRec.start();
- this._videoRec.start();
- runInAction(() => {
- this._screenCapture = true;
- this.dataDoc.mediaState = "recording";
- });
- DocUtils.ActiveRecordings.push(this);
- } else {
- this._audioRec?.stop();
- this._videoRec?.stop();
- runInAction(() => {
- this._screenCapture = false;
- this.dataDoc.mediaState = "paused";
- });
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ // _numScreens = 5;
+ // _camera: Camera | undefined;
+ // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
+ // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
+ @computed get threed() {
+ // if (this.rootDoc.videoWall) {
+ // const screens: any[] = [];
+ // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
+ // let count = 0;
+ // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
+ // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
+ // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
+ // this._camera = props.camera;
+ // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
+ // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+ // }}>
+ // {/* <ambientLight />*/}
+ // <pointLight position={[10, 10, 10]} intensity={1} />
+ // {screens}
+ // </ Canvas>;
+ // }
+ return (null);
+ }
+ toggleRecording = async () => {
+ if (!this._screenCapture) {
+ this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
+ const aud_chunks: any = [];
+ this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
+ 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._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.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.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");
+ };
+ this._audioRec.start();
+ this._videoRec.start();
+ runInAction(() => {
+ this._screenCapture = true;
+ this.dataDoc.mediaState = "recording";
+ });
+ DocUtils.ActiveRecordings.push(this);
+ } else {
+ this._audioRec?.stop();
+ this._videoRec?.stop();
+ runInAction(() => {
+ this._screenCapture = false;
+ this.dataDoc.mediaState = "paused";
+ });
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
- CaptureManager.Instance.open(this.rootDoc);
- }
- }
+ 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));
- 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;
- }
- 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();
- 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}
- isAnnotationOverlay={true}
- select={emptyFunction}
- isContentActive={emptyFunction}
- scaling={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 >;
- }
+ 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));
+ 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;
+ }
+ 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();
+ 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}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ isContentActive={emptyFunction}
+ scaling={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 >;
+ }
} \ 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..1b891034f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,24 +1,26 @@
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 * as rp from 'request-promise';
-import { Doc, DocListCast } from "../../../fields/Doc";
+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, VideoField } from "../../../fields/URLField";
+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 { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DocumentManager } from "../../util/DocumentManager";
+import { RecordingApi } from "../../util/RecordingApi";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
+import { undoBatch } from "../../util/UndoManager";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionStackedTimeline } from "../collections/CollectionStackedTimeline";
+import { CollectionStackedTimeline, TrimScope } from "../collections/CollectionStackedTimeline";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from "../DocComponent";
@@ -27,76 +29,199 @@ import { MarqueeAnnotator } from "../MarqueeAnnotator";
import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
-import { LinkDocPreview } from "./LinkDocPreview";
+import { RecordingBox } from "./RecordingBox/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); }
+ /**
+ * 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;
+ @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;
+
@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"]); }
+ @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;
+
- private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; }
+ @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("/")) : "";
+ }
+
+
+ // 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 ?? "";
+ }
+
+ // 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; }
- constructor(props: Readonly<ViewBoxAnnotatableProps & FieldViewProps>) {
- super(props);
- VideoBox.Instance = this;
+
+ 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);
}
- 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;
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this.Pause();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ document.removeEventListener("keydown", this.keyEvents, true);
}
- 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;
+ // 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;
+
+ // if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
+ // console.log('VideoBox : Play : presentation mode', this._playing);
+ // return;
+ // }
+
+ // if presentation isn't null, call followmovements on the recording api
+ if (this.presentation) {
+ // console.log("presentation isn't null")
+ const err = RecordingApi.Instance.playMovements(this.presentation, this.player?.currentTime || 0, this);
+ err && console.log(err)
+ } else {
+ // console.log("presentation is null")
+ }
+
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);
@@ -105,10 +230,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
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) => {
+ if (this.presentation) {
+ console.log('VideoBox : Pause');
+ const err = RecordingApi.Instance.pauseMovements();
+ err && console.log(err);
+ }
+
this._playing = false;
+ this.removeCurrentlyPlaying();
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
@@ -120,12 +257,23 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
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");
} catch (e) {
@@ -133,16 +281,46 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
- @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
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);
}
@@ -168,15 +346,25 @@ 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 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));
+ returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
}
}
- private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ 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);
+ }
+
+ // 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);
@@ -194,6 +382,27 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
(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);
@@ -204,72 +413,79 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
- 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 = () => {
+ const video = document.createElement('video');
+ const thumbnailPromises: Promise<any>[] = [];
+
+ video.onloadedmetadata = () => {
+ video.currentTime = 0;
+ };
+
+ video.onseeked = () => {
+ const canvas = document.createElement('canvas');
+ canvas.height = video.videoHeight;
+ canvas.width = video.videoWidth;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
+ const imgUrl = canvas.toDataURL();
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, "");
+ const encodedFilename = encodeURIComponent("thumbnail" + retitled + "_" + video.currentTime.toString().replace(/\./, "_"));
+ const filename = basename(encodedFilename);
+ thumbnailPromises.push(VideoBox.convertDataUri(imgUrl, filename));
+ 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.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) {
@@ -283,37 +499,55 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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: "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" });
+ // 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;
+
+ // 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 interactive = CurrentUserUtils.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" }}>
- <div className={classname}>
- <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ <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>
@@ -326,10 +560,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</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("/")) : "";
- }
@action youtubeIframeLoaded = (e: any) => {
if (!this._youtubeContentCreated) {
@@ -340,7 +570,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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) {
@@ -357,7 +588,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._disposers.youtubeReactionDisposer?.();
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,
+ () => CurrentUserUtils.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);
@@ -372,48 +603,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
});
}
}
- 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>;
- }
+
+ // 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);
@@ -421,14 +623,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}, 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) => {
+ 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,
@@ -436,19 +642,30 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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;
- },
- emptyFunction,
- (e: PointerEvent) => this.layoutDoc._currentTimecode = 0);
+
+ // 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;
@@ -460,6 +677,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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;
@@ -468,52 +687,278 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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;
+ }
+ else if (this.player) {
+ // 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;
+
+ // 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.ActiveTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
+
+ // 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());
- setAnchorTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
+ setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
+ playing = () => this._playing;
+
+ contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+
+ scaling = () => this.props.scaling?.() || 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];
+
+ 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={this._stackedTimeline} {...this.props}
+ <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + "-dictation"}
mediaPath={this.audiopath}
@@ -522,65 +967,36 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
endTag={"_timecodeToHide" /* videoEnd */}
bringToFront={emptyFunction}
CollectionView={undefined}
- duration={this.duration}
playFrom={this.playFrom}
- setTime={this.setAnchorTime}
+ 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}
- trimming={false}
- trimStart={0}
- trimEnd={this.duration}
- trimDuration={this.duration}
- setStartTrim={emptyFunction}
- setEndTrim={emptyFunction}
+ 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} />;
}
- 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);
- }
- }
-
- finishMarquee = action(() => {
- this._marqueeing = undefined;
- this.props.select(true);
- });
-
- @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();
- 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];
- timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+ 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,
+ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined,
borderRadius,
overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
}} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
@@ -622,12 +1038,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
containerOffset={this.marqueeOffset}
addDocument={this.addDocWithTimecode}
finishMarquee={this.finishMarquee}
- savedAnnotations={this._savedAnnotations}
+ savedAnnotations={this.savedAnnotations}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
/>}
{this.renderTimeline}
- {this.uIButtons}
</div>
</div >);
}
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..d14af49ea 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -15,7 +15,6 @@ 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";
@@ -41,7 +40,6 @@ 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); }
@@ -53,7 +51,8 @@ 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 = "";
@@ -69,19 +68,22 @@ 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);
+ @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 this._scrollHeight; }
+ @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 ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.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.
}
@@ -103,63 +105,79 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
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;
+ }
+ }
+
+ lockout = false;
+ 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.lockout && 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.lockout = 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.lockout = 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";
+ 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"`;
+ CurrentUserUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
});
- reaction(() => this.props.isSelected(),
+ 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
+ } 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.
- 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);
- });
- }
+ this.updateThumb();
}
- });
+ }, { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) });
this._disposers.autoHeight = reaction(() => this.layoutDoc._autoHeight,
autoHeight => {
if (autoHeight) {
this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]);
- this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
}
});
@@ -181,31 +199,26 @@ 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);
- }
+ 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);
@@ -226,6 +239,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// clear selection
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
@@ -238,21 +252,22 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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 scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .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),
y: NumCast(this.layoutDoc._scrollTop),
@@ -262,15 +277,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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 sel = this._iframe.contentWindow.getSelection();
if (sel) {
- this.createTextAnnotation(sel, sel.getRangeAt(0));
+ 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);
}
@@ -278,6 +297,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
@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 word = getWordAtPoint(e.target, e.clientX, e.clientY);
@@ -315,6 +335,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
+ 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) {
@@ -333,8 +356,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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);
+ 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 = "";
@@ -365,29 +388,32 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@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 &&
+ 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._scrollTop = this._outerRef.current.scrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
- }
+ 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) => {
@@ -447,6 +473,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (url && !preview) {
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) {
@@ -504,7 +536,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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" });
+ !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: () => {
this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts;
@@ -529,7 +561,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
onMarqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.ActiveTool)) {
setupMoveUpEvents(this, e, action(e => {
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
@@ -538,9 +570,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
@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();
@@ -554,10 +590,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@computed get urlContent() {
if (this._hackHide || (this.webThumb && (!this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc))) return (null);
+ this.props.thumbShown?.();
const field = this.dataDoc[this.props.fieldKey];
let view;
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"}
@@ -571,7 +608,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
}
@@ -590,10 +633,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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 nativeHeight = NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"]);
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;
+ this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio);
onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
@@ -603,7 +648,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"]);
+ 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);
@@ -613,19 +664,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._showSidebar = true;
}
else {
- this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
+ this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar;
this.layoutDoc._width = this.layoutDoc[WidthSym]() * nativeWidth * pdfratio / curNativeWidth;
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ 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)
+ 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;
+ const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== "none" && CurrentUserUtils.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>;
@@ -633,10 +688,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@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`} />)}
+ <Annotation {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />)}
</div>;
}
@@ -648,7 +702,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<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)} />
+ 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>
@@ -665,7 +719,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
showInfo = action((anno: Opt<Doc>) => this._overlayAnnoInfo = anno);
setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => this._setPreviewCursor = func;
- panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
+ panelWidth = () => this.props.PanelWidth() / (this.props.scaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (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);
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@@ -678,9 +732,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
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[]) =>
@@ -703,17 +758,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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"} />;
+ 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-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
<div className="webBox-container" style={{
- position: "absolute",
- width: `calc(${100 / scale}% - ${(this.sidebarWidth() + WebBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`,
+ width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : (this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale * (this._previewWidth ? scale : 1)}px)`,
transform: `scale(${scale})`,
pointerEvents
}} onContextMenu={this.specificContextMenu}>
@@ -726,7 +780,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}
>
- <div className={"webBox-innerContent"} style={{ height: NumCast(this.scrollHeight, 50), pointerEvents }}>
+ <div className={"webBox-innerContent"} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : "100%", pointerEvents }}>
{this.content}
<div style={{ mixBlendMode: "multiply" }}>
{renderAnnotations(this.transparentFilter)}
@@ -737,19 +791,26 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</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 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.SidebarShown ? undefined : "none",
+ width: WebBox.sidebarResizerWidth,
+ left: `calc(100% - ${this.sidebarWidth() - WebBox.sidebarResizerWidth}px)`
+ }}
+ onPointerDown={e => this.sidebarBtnDown(e, false)} />
<SidebarAnnos ref={this._sidebarRef}
{...this.props}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
@@ -758,7 +819,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
setHeight={emptyFunction}
- nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth) - WebBox.sidebarResizerWidth / (this.props.scaling?.() || 1)}
showSidebar={this.SidebarShown}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
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..6cd56f84e 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -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%;
@@ -40,8 +40,10 @@
height: 80%;
}
- &.clickBtn {
+ &.clickBtn,
+ &.clickBtnLabel {
cursor: pointer;
+ flex-direction: column;
&:hover {
background-color: rgba(0, 0, 0, 0.3) !important;
@@ -53,6 +55,12 @@
}
}
+ &.clickBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.textBtn {
display: grid;
/* grid-row: auto; */
@@ -64,12 +72,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
@@ -146,10 +156,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 +176,12 @@
}
}
+ &.toolBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.menuBtn {
cursor: pointer !important;
border-radius: 0px;
@@ -166,15 +191,16 @@
width: 45% !important;
height: 45%;
}
-
- &:hover{
+
+ &:hover {
filter: brightness(0.85);
}
}
- &.colorBtn {
+ &.colorBtn,
+ &.colorBtnLabel {
color: black;
cursor: pointer;
flex-direction: column;
@@ -204,6 +230,12 @@
}
}
+ &.colorBtnLabel {
+ svg {
+ margin-top: -4px;
+ }
+ }
+
&.drpdownList {
width: 100%;
display: grid;
@@ -278,6 +310,23 @@
background-color: #e3e3e3;
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
border-radius: $standard-border-radius;
+
+ 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;
+ }
}
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index ca13590de..85efc67a5 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,21 +1,24 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
+import { StringIterator } from 'lodash';
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 { aggregateBounds, Utils } from '../../../../Utils';
import { DocumentType } from '../../../documents/DocumentTypes';
+import { CurrentUserUtils } from '../../../util/CurrentUserUtils';
import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
import { SelectionManager } from '../../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { CollectionViewType } from '../../collections/CollectionView';
import { ContextMenu } from '../../ContextMenu';
import { DocComponent } from '../../DocComponent';
@@ -23,6 +26,7 @@ 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';
@@ -67,7 +71,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
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" });
@@ -75,6 +79,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
}
+ 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;
@@ -107,25 +114,27 @@ 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")}
- onPointerUp={() => this._batch?.end()}
- onChange={e => { e.stopPropagation(); setValue(Number(e.target.value)); }}
- />
- </div>;
+ const dropdown = <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} >
+ <input type="range" step="1" min={NumCast(this.rootDoc.numBtnMin, 0)} max={NumCast(this.rootDoc.numBtnMax, 100)} value={checkResult}
+ className={"menu-slider"} id="slider"
+ onPointerDown={() => this._batch = UndoManager.StartBatch("presDuration")}
+ onPointerUp={() => this._batch?.end()}
+ onChange={e => { e.stopPropagation(); setValue(Number(e.target.value)); }}
+ />
+ </div>;
return (
<div
className={`menuButton ${this.type} ${numBtnType}`}
onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}
>
{checkResult}
+ {label}
{this.rootDoc.dropDownOpen ? dropdown : null}
</div>
);
@@ -206,7 +215,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
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>}
+ {!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 }}>
@@ -229,6 +238,7 @@ 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;
@@ -263,7 +273,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// Get items to place into the list
const list = this.buttonList.map((value) => {
- if (Doc.UserDoc().noviceMode && !noviceList.includes(value)) {
+ if (Doc.noviceMode && !noviceList.includes(value)) {
return;
}
return <div className="list-item" key={`${value}`}
@@ -276,8 +286,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>;
});
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor, position: "absolute" }}>
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
+ <div className="fontIconBox-label" style={{ bottom: 0, position: "absolute", color: color, backgroundColor: backgroundColor }}>
{this.label}
</div>;
@@ -330,8 +340,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
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" }}>
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
+ <div className="fontIconBox-label" style={{ color, backgroundColor }}>
{this.label}
</div>;
@@ -342,7 +352,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</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)}
onPointerDown={e => e.stopPropagation()}>
@@ -374,8 +384,8 @@ 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" }}>
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
+ <div className="fontIconBox-label" style={{ color, backgroundColor }}>
{this.label}
</div>;
@@ -387,13 +397,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<input type="checkbox"
checked={backgroundColor === Colors.MEDIUM_BLUE}
/>
- <span className="slider round"></span>
+ <span className="slider round" />
</label>
</div>
);
} else {
return (
- <div className={`menuButton ${this.type}`}
+ <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}
@@ -416,7 +426,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
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>}
+ {!this.label || !FontIconBox.GetShowLabels() ? (null) :
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
</div>
</div>
);
@@ -442,12 +453,12 @@ 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" }}>
+ 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) :
+ const menuLabel = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color, backgroundColor: "transparent" }}>
{this.label}
</div>;
@@ -455,7 +466,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
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:
@@ -489,7 +500,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,7 +512,7 @@ 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>
@@ -514,7 +525,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
{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)} />
+ <FontIconBadge value={Cast(this.Document.badgeValue,"string",null)} />
</div >
);
break;
@@ -523,9 +534,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
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>;
+ button !== null ?
+ <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>
+ {button}
+ </Tooltip > : null
}
}
@@ -559,8 +571,8 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
// 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;
+ 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");
@@ -656,13 +668,20 @@ 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 (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;
@@ -692,36 +711,115 @@ ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) {
});
+export function checkInksToGroup() {
+ // console.log("getting here to inks group");
+ if (CurrentUserUtils.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 (CurrentUserUtils.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, ffView);
+ });
+ }
+ 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) ?
+ return ((CurrentUserUtils.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ?
Colors.MEDIUM_BLUE : "transparent";
}
if (["circle", "square", "line"].includes(tool)) {
if (GestureOverlay.Instance.InkShape === tool) {
- Doc.UserDoc().activeInkTool = InkTool.None;
+ CurrentUserUtils.ActiveTool = InkTool.None;
GestureOverlay.Instance.InkShape = InkTool.None;
} else {
- Doc.UserDoc().activeInkTool = InkTool.Pen;
+ CurrentUserUtils.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;
+ if (CurrentUserUtils.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+ CurrentUserUtils.ActiveTool = InkTool.None;
+ } else if (tool == InkTool.Write) {
+ // console.log("write mode selected - create groupDoc here!", tool)
+ CurrentUserUtils.ActiveTool = tool;
+ GestureOverlay.Instance.InkShape = "";
} else {
- Doc.UserDoc().activeInkTool = tool;
+ CurrentUserUtils.ActiveTool = tool as any;
GestureOverlay.Instance.InkShape = "";
}
} else {
- Doc.UserDoc().activeInkTool = InkTool.None;
+ CurrentUserUtils.ActiveTool = InkTool.None;
}
});
@@ -805,9 +903,9 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
}
else if (selected) {
if (NumCast(selected.schemaPreviewWidth) > 0) {
- selected.schemaPreviewWidth = 200;
- } else {
selected.schemaPreviewWidth = 0;
+ } else {
+ selected.schemaPreviewWidth = 200;
}
}
});
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/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 364be461f..1d8e3a2cf 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -182,7 +182,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}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 34908e54b..bb3791f1e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -8,35 +8,41 @@ 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 { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils";
+import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu";
+import { Tooltip } from "@material-ui/core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export class DashFieldView {
_fieldWrapper: HTMLDivElement; // container for label and value
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
+
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.textContent = node.attrs.fieldKey.startsWith("#") ? node.attrs.fieldKey : node.attrs.fieldKey + " " + strVal;
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
+ 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._fieldWrapper);
+ />, this._fieldWrapper));
(this as any).dom = this._fieldWrapper;
}
destroy() { ReactDOM.unmountComponentAtNode(this._fieldWrapper); }
@@ -58,7 +64,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) {
@@ -77,16 +82,17 @@ 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
@@ -109,10 +115,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
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();
- }));
+ r?.addEventListener("pointerdown", action(e => e.stopPropagation()));
}} >
{strVal}
</span>;
@@ -124,7 +127,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
@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! }]);
this.updateText(span.textContent!, true);
e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view
}
@@ -142,7 +144,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
@action
updateText = (nodeText: string, forceMatch: boolean) => {
- this._showEnumerables = false;
if (nodeText) {
const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText;
@@ -154,7 +155,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
(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(":=")) {
@@ -166,7 +166,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
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);
+ 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;
@@ -178,18 +178,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
}
}
- // 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;
@@ -208,6 +197,16 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
}
}
+
+ // 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,
@@ -220,8 +219,40 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
{this.props.fieldKey.startsWith("#") ? (null) : this.fieldValueContent}
- {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
-
</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);
+ }
+
+ 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>,
+ ];
+
+ return this.getElement(buttons);
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 6192b6829..8e1698eba 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,25 +1,26 @@
+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 { 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 { 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, 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 { RichTextUtils } from '../../../../fields/RichTextUtils';
-import { Cast, DateCast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { ComputedField } from '../../../../fields/ScriptField';
+import { 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, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -28,7 +29,8 @@ 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 { 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";
@@ -38,10 +40,11 @@ import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
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 { LinkDocPreview } from '../LinkDocPreview';
import { DashDocCommentView } from "./DashDocCommentView";
@@ -60,9 +63,6 @@ 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");
export interface FormattedTextBoxProps {
@@ -83,7 +83,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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();
@@ -220,6 +220,7 @@ 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.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -244,8 +245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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
@@ -256,7 +255,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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("#")) {
@@ -284,6 +283,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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
+ ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
unchanged = false;
}
this._applyingChange = "";
@@ -346,37 +346,78 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ autoLink = () => {
+ if (this._editorView?.state.doc.textContent) {
+ const newAutoLinks = new Set<Doc>();
+ const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+ 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(CurrentUserUtils.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 = () => {
+ const title = StrCast(this.dataDoc.title);
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"] &&
+ (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;
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(CurrentUserUtils.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)) {
@@ -454,7 +495,7 @@ 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](),
@@ -514,35 +555,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
updateHighlights = () => {
+ const highlights = FormattedTextBox._globalHighlights;
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
- if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) {
+ if (highlights.indexOf("Audio Tags") === -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, "");
}
- if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+ if (highlights.indexOf("Text from Others") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
}
- if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+ if (highlights.indexOf("My Text") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
}
- if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+ if (highlights.indexOf("Todo Items") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" });
}
- if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+ if (highlights.indexOf("Important Items") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" });
}
- if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+ if (highlights.indexOf("Bold Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror strong > span", { "font-size": "large" }, "");
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror :not(strong > span)", { "font-size": "0px" }, "");
+ }
+ if (highlights.indexOf("Disagree Items") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" });
}
- if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+ if (highlights.indexOf("Ignore Items") !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" });
}
- if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -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() }));
setTimeout(this.updateHighlights);
}
- if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -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() }));
@@ -599,29 +645,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}), 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 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.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
+ (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"
}));
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" });
+ !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.UserDoc().noviceMode && uicontrols.push({
+ !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"
});
@@ -631,12 +677,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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({
+ !Doc.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);
+ MakeTemplate(this.rootDoc, true, title);
} else if (!this.rootDoc.isTemplateDoc) {
const title = StrCast(this.rootDoc.title);
this.rootDoc.title = "text";
@@ -645,7 +691,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this.rootDoc.isTemplateDoc = false;
this.rootDoc.isTemplateForField = "";
this.rootDoc.layoutKey = "layout";
- this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title);
+ 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
@@ -721,6 +767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
+ // 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 +775,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) => {
@@ -784,6 +831,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
};
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);
@@ -803,9 +851,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
- return this._focusSpeed;
+ return this._didScroll ? this._focusSpeed : 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) => {
@@ -814,6 +863,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (nh) this.layoutDoc._nativeHeight = scrollHeight;
}
+ @computed get contentScaling() { return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.scaling?.() || 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);
@@ -827,9 +877,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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));
+ autoHeight && this.props.setHeight?.(this.contentScaling * (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
+ 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;
@@ -889,11 +939,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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 });
@@ -925,7 +977,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}, { fireImmediately: true }
);
quickScroll = undefined;
- setTimeout(this.tryUpdateScrollHeight, 10);
+ this.tryUpdateScrollHeight();
}
pushToGoogleDoc = async () => {
@@ -1098,6 +1150,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
});
}
+ _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);
@@ -1112,7 +1165,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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,6 +1173,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} else {
scrollRef.scrollTo({ top: scrollPos });
}
+ this._didScroll = true;
}
return true;
},
@@ -1154,19 +1208,21 @@ 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)) {
+ 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);
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;
}
@@ -1195,6 +1251,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
onPointerDown = (e: React.PointerEvent): void => {
+ 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") {
e.preventDefault();
@@ -1256,7 +1313,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
!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;
@@ -1356,9 +1413,9 @@ 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)));
}
@@ -1399,6 +1456,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@action
onBlur = (e: any) => {
+ this.autoLink();
FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
@@ -1421,12 +1479,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} catch (e: any) { console.log(e.message); }
this._lastText = curText;
}
+ 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) => {
- // 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 (e.altKey) {
e.preventDefault();
return;
@@ -1443,9 +1506,9 @@ 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 &&
+ 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();
@@ -1462,7 +1525,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
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({}))
+ [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();
@@ -1486,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (children) {
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
+ 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.doc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
@@ -1496,7 +1559,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
}
- fitToBox = () => this.props.Document._fitToBox;
+ fitContentsToBox = () => this.props.Document._fitContentsToBox;
sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
if (!this.layoutDoc._showSidebar) this.toggleSidebar();
@@ -1571,22 +1634,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
ScreenToLocalTransform={this.sidebarScreenToLocal}
renderDepth={this.props.renderDepth + 1}
setHeight={this.setSidebarHeight}
- fitContentsToDoc={this.fitToBox}
+ fitContentsToBox={this.fitContentsToBox}
noSidebar={true}
fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
};
- return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
+ return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.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 selected = this.props.isSelected() || this.Document.forceActive;
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 interactive = (CurrentUserUtils.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);
@@ -1608,7 +1671,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
<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),
+ 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),
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 1fde6e5f0..3e673c0b2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,6 +2,7 @@ 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';
@@ -9,7 +10,7 @@ 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 findLinkMark(marks: 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: Mark[]) => Mark | undefined) {
let before = 0, nbef = rpos.nodeBefore;
while (nbef && finder(nbef.marks)) {
@@ -82,14 +83,14 @@ export class FormattedTextBoxComment {
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));
+ 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) {
@@ -115,6 +116,7 @@ export class FormattedTextBoxComment {
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
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index c76eda859..c66cb502e 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -1,20 +1,15 @@
-import { chainCommands, exitCode, joinDown, joinUp, lift, deleteSelection, joinBackward, selectNodeBackward, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn, newlineInCode } from "prosemirror-commands";
-import { liftTarget } from "prosemirror-transform";
+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 { 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 { 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;
@@ -48,31 +43,8 @@ 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)) {
+ switch (GetEffectiveAcl(props.DataDoc)) {
case AclAugment: return false;
case AclSelfEdit:
for (var i = state.selection.from; i < state.selection.to; i++) {
@@ -108,12 +80,12 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//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("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<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;
- }
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const ref = state.selection;
const range = ref.$from.blockRange(ref.$to);
@@ -138,8 +110,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
});
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;
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
@@ -187,14 +158,13 @@ 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) => {
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
if (!deleteSelection(state, (tx: Transaction<S>) => {
@@ -216,8 +186,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;
+ if (props.onKey?.(event, props)) return true;
if (!canEdit(state)) return true;
const trange = state.selection.$from.blockRange(state.selection.$to);
@@ -276,8 +246,6 @@ 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);
return keys;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 4814d6e9a..98343a261 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -1,10 +1,10 @@
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 { 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, NodeType, ResolvedPos } from "prosemirror-model";
+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";
@@ -35,6 +35,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
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;
@@ -66,7 +67,6 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
runInAction(() => {
RichTextMenu.Instance = this;
this._canFade = false;
- //this.Pinned = BoolCast(Doc.UserDoc()["menuRichText-pinned"]);
this.Pinned = true;
});
}
@@ -79,6 +79,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._reaction?.();
}
+ @computed get noAutoLink() { return this._noLinkActive; }
@computed get bold() { return this._boldActive; }
@computed get underline() { return this._underlineActive; }
@computed get italics() { return this._italicsActive; }
@@ -118,7 +119,7 @@ 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._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]) : "...";
@@ -220,7 +221,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;
@@ -264,6 +265,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,6 +275,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
activeMarks.forEach(mark => {
switch (mark.name) {
+ case "noAutoLinkAnchor": this._noLinkActive = true; break;
case "strong": this._boldActive = true; break;
case "em": this._italicsActive = true; break;
case "underline": this._underlineActive = true; break;
@@ -283,6 +286,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
}
+ 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);
@@ -310,10 +321,17 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
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);
+ }
}
}
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index bafae84dc..8851d52e4 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -275,7 +275,7 @@ export class RichTextRules {
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);
+ DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to:portal from", undefined);
const fstate = this.TextBox.EditorView?.state;
if (fstate && selection) {
@@ -294,6 +294,25 @@ export class RichTextRules {
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
@@ -329,7 +348,7 @@ export class RichTextRules {
this.Document[DataSym].tags = `${tags + "#" + tag + ':'}`;
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" + tag });
- return state.tr.deleteRange(start, end).insert(start, fieldView);
+ return state.tr.deleteRange(start, end).insert(start, fieldView).insertText(" ");
}),
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 6103a28d6..2fde5c7ba 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -17,7 +17,53 @@ export const marks: { [index: string]: MarkSpec } = {
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: {
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..b169ad6cd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -10,17 +10,16 @@ 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 { BoolCast, Cast, DocCast, NumCast, StrCast } from "../../../../fields/Types";
+import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } 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 { MarqueeViewBounds } from "../../collections/collectionFreeForm";
import { CollectionView, CollectionViewType } from "../../collections/CollectionView";
import { TabDocView } from "../../collections/TabDocView";
import { ViewBoxBaseComponent } from "../../DocComponent";
@@ -30,12 +29,21 @@ import { CollectionFreeFormDocumentView } from "../CollectionFreeFormDocumentVie
import { FieldView, FieldViewProps } from '../FieldView';
import "./PresBox.scss";
import { PresEffect, PresMovement, PresStatus } from "./PresEnums";
+import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-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
@@ -47,17 +55,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* @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) {
+ 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>);
@@ -71,7 +79,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
public static EffectsProvider(layoutDoc: Doc, renderDoc: any) {
return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ?
- PresBox.renderEffectsDoc(renderDoc, layoutDoc)
+ PresBox.renderEffectsDoc(renderDoc, layoutDoc, PresBox.Instance.childDocs[PresBox.Instance.itemIndex])
:
renderDoc;
}
@@ -90,13 +98,20 @@ 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) {
@@ -117,21 +132,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 (CurrentUserUtils.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() {
@@ -149,8 +152,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get selectedDoc() { return this.selectedDocumentView?.rootDoc; }
+ _unmounting = false;
@action
componentWillUnmount() {
+ this._unmounting = true;
document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
this._presKeyEventsActive = false;
this.resetPresentation();
@@ -161,22 +166,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
componentDidMount() {
- this.rootDoc.presBox = this.rootDoc;
+ 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));
+ DocListCastAsync(CurrentUserUtils.MyTrails.data).then(pres =>
+ !pres?.includes(this.rootDoc) && Doc.AddDocToList(CurrentUserUtils.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;
+ if (pres) CurrentUserUtils.ActivePresentation = pres;
+ else CurrentUserUtils.ActivePresentation = this.rootDoc;
document.removeEventListener("keydown", PresBox.keyEventsWrapper, true);
document.addEventListener("keydown", PresBox.keyEventsWrapper, true);
this._presKeyEventsActive = true;
@@ -202,30 +207,18 @@ 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
+ // 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);
@@ -311,6 +304,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.startTempMedia(targetDoc, activeItem);
}
if (targetDoc) {
+ 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;
@@ -322,7 +316,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
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,22 +325,40 @@ 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);
}
/**
@@ -360,7 +372,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
@@ -388,7 +400,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,17 +413,20 @@ 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
+ 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);
}
@@ -601,27 +616,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* 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(CurrentUserUtils.MyOverlayDocs?.data).includes(this.layoutDoc)) {
this.layoutDoc.presStatus = PresStatus.Edit;
- Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, this.rootDoc);
+ Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, this.rootDoc);
CollectionDockingView.AddSplit(this.rootDoc, "right");
- } else if ((true || this.layoutDoc.context) && docView) {
- console.log("case 2");
+ } 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(CurrentUserUtils.MyOverlayDocs, undefined, this.rootDoc);
+ this.props.removeDocument?.(this.layoutDoc);
}
}
@@ -629,14 +638,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* 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;
+ }
});
/**
@@ -710,11 +722,11 @@ 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);
+ 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) &&
+ isContentActive = (outsideReaction?: boolean) => ((CurrentUserUtils.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition) &&
(this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
/**
@@ -809,13 +821,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//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) => {
@@ -850,7 +862,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
break;
case "Escape":
- if (CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { this.updateMinimize(); }
+ if (DocListCast(CurrentUserUtils.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);
@@ -984,7 +996,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/**
* Method called for viewing paths which adds a single line with
* points at the center of each document added.
- * Design choice: When this is called it sets _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)
@@ -1103,7 +1115,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
+ const tagDoc = doc;// Cast(doc.presentationTargetDoc, Doc, null);
switch (effect) {
case PresEffect.Left:
tagDoc.presEffectDirection = PresEffect.Left;
@@ -1129,7 +1141,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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);
+ const tagDoc = doc;//Cast(doc.presentationTargetDoc, Doc, null);
switch (effect) {
case PresEffect.Bounce:
tagDoc.presEffect = PresEffect.Bounce;
@@ -1166,7 +1178,7 @@ 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; })}>
@@ -1218,6 +1230,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="ribbon-property">
<input className="presBox-input"
type="number" value={transitionSpeed}
+ onKeyDown={e => e.stopPropagation()}
onChange={action((e) => this.setTransitionTime(e.target.value))} /> s
</div>
<div className="ribbon-propertyUpDown">
@@ -1257,6 +1270,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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">
@@ -1288,12 +1302,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{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={`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={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Roll ? "active" : ""}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>Roll</div>
</div>
</div>
<div className="ribbon-doubleButton" style={{ display: effect === 'None' ? "none" : "inline-flex" }}>
@@ -1303,11 +1317,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</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>
+ <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="ribbon-final-box">
@@ -1322,7 +1336,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get effectDirection(): string {
let effect = '';
- switch (this.targetDoc.presEffectDirection) {
+ 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;
@@ -1338,8 +1352,8 @@ 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);
+ 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);
@@ -1369,8 +1383,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const scroll = targetDoc._scrollTop;
activeItem.presPinView = true;
activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeItem.presPinTimecode = targetDoc._currentTimecode;
+ } 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;
@@ -1391,8 +1406,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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 ([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;
@@ -1422,6 +1438,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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>
@@ -1431,6 +1448,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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>
@@ -1440,6 +1458,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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>
@@ -1460,6 +1479,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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>
@@ -1485,6 +1505,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@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 : "";
@@ -1504,6 +1526,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<input className="presBox-input"
style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }}
type="number" value={NumCast(activeItem.presStartTime)}
+ onKeyDown={e => e.stopPropagation()}
onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presStartTime = Number(e.target.value); })}
/>
</div>
@@ -1522,6 +1545,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
<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); })}
@@ -1530,7 +1554,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</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"}`}
id="toolbar-slider"
@@ -1554,7 +1578,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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"}`}
id="toolbar-slider"
@@ -1580,9 +1604,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}} />
</div>
<div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}>
- <div className="slider-text">0 s</div>
+ <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">
@@ -1775,16 +1799,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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;
@@ -2217,7 +2241,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const propTitle = CurrentUserUtils.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 === CurrentUserUtils.ActivePresentation);
const activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
return (mode === CollectionViewType.Carousel3D) ? (null) : (
@@ -2268,13 +2292,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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"
+ {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.Carousel3D}>3D Carousel</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" : ""}`}>
@@ -2414,66 +2439,141 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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
this.isPres;
// 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 presKeyEvents: boolean = (this.isPres && this._presKeyEventsActive && this.rootDoc === CurrentUserUtils.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">
+ return DocListCast(CurrentUserUtils.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 }} onClick={() => this.layoutDoc.presLoop = !this.layoutDoc.presLoop}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip>
+ <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 >
:
- <div className="presBox-cont" style={{ minWidth: CurrentUserUtils.OverlayDocs.includes(this.layoutDoc) ? 240 : undefined }} >
+ <div className="presBox-cont" style={{ minWidth: DocListCast(CurrentUserUtils.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>;
}
}
-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;
+
+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));
}); \ No newline at end of file
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..6f853b2bc 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -2,17 +2,18 @@ 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 { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
-import { Cast, NumCast, StrCast } from "../../../../fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
-import { DocUtils } from "../../../documents/Documents";
+import { Docs, 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 { CollectionViewType } from "../../collections/CollectionView";
import { ViewBoxBaseComponent } from '../../DocComponent';
import { EditableView } from "../../EditableView";
import { Colors } from "../../global/globalEnums";
@@ -23,6 +24,7 @@ import { PresBox } from "./PresBox";
import "./PresElementBox.scss";
import { PresMovement } from "./PresEnums";
import React = require("react");
+import { List } from "../../../../fields/List";
/**
* This class models the view a document added to presentation will have in the presentation.
* It involves some functionality for its buttons and options.
@@ -33,12 +35,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
_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 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() {
@@ -76,10 +76,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
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 +86,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}
@@ -106,6 +106,39 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
</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; }
@@ -133,11 +166,11 @@ 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);
+ PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
return this.startDrag(e);
}), emptyFunction, emptyFunction);
}
@@ -158,9 +191,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
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];
+ const doc = this._itemRef.current || dragArray[0];
doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
dragItem.push(doc);
} else if (dragArray.length >= 1) {
@@ -224,11 +261,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@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
@@ -248,130 +286,230 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@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 toolbarWidth(): number {
- const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
- let width: number = NumCast(this.presBox._width);
- if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
- if (width === 0) width = 300;
- return width;
- }
+ @computed get recordingIsInOverlay() {
+ let isInOverlay = false
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ isInOverlay = true
+ return
+ }
+ })
+ return isInOverlay
+ }
- // TODO: this is what you were looking for
- @computed get mainItem() {
- 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 targetDoc: Doc = this.targetDoc;
- const activeItem: Doc = this.rootDoc;
- return (
- <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
- }}
- 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);
- }}
- onDoubleClick={action(e => {
- this.toggleProperties();
- PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
- })}
- 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()} />
- </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>
- </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 >);
- }
+ removeAllRecordingInOverlay = () => {
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
+ }
+ })
+ }
- render() {
- return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
- }
+ @undoBatch
+ @action
+ hideRecording = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ this.removeAllRecordingInOverlay()
+ }
+
+ @undoBatch
+ @action
+ showRecording = (activeItem: Doc) => {
+ this.removeAllRecordingInOverlay()
+ if (activeItem.recording) {
+ // if we already have an existing recording
+ Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+
+ }
+ }
+
+ @undoBatch
+ @action
+ startRecording = (activeItem: Doc) => {
+ // Remove every recording that already exists in overlay view
+ DocListCast(CurrentUserUtils.MyOverlayDocs.data).forEach((doc) => {
+ if (doc.slides !== null) {
+ Doc.RemoveDocFromList(CurrentUserUtils.MyOverlayDocs, undefined, doc);
+ }
+ })
+
+ if (activeItem.recording) {
+ // if we already have an existing recording
+ Doc.AddDocToList(CurrentUserUtils.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+
+ } else {
+ // if we dont have any recording
+ const recording = Docs.Create.WebCamDocument("", {
+ _width: 400, _height: 200,
+ // 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(CurrentUserUtils.MyOverlayDocs, undefined, recording);
+ }
+ }
+
+ @computed
+ get toolbarWidth(): number {
+ const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
+ let width: number = NumCast(this.presBox._width);
+ if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
+ if (width === 0) width = 300;
+ return width;
+ }
+
+ @computed get mainItem() {
+ 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 targetDoc: Doc = this.targetDoc;
+ const activeItem: Doc = this.rootDoc;
+ return (
+ <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
+ }}
+ 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();
+ PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+ })}
+ 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>} */}
+ {/* <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={this.hideRecording}
+ style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={"video-slash"} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip> :
+ <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
+ <div className="slideButton"
+ onClick={() => 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></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 >);
+ }
+
+ render() {
+ return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
+ }
} \ No newline at end of file