aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormehekj <mehek.jethani@gmail.com>2022-06-20 18:12:40 -0400
committermehekj <mehek.jethani@gmail.com>2022-06-20 18:12:40 -0400
commit145117365b2708ef6b365c6f0f10c38b85a87307 (patch)
tree266b75feb19c3ded22f8185e8ed11eb91e6b6309 /src
parent7eedde332010c8896be636f0b5c6a7b2c8043e48 (diff)
parent3351c0372a8372a3e91bbd814f827a8b984f8665 (diff)
Merge branch 'master' into temporalmedia-mehek
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts12
-rw-r--r--src/client/Network.ts74
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts47
-rw-r--r--src/client/goldenLayout.js7
-rw-r--r--src/client/util/CurrentUserUtils.ts134
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/util/History.ts6
-rw-r--r--src/client/util/RecordingApi.ts269
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/views/DashboardView.scss48
-rw-r--r--src/client/views/DashboardView.tsx69
-rw-r--r--src/client/views/DocComponent.tsx6
-rw-r--r--src/client/views/DocumentButtonBar.tsx52
-rw-r--r--src/client/views/DocumentDecorations.tsx1049
-rw-r--r--src/client/views/GestureOverlay.tsx9
-rw-r--r--src/client/views/InkTangentHandles.tsx3
-rw-r--r--src/client/views/InkTranscription.tsx184
-rw-r--r--src/client/views/LightboxView.tsx2
-rw-r--r--src/client/views/Main.tsx3
-rw-r--r--src/client/views/MainView.scss15
-rw-r--r--src/client/views/MainView.tsx52
-rw-r--r--src/client/views/MarqueeAnnotator.tsx10
-rw-r--r--src/client/views/OverlayView.scss12
-rw-r--r--src/client/views/OverlayView.tsx14
-rw-r--r--src/client/views/PropertiesButtons.tsx14
-rw-r--r--src/client/views/PropertiesView.tsx2
-rw-r--r--src/client/views/ScriptingRepl.tsx3
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx135
-rw-r--r--src/client/views/collections/CollectionDockingViewMenu.scss34
-rw-r--r--src/client/views/collections/CollectionDockingViewMenu.tsx48
-rw-r--r--src/client/views/collections/CollectionMenu.tsx7
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx1
-rw-r--r--src/client/views/collections/CollectionStackingView.scss5
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx2
-rw-r--r--src/client/views/collections/TabDocView.tsx146
-rw-r--r--src/client/views/collections/TreeView.tsx15
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx158
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx124
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx3
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx3
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx4
-rw-r--r--src/client/views/nodes/AudioBox.tsx85
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx4
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx3
-rw-r--r--src/client/views/nodes/DocumentView.tsx21
-rw-r--r--src/client/views/nodes/FieldView.tsx1
-rw-r--r--src/client/views/nodes/FilterBox.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx2
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx6
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx14
-rw-r--r--src/client/views/nodes/PDFBox.tsx44
-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.tsx2173
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js4
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx32
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx7
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx22
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts2
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx152
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss328
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx438
-rw-r--r--src/client/views/pdf/PDFViewer.tsx7
-rw-r--r--src/client/views/topbar/TopBar.scss276
-rw-r--r--src/client/views/topbar/TopBar.tsx126
-rw-r--r--src/fields/Doc.ts1
-rw-r--r--src/fields/Types.ts4
-rw-r--r--src/fields/URLField.ts1
-rw-r--r--src/fields/documentSchemas.ts6
-rw-r--r--src/mobile/AudioUpload.tsx4
-rw-r--r--src/server/ApiManagers/UploadManager.ts462
80 files changed, 4746 insertions, 3324 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 4dd9979ea..b87980397 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -677,6 +677,8 @@ export function StopEvent(e: React.PointerEvent | React.MouseEvent) {
e.preventDefault();
}
+
+
export function setupMoveUpEvents(
target: object,
e: React.PointerEvent,
@@ -692,6 +694,7 @@ export function setupMoveUpEvents(
(target as any)._lastTap = Date.now();
(target as any)._downX = (target as any)._lastX = e.clientX;
(target as any)._downY = (target as any)._lastY = e.clientY;
+ (target as any)._noClick = false;
const _moveEvent = (e: PointerEvent): void => {
if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
@@ -723,17 +726,20 @@ export function setupMoveUpEvents(
clearTimeout((target as any)._doubleTime);
(target as any)._doubleTime = undefined;
}
- clickEvent(e, (target as any)._doubleTap);
+ (target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
}
document.removeEventListener("pointermove", _moveEvent);
document.removeEventListener("pointerup", _upEvent);
};
+ const _clickEvent = (e: MouseEvent): void => {
+ if ((target as any)._noClick) e.stopPropagation();
+ document.removeEventListener("click", _clickEvent, true);
+ }
if (stopPropagation) {
e.stopPropagation();
e.preventDefault();
}
- document.removeEventListener("pointermove", _moveEvent);
- document.removeEventListener("pointerup", _upEvent);
document.addEventListener("pointermove", _moveEvent);
document.addEventListener("pointerup", _upEvent);
+ document.addEventListener("click", _clickEvent, true);
} \ No newline at end of file
diff --git a/src/client/Network.ts b/src/client/Network.ts
index bf2918734..3597e7b2b 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -4,46 +4,46 @@ import { Upload } from "../server/SharedMediaTypes";
export namespace Networking {
- export async function FetchFromServer(relativeRoute: string) {
- return (await fetch(relativeRoute)).text();
- }
+ export async function FetchFromServer(relativeRoute: string) {
+ return (await fetch(relativeRoute)).text();
+ }
- export async function PostToServer(relativeRoute: string, body?: any) {
- const options = {
- uri: Utils.prepend(relativeRoute),
- method: "POST",
- body,
- json: true
- };
- return requestPromise.post(options);
- }
+ export async function PostToServer(relativeRoute: string, body?: any) {
+ const options = {
+ uri: Utils.prepend(relativeRoute),
+ method: "POST",
+ body,
+ json: true
+ };
+ return requestPromise.post(options);
+ }
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
- const formData = new FormData();
- if (Array.isArray(files)) {
- if (!files.length) {
- return [];
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+ const formData = new FormData();
+ if (Array.isArray(files)) {
+ if (!files.length) {
+ return [];
+ }
+ files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+ } else {
+ formData.append(Utils.GenerateGuid(), files);
}
- files.forEach(file => formData.append(Utils.GenerateGuid(), file));
- } else {
- formData.append(Utils.GenerateGuid(), files);
- }
- const parameters = {
- method: 'POST',
- body: formData
- };
- const response = await fetch("/uploadFormData", parameters);
- return response.json();
- }
+ const parameters = {
+ method: 'POST',
+ body: formData
+ };
+ const response = await fetch("/uploadFormData", parameters);
+ return response.json();
+ }
- export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
- const parameters = {
- method: 'POST',
- body: JSON.stringify({ videoId }),
- json: true
- };
- const response = await fetch("/uploadYoutubeVideo", parameters);
- return response.json();
- }
+ export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+ const parameters = {
+ method: 'POST',
+ body: JSON.stringify({ videoId }),
+ json: true
+ };
+ const response = await fetch("/uploadYoutubeVideo", parameters);
+ return response.json();
+ }
} \ No newline at end of file
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 161dff6e0..ca942a38a 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -9,6 +9,7 @@ export enum DocumentType {
KVP = "kvp",
VID = "video",
AUDIO = "audio",
+ REC = "recording",
PDF = "pdf",
INK = "inks",
SCREENSHOT = "screenshot",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2ca9cdb71..5f009573e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -11,7 +11,7 @@ import { RichTextField } from "../../fields/RichTextField";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../fields/Types";
-import { AudioField, ImageField, MapField, PdfField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
+import { AudioField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Upload } from "../../server/SharedMediaTypes";
import { OmitKeys, Utils, aggregateBounds } from "../../Utils";
@@ -61,6 +61,7 @@ import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { DocumentType } from "./DocumentTypes";
import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { MapBox } from "../views/nodes/MapBox/MapBox";
+import { RecordingBox } from "../views/nodes/RecordingBox/RecordingBox";
const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", ""));
class EmptyBox {
@@ -97,6 +98,7 @@ export class DocumentOptions {
allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?");
childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? ");
+ userColor?: string; // color associated with a Dash user (seen in header fields of shared documents)
color?: string; // foreground color data doc
backgroundColor?: STRt = new StrInfo("background color for data doc");
_backgroundColor?: STRt = new StrInfo("background color for each template layout doc (overrides backgroundColor)", true);
@@ -113,7 +115,8 @@ export class DocumentOptions {
_dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", true);
_dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units", true);
_fitWidth?: BOOLt = new BoolInfo("whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)", true);
- _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+ _fitContentsToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+ _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom]
_lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
_lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
_isPushpin?: boolean; // whether document, when clicked, toggles display of its link target
@@ -188,11 +191,18 @@ export class DocumentOptions {
childLayoutString?: string; // template string for collection to use to render its children
childDontRegisterViews?: boolean;
childHideLinkButton?: boolean; // hide link buttons on all children
+ childContextMenuFilters?: List<ScriptField>;
+ childContextMenuScripts?: List<ScriptField>;
+ childContextMenuLabels?: List<string>;
+ childContextMenuIcons?: List<string>;
hideLinkButton?: boolean; // whether the blue link counter button should be hidden
+ hideDecorationTitle?: boolean;
+ hideOpenButton?: boolean;
+ hideResizeHandles?: boolean;
+ hideDocumentButtonBar?: boolean;
hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden
isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: boolean;
- watchedDocuments?: Doc; // list of documents an icon doc monitors in order to display a badge count
targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc)
templates?: List<string>;
hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images)
@@ -244,7 +254,8 @@ export class DocumentOptions {
numBtnType?: string;
numBtnMax?: number;
numBtnMin?: number;
- switchToggle?: boolean;
+ switchToggle?: boolean;
+ badgeValue?: ScriptField;
//LINEAR VIEW
linearViewIsExpanded?: boolean; // is linear view expanded
@@ -288,6 +299,7 @@ export class DocumentOptions {
treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically
+ treeViewChildDoubleClick?: ScriptField; //
// Action Button
buttonMenu?: boolean; // whether a action button should be displayed
@@ -409,6 +421,10 @@ export namespace Docs {
layout: { view: AudioBox, dataField: defaultDataKey },
options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" }
}],
+ [DocumentType.REC, {
+ layout: { view: VideoBox, dataField: defaultDataKey },
+ options: { _height: 100, backgroundColor: "pink", links: "@links(self)" }
+ }],
[DocumentType.PDF, {
layout: { view: PDFBox, dataField: defaultDataKey },
options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" }
@@ -476,7 +492,7 @@ export namespace Docs {
options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)" },
}],
[DocumentType.WEBCAM, {
- layout: { view: DashWebRTCVideo, dataField: defaultDataKey },
+ layout: { view: RecordingBox, dataField: defaultDataKey },
options: { links: "@links(self)" }
}],
[DocumentType.PRESELEMENT, {
@@ -653,6 +669,7 @@ export namespace Docs {
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
dataProps[fieldKey + "-annotations"] = new List<Doc>();
+ dataProps[fieldKey + "-sidebar"] = new List<Doc>();
const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
const viewFirstProps: { [id: string]: any } = {};
@@ -671,9 +688,9 @@ export namespace Docs {
return viewDoc;
}
- export function ImageDocument(url: string, options: DocumentOptions = {}) {
- const imgField = new ImageField(url);
- return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(url), ...options });
+ export function ImageDocument(url: string|ImageField, options: DocumentOptions = {}) {
+ const imgField = url instanceof ImageField ? url : new ImageField(url);
+ return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options });
}
export function PresDocument(options: DocumentOptions = {}) {
@@ -710,6 +727,10 @@ export namespace Docs {
{ ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
}
+ export function RecordingDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.REC), "", options);
+ }
+
export function SearchDocument(options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List<Doc>([]), options);
}
@@ -1097,8 +1118,11 @@ export namespace DocUtils {
export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) {
broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1);
- return DocUtils.ActiveRecordings.map(audio =>
- DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording annotation:linked recording", "recording timeline"));
+ return DocUtils.ActiveRecordings.map(audio => {
+ const link = DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording annotation:linked recording", "recording timeline");
+ link && (link.followLinkLocation = "add:right");
+ return link;
+ });
}
export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
@@ -1170,6 +1194,9 @@ export namespace DocUtils {
} else if (field instanceof AudioField) {
created = Docs.Create.AudioDocument((field).url.href, resolved);
layout = AudioBox.LayoutString;
+ } else if (field instanceof RecordingField) {
+ created = Docs.Create.RecordingDocument((field).url.href, resolved);
+ layout = RecordingBox.LayoutString;
} else if (field instanceof InkField) {
created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved);
layout = InkingStroke.LayoutString;
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 82b10608d..012ee163c 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -458,8 +458,9 @@
if (this._bDragging === true) {
this._bDragging = false;
this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
- } else if (oEvent.target) { // make title receive pointer events to allow setting insertion position or selecting texst range
- if (oEvent.target.className.includes("lm_title_wrap")) {
+ } else { // make title receive pointer events to allow setting insertion position or selecting texst range
+ const classname = typeof oEvent.target?.className === "string" ? oEvent.target.className : "";
+ if (classname.includes("lm_title_wrap")) {
oEvent.target.children[0].style.pointerEvents = "all";
oEvent.target.children[0].focus();
}
@@ -1431,7 +1432,7 @@
this.root.callDownwards('_$init');
if (config.maximisedItemId === '__glMaximised') {
- this.root.getItemsById(config.maximisedItemId)[0].toggleMaximise();
+ this.root.getItemsById(config.maximisedItemId)[0]?.toggleMaximise();
}
},
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 51bfdbbd2..b4e18a8bb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -7,8 +7,8 @@ import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
-import { nullAudio } from "../../fields/URLField";
+import { BoolCast, Cast, DateCast, DocCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
+import { ImageField, nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
@@ -22,11 +22,10 @@ import { TreeView } from "../views/collections/TreeView";
import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
-import { LabelBox } from "../views/nodes/LabelBox";
import { CollectionFreeFormDocumentView } from "../views/nodes/CollectionFreeFormDocumentView";
import { OverlayView } from "../views/OverlayView";
import { DocumentManager } from "./DocumentManager";
-import { DragManager, dropActionType } from "./DragManager";
+import { DragManager } from "./DragManager";
import { makeTemplate, MakeTemplate } from "./DropConverter";
import { HistoryUtil } from "./History";
import { LinkManager } from "./LinkManager";
@@ -272,8 +271,8 @@ export class CurrentUserUtils {
doc.emptyScreenshot = Docs.Create.ScreenshotDocument({ ...standardOps(), title: "empty screenshot", _width: 400, _height: 200 });
}
if (doc.emptyWall === undefined) {
- doc.emptyWall = Docs.Create.ScreenshotDocument({ ...standardOps(), title: "screen snapshot", _width: 400, _height: 200, });
- (doc.emptyWall as Doc).videoWall = true;
+ doc.emptyWall = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ (doc.emptyWall as Doc).recording = true;
}
if (doc.emptyAudio === undefined) {
doc.emptyAudio = Docs.Create.AudioDocument(nullAudio, { ...standardOps(), title: "audio recording", x: 200, y: 200, _width: 200, _height: 100, });
@@ -355,6 +354,7 @@ export class CurrentUserUtils {
}
static menuBtnDescriptions(doc: Doc) {
+ const badgeValue = ScriptField.MakeFunction("((len) => len ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length)")
return [
{ title: "Dashboards", target: Cast(doc.myDashboards, Doc, null), icon: "desktop", click: 'selectMainMenu(self)' },
{ title: "Search", target: Cast(doc.mySearchPanel, Doc, null), icon: "search", click: 'selectMainMenu(self)' },
@@ -362,7 +362,7 @@ export class CurrentUserUtils {
{ title: "Tools", target: Cast(doc.myTools, Doc, null), icon: "wrench", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" },
{ title: "Imports", target: Cast(doc.myImportDocs, Doc, null), icon: "upload", click: 'selectMainMenu(self)' },
{ title: "Recently Closed", target: Cast(doc.myRecentlyClosedDocs, Doc, null), icon: "archive", click: 'selectMainMenu(self)' },
- { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', watchedDocuments: doc.mySharedDocs as Doc },
+ { title: "Shared with me", target: Cast(doc.mySharedDocs, Doc, null), icon: "users", click: 'selectMainMenu(self)', badgeValue},
{ title: "Trails", target: Cast(doc.myTrails, Doc, null), icon: "pres-trail", click: 'selectMainMenu(self)' },
{ title: "User Doc", target: Cast(doc.myUserDoc, Doc, null), icon: "address-card", click: 'selectMainMenu(self)', hidden: "IsNoviceMode()" },
];
@@ -370,8 +370,9 @@ export class CurrentUserUtils {
static async setupMenuPanel(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
if (doc.menuStack === undefined) {
- await this.setupSharingSidebar(doc, sharingDocumentId, linkDatabaseId); // sets up the right sidebar collection for mobile upload documents and sharing
- const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, watchedDocuments, hidden }) =>
+ await this.setupLinkDocs(doc, linkDatabaseId);
+ await this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
+ const menuBtns = CurrentUserUtils.menuBtnDescriptions(doc).map(({ title, target, icon, click, badgeValue, hidden }) =>
Docs.Create.FontIconDocument({
icon,
btnType: ButtonType.MenuButton,
@@ -382,12 +383,13 @@ export class CurrentUserUtils {
dontUndo: true,
title,
target,
+ dontRegisterView: true,
hidden: hidden ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined,
_dropAction: "alias",
_removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
_width: 60,
_height: 60,
- watchedDocuments,
+ badgeValue,
onClick: ScriptField.MakeScript(click, { scriptContext: "any" })
})
);
@@ -400,6 +402,7 @@ export class CurrentUserUtils {
_chromeHidden: true,
backgroundColor: Colors.DARK_GRAY,
boxShadow: "rgba(0,0,0,0)",
+ dontRegisterView: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
ignoreClick: true,
_gridGap: 0,
@@ -720,7 +723,7 @@ export class CurrentUserUtils {
];
doc.dockedBtns = CurrentUserUtils.linearButtonList({
title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true,
- linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
+ childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
}, btnDescs.map(desc => doc[`dockedBtn-${desc.title}`] as Doc ?? (doc[`dockedBtn-${desc.title}`] = dockBtn({ title: desc.title, ...desc.opts() }))));
}
}
@@ -834,12 +837,13 @@ export class CurrentUserUtils {
});
if (doc.contextMenuBtns === undefined) {
doc.contextMenuBtns = CurrentUserUtils.linearButtonList(
- { title: "menu buttons", flexGap: 0, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 },
+ { title: "menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 },
CurrentUserUtils.contextMenuTools(doc).map(params =>
!params.subMenu ?
btnFunc(params) :
CurrentUserUtils.linearButtonList({
title: params.title,
+ childDontRegisterViews: true,
linearViewSubMenu: true, flexGap: 0, ignoreClick: true,
linearViewExpandable: true, icon: params.title, _height: 30,
linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined,
@@ -855,6 +859,7 @@ export class CurrentUserUtils {
btnFunc(params) :
CurrentUserUtils.linearButtonList({
title: params.title,
+ childDontRegisterViews: true,
linearViewSubMenu: true, flexGap: 0, ignoreClick: true,
linearViewExpandable: true, icon: params.title, _height: 30,
linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined,
@@ -897,10 +902,7 @@ export class CurrentUserUtils {
}
// Sharing sidebar is where shared documents are contained
- static async setupSharingSidebar(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
- if (doc.myPublishedDocs === undefined) {
- doc.myPublishedDocs = new List<Doc>();
- }
+ static async setupLinkDocs(doc: Doc, linkDatabaseId: string) {
if (doc.myLinkDatabase === undefined) {
let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
if (!linkDocs) {
@@ -912,29 +914,41 @@ export class CurrentUserUtils {
}
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
- // TODO:glr NOTE: treeViewHideTitle & _showTitle may be confusing, treeViewHideTitle is for the editable title (just for tree view), _showTitle is to show the Document title for any document
- if (doc.mySharedDocs === undefined) {
- let sharedDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(sharingDocumentId + "outer");
- if (!sharedDocs) {
- sharedDocs = Docs.Create.TreeDocument([], {
- title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
- _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- _chromeHidden: true, boxShadow: "0 0",
- explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
- }, sharingDocumentId + "outer", sharingDocumentId);
- (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment;
- }
- if (sharedDocs instanceof Doc) {
- Doc.GetProto(sharedDocs).userColor = sharedDocs.userColor || "rgb(202, 202, 202)";
- const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
- const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
- sharedDocs.childContextMenuFilters = new List<ScriptField>([dashboardFilter!,]);
- sharedDocs.childContextMenuScripts = new List<ScriptField>([addToDashboards!,]);
- sharedDocs.childContextMenuLabels = new List<string>(["Add to Dashboards",]);
- sharedDocs.childContextMenuIcons = new List<string>(["user-plus",]);
-
- }
- doc.mySharedDocs = new PrefetchProxy(sharedDocs as Doc);
+ }
+ // A user's sharing document is where all documents that are shared to that user are placed.
+ // When the user views one of these documents, it will be added to the sharing documents 'viewed' list field
+ // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents
+ static async setupSharedDocs(doc: Doc, sharingDocumentId: string) {
+ const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
+ const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
+ const dblClkScript = ScriptField.MakeScript("{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}", {scriptContext:"any", documentView:Doc.name})
+
+ const sharedDocOpts:DocumentOptions = {
+ title: "My Shared Docs",
+ userColor: "rgb(202, 202, 202)",
+ childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
+ childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
+ childContextMenuLabels: new List<string>(["Add to Dashboards",]),
+ childContextMenuIcons: new List<string>(["user-plus",]),
+ treeViewChildDoubleClick: dblClkScript,
+ };
+ const sharedRequiredDocOpts:DocumentOptions = {
+ "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
+ childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar
+ _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
+ };
+
+ const sharedDocs = Docs.newAccount ? undefined : DocCast(doc.mySharedDocs) ?? DocCast(await DocServer.GetRefField(sharingDocumentId + "outer"));
+ if (!(sharedDocs instanceof Doc)) {
+ doc.mySharedDocs = new PrefetchProxy(
+ Docs.Create.TreeDocument([], {...sharedDocOpts, ...sharedRequiredDocOpts}, sharingDocumentId + "outer", sharingDocumentId));
+ } else {
+ Object.entries(sharedRequiredDocOpts).forEach(pair => {
+ const targetDoc = pair[0].startsWith("_") ? sharedDocs as Doc : Doc.GetProto(sharedDocs as Doc);
+ targetDoc[pair[0]] = pair[1];
+ });
}
}
@@ -945,7 +959,7 @@ export class CurrentUserUtils {
doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], {
title: "My Imports", _forceActive: true, buttonMenu: true, buttonMenuDoc: newImportButton, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0,
childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true,
- explainer: "This is where documents that are Imported into Dash will go."
+ dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go."
}));
}
}
@@ -954,7 +968,7 @@ export class CurrentUserUtils {
static setupSearchSidebar(doc: Doc) {
if (doc.mySearchPanel === undefined) {
doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({
- backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true,
+ dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true,
childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true
})) as any as Doc;
}
@@ -1042,6 +1056,7 @@ export class CurrentUserUtils {
doc.savedFilters = new List<Doc>();
doc.filterDocCount = 0;
doc.freezeChildren = "remove|add";
+ doc.myPublishedDocs = doc.myPublishedDocs ?? new List<Doc>();
doc.myHeaderBarDoc = doc.myHeaderBarDoc ?? Docs.Create.MulticolumnDocument([], { title: "header bar", system: true });
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
@@ -1094,6 +1109,7 @@ export class CurrentUserUtils {
Docs.newAccount = !(field instanceof Doc);
await Docs.Prototypes.initialize();
const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
+ Docs.newAccount &&(userDoc.activePage = "home");
const updated = this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
(await DocListCastAsync(Cast(Doc.UserDoc().myLinkDatabase, Doc, null)?.data))?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager
const a1 = await Cast(link?.anchor1, Doc, null);
@@ -1111,6 +1127,9 @@ export class CurrentUserUtils {
public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
+ if (!DocListCast(CurrentUserUtils.MyDashboards.data).includes(doc)) {
+ Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc);
+ }
if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard
!("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]));
@@ -1184,18 +1203,42 @@ export class CurrentUserUtils {
input.click();
}
+ public static CaptureDashboardThumbnail() {
+ const docView = CollectionDockingView.Instance.props.DocumentView?.();
+ const content = docView?.ContentDiv;
+ if (docView && content) {
+ const _width = Number(getComputedStyle(content).width.replace("px",""));
+ const _height = Number(getComputedStyle(content).height.replace("px",""));
+ return CollectionFreeFormView.UpdateIcon(
+ docView.layoutDoc[Id] + "-icon" + (new Date()).getTime(),
+ content,
+ _width, _height,
+ _width, _height, 0, 1, true, docView.layoutDoc[Id] + "-icon",
+ (iconFile, _nativeWidth, _nativeHeight) => {
+ const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: docView.rootDoc.title+"-icon", _width, _height, _nativeWidth, _nativeHeight});
+ const proto = Cast(img.proto, Doc, null)!;
+ proto["data-nativeWidth"] = _width;
+ proto["data-nativeHeight"] = _height;
+ Doc.GetProto(CurrentUserUtils.ActiveDashboard).thumb = img;
+ });
+ }
+
+ }
+
public static async snapshotDashboard(userDoc: Doc) {
const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard);
Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy);
CurrentUserUtils.openDashboard(userDoc, copy);
}
+ public static closeActiveDashboard = () => {
+ Doc.UserDoc().activeDashboard = undefined;
+ }
+
public static createNewDashboard = async (userDoc: Doc, id?: string) => {
const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true);
const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc;
const dashboardCount = DocListCast(dashboards.data).length + 1;
- const emptyPane = Cast(userDoc.emptyPane, Doc, null);
- emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
const freeformOptions: DocumentOptions = {
x: 0,
y: 400,
@@ -1203,7 +1246,7 @@ export class CurrentUserUtils {
_height: 1000,
_fitWidth: true,
_backgroundGridShow: true,
- title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
+ title: `Untitled Tab 1`,
};
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row");
@@ -1212,11 +1255,12 @@ export class CurrentUserUtils {
// switching the tabs from the datadoc to the regular doc
const dashboardTabs = DocListCast(dashboardDoc[DataSym].data);
dashboardDoc.data = new List<Doc>(dashboardTabs);
+ dashboardDoc["pane-count"] = 1;
userDoc.activePresentation = presentation;
Doc.AddDocToList(dashboards, "data", dashboardDoc);
- CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
+ // CurrentUserUtils.openDashboard(userDoc, dashboardDoc);
}
public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 9f8c49081..dbfba8992 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -455,7 +455,7 @@ export namespace DragManager {
if (dragData instanceof DocumentDragData) {
dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction;
}
- if (((e.target as any)?.className === "lm_tabs" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
+ if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
if (!startWindowDragTimer) {
startWindowDragTimer = setTimeout(async () => {
startWindowDragTimer = undefined;
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index e6f75a7db..632348306 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -58,8 +58,10 @@ export namespace HistoryUtil {
export function getState(): ParsedUrl {
const state = copyState(history.state);
- state.initializers = state.initializers || {};
- return state;
+ if (state) {
+ state.initializers = state.initializers || {};
+ }
+ return state ?? {initializers:{}};
}
// export function addHandler(handler: (state: ParsedUrl | null) => void) {
diff --git a/src/client/util/RecordingApi.ts b/src/client/util/RecordingApi.ts
new file mode 100644
index 000000000..021feee9a
--- /dev/null
+++ b/src/client/util/RecordingApi.ts
@@ -0,0 +1,269 @@
+import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
+import { IReactionDisposer, observable, reaction } from "mobx";
+import { NumCast } from "../../fields/Types";
+import { Doc } from "../../fields/Doc";
+import { VideoBox } from "../views/nodes/VideoBox";
+import { scaleDiverging } from "d3-scale";
+import { Transform } from "./Transform";
+
+type Movement = {
+ time: number,
+ panX: number,
+ panY: number,
+ scale: number,
+}
+
+type Presentation = {
+ movements: Array<Movement> | null
+ meta: Object,
+}
+
+export class RecordingApi {
+
+ private static NULL_PRESENTATION: Presentation = {
+ movements: null,
+ meta: {},
+ }
+
+ // instance variables
+ private currentPresentation: Presentation;
+ private isRecording: boolean;
+ private absoluteStart: number;
+
+
+ // create static instance and getter for global use
+ @observable static _instance: RecordingApi;
+ public static get Instance(): RecordingApi { return RecordingApi._instance }
+ public constructor() {
+ // init the global instance
+ RecordingApi._instance = this;
+
+ // init the instance variables
+ this.currentPresentation = RecordingApi.NULL_PRESENTATION
+ this.isRecording = false;
+ this.absoluteStart = -1;
+
+ // used for tracking movements in the view frame
+ this.disposeFunc = null;
+ this.recordingFFView = null;
+
+ // for now, set playFFView
+ this.playFFView = null;
+ this.timers = null;
+ }
+
+ // little helper :)
+ private get isInitPresenation(): boolean {
+ return this.currentPresentation.movements === null
+ }
+
+ public start = (meta?: Object): Error | undefined => {
+ // check if already init a presentation
+ if (!this.isInitPresenation) {
+ console.error('[recordingApi.ts] start() failed: current presentation data exists. please call clear() first.')
+ return new Error('[recordingApi.ts] start()')
+ }
+
+ // update the presentation mode
+ Doc.UserDoc().presentationMode = 'recording'
+
+ // (1a) get start date for presenation
+ const startDate = new Date()
+ // (1b) set start timestamp to absolute timestamp
+ this.absoluteStart = startDate.getTime()
+
+ // (2) assign meta content if it exists
+ this.currentPresentation.meta = meta || {}
+ // (3) assign start date to currentPresenation
+ this.currentPresentation.movements = []
+ // (4) set isRecording true to allow trackMovements
+ this.isRecording = true
+ }
+
+ public clear = (): Error | Presentation => {
+ // TODO: maybe archive the data?
+ if (this.isRecording) {
+ console.error('[recordingApi.ts] clear() failed: currently recording presentation. call pause() first')
+ return new Error('[recordingApi.ts] clear()')
+ }
+
+ // update the presentation mode
+ Doc.UserDoc().presentationMode = 'none'
+ // set the previus recording view to the play view
+ this.playFFView = this.recordingFFView
+
+ const presCopy = { ...this.currentPresentation }
+
+ // clear presenation data
+ this.currentPresentation = RecordingApi.NULL_PRESENTATION
+ // clear isRecording
+ this.isRecording = false
+ // clear absoluteStart
+ this.absoluteStart = -1
+ // clear the disposeFunc
+ this.removeRecordingFFView()
+
+ return presCopy;
+ }
+
+ public pause = (): Error | undefined => {
+ if (this.isInitPresenation) {
+ console.error('[recordingApi.ts] pause() failed: no presentation started. try calling init() first')
+ return new Error('[recordingApi.ts] pause(): no presentation')
+ }
+ // don't allow track movments
+ this.isRecording = false
+
+ // set adjust absoluteStart to add the time difference
+ const timestamp = new Date().getTime()
+ this.absoluteStart = timestamp - this.absoluteStart
+ }
+
+ public resume = () => {
+ this.isRecording = true
+ // set absoluteStart to the difference in time
+ this.absoluteStart = new Date().getTime() - this.absoluteStart
+ }
+
+ private trackMovements = (panX: number, panY: number, scale: number = 0): Error | undefined => {
+ // ensure we are recording
+ if (!this.isRecording) {
+ return new Error('[recordingApi.ts] trackMovements()')
+ }
+ // check to see if the presetation is init
+ if (this.isInitPresenation) {
+ return new Error('[recordingApi.ts] trackMovements(): no presentation')
+ }
+
+ // get the time
+ const time = new Date().getTime() - this.absoluteStart
+ // make new movement object
+ const movement: Movement = { time, panX, panY, scale }
+
+ // add that movement to the current presentation data's movement array
+ this.currentPresentation.movements && this.currentPresentation.movements.push(movement)
+ }
+
+ // instance variable for the FFView
+ private disposeFunc: IReactionDisposer | null;
+ private recordingFFView: CollectionFreeFormView | null;
+
+ // set the FFView that will be used in a reaction to track the movements
+ public setRecordingFFView = (view: CollectionFreeFormView): void => {
+ // set the view to the current view
+ if (view === this.recordingFFView || view == null) return;
+
+ // this.recordingFFView = view;
+ // set the reaction to track the movements
+ this.disposeFunc = reaction(
+ () => ({ x: NumCast(view.Document.panX, -1), y: NumCast(view.Document.panY, -1), scale: NumCast(view.Document.viewScale, -1) }),
+ (res) => (res.x !== -1 && res.y !== -1 && this.isRecording) && this.trackMovements(res.x, res.y, res.scale)
+ )
+
+ // for now, set the most recent recordingFFView to the playFFView
+ this.recordingFFView = view;
+ }
+
+ // call on dispose function to stop tracking movements
+ public removeRecordingFFView = (): void => {
+ this.disposeFunc?.();
+ this.disposeFunc = null;
+ }
+
+ // TODO: extract this into different class with pause and resume recording
+ // TODO: store the FFview with the movements
+ private playFFView: CollectionFreeFormView | null;
+ private timers: NodeJS.Timeout[] | null;
+
+ public setPlayFFView = (view: CollectionFreeFormView): void => {
+ this.playFFView = view
+ }
+
+ // pausing movements will dispose all timers that are planned to replay the movements
+ // play movemvents will recreate them when the user resumes the presentation
+ public pauseMovements = (): undefined | Error => {
+ if (this.playFFView === null) {
+ return new Error('[recordingApi.ts] pauseMovements() failed: no view')
+ }
+
+ if (!this._isPlaying) {
+ //return new Error('[recordingApi.ts] pauseMovements() failed: not playing')
+ return
+ }
+ this._isPlaying = false
+ // TODO: set userdoc presentMode to browsing
+ this.timers?.map(timer => clearTimeout(timer))
+
+ // this.videoBox = null;
+ }
+
+ private videoBox: VideoBox | null = null;
+
+ // by calling pause on the VideoBox, the pauseMovements will be called
+ public pauseVideoAndMovements = (): boolean => {
+ this.videoBox?.Pause()
+
+ this.pauseMovements()
+ return this.videoBox == null
+ }
+
+ public _isPlaying = false;
+
+ public playMovements = (presentation: Presentation, timeViewed: number = 0, videoBox?: VideoBox): undefined | Error => {
+ if (presentation.movements === null || this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data or no view')
+ }
+ if(this._isPlaying) return;
+
+ this._isPlaying = true;
+ Doc.UserDoc().presentationMode = 'watching';
+
+ // TODO: consider this bug at the end of the clip on seek
+ this.videoBox = videoBox || null;
+
+ // only get the movements that are remaining in the video time left
+ const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+
+ // helper to replay a movement
+ const document = this.playFFView
+ let preScale = -1;
+ const zoomAndPan = (movement: Movement) => {
+ const { panX, panY, scale } = movement;
+ (scale !== -1 && preScale !== scale) && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
+ document.Document._panX = panX;
+ document.Document._panY = panY;
+
+ preScale = scale;
+ }
+
+ // set the first frame to be at the start of the pres
+ zoomAndPan(filteredMovements[0]);
+
+ // make timers that will execute each movement at the correct replay time
+ this.timers = filteredMovements.map(movement => {
+ const timeDiff = movement.time - timeViewed*1000
+ return setTimeout(() => {
+ // replay the movement
+ zoomAndPan(movement)
+ // if last movement, presentation is done -> set the instance var
+ if (movement === filteredMovements[filteredMovements.length - 1]) RecordingApi.Instance._isPlaying = false;
+ }, timeDiff)
+ })
+ }
+
+ // Unfinished code for tracing multiple free form views
+ // export let pres: Map<CollectionFreeFormView, IReactionDisposer> = new Map()
+
+ // export function AddRecordingFFView(ffView: CollectionFreeFormView): void {
+ // pres.set(ffView,
+ // reaction(() => ({ x: ffView.panX, y: ffView.panY }),
+ // (pt) => RecordingApi.trackMovements(ffView, pt.x, pt.y)))
+ // )
+ // }
+
+ // export function RemoveRecordingFFView(ffView: CollectionFreeFormView): void {
+ // const disposer = pres.get(ffView);
+ // disposer?.();
+ // pres.delete(ffView)
+ // }
+}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 3b0a47b54..223b0268c 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -223,7 +223,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
paramList.push(`${key}: ${typeof val === "object" ? Object.getPrototypeOf(val).constructor.name : typeof val}`);
}
const paramString = paramList.join(", ");
- const body = addReturn ? `return ${script};` : `return ${script};`;
+ const body = addReturn ? `return ${script};` : script;
const reqTypes = requiredType ? `: ${requiredType}` : '';
const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
host.writeFile("file.ts", funcScript);
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
new file mode 100644
index 000000000..67587dd2b
--- /dev/null
+++ b/src/client/views/DashboardView.scss
@@ -0,0 +1,48 @@
+.dashboard-view {
+ padding: 50px;
+ display: flex;
+ flex-direction: row;
+
+ .left-menu {
+ display: flex;
+ flex-direction: column;
+ width: 300px;
+ }
+
+ .all-dashboards {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ overflow-y: scroll;
+ }
+}
+
+.text-button {
+ padding: 10px 0;
+ &:hover {
+ font-weight: 500;
+ }
+
+ &.selected {
+ font-weight: 700;
+ }
+}
+
+.dashboard-container {
+ border-radius: 10px;
+ width: 250px;
+ border: solid .5px grey;
+ display: flex;
+ flex-direction: column;
+ margin: 0 30px 30px 30px;
+ overflow: hidden;
+
+ &:hover {
+ border: solid 1.5px grey;
+ }
+
+ .title {
+ margin: 10px;
+ font-weight: 500;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
new file mode 100644
index 000000000..efc1644fe
--- /dev/null
+++ b/src/client/views/DashboardView.tsx
@@ -0,0 +1,69 @@
+import { action, observable } from "mobx";
+import { extname } from 'path';
+import { observer } from "mobx-react";
+import * as React from 'react';
+import { Doc, DocListCast } from "../../fields/Doc";
+import { Id } from "../../fields/FieldSymbols";
+import { Cast, ImageCast, StrCast } from "../../fields/Types";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
+import { UndoManager } from "../util/UndoManager";
+import "./DashboardView.scss"
+
+enum DashboardGroup {
+ MyDashboards, SharedDashboards
+}
+
+@observer
+export class DashboardView extends React.Component {
+
+ //TODO: delete dashboard, share dashboard, etc.
+
+ @observable
+ private selectedDashboardGroup = DashboardGroup.MyDashboards;
+
+ @action
+ selectDashboardGroup = (group: DashboardGroup) => {
+ this.selectedDashboardGroup = group
+ }
+
+ newDashboard = async () => {
+ const batch = UndoManager.StartBatch("new dash");
+ await CurrentUserUtils.createNewDashboard(Doc.UserDoc());
+ batch.end();
+ }
+
+ clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => {
+ if (e.detail === 2) {
+ Doc.UserDoc().activeDashboard = dashboard;
+ Doc.UserDoc().activePage = "dashboard";
+ }
+ }
+
+ getDashboards = () => {
+ const allDashbaords = DocListCast(CurrentUserUtils.MyDashboards.data);
+ // TODO: filter the dashboards
+ // return allDashbaords.filter(...)
+ return allDashbaords
+ }
+
+ render() {
+ return <div className="dashboard-view">
+ <div className="left-menu">
+ <div className="text-button" onClick={this.newDashboard}>New</div>
+ <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`}onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards) }>My Dashboards</div>
+ <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards) }>Shared Dashboards</div>
+ </div>
+ <div className="all-dashboards">
+ {this.getDashboards().map((dashboard) => {
+ const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href;
+ return <div className="dashboard-container" key={dashboard[Id]} onClick={e => this.clickDashboard(e, dashboard)}>
+ <img src={href ?? "https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU="}></img>
+ <div className="title"> {StrCast(dashboard.title)} </div>
+ </div>
+
+ })}
+
+ </div>
+ </div>
+ }
+}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 9c176a4fd..8486b3d62 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -60,12 +60,9 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
@computed get layoutDoc() { return Doc.Layout(this.props.Document); }
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
-
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
- lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc }).result;
-
isContentActive = (outsideReaction?: boolean) => (
this.props.isContentActive?.() === false ? false :
(CurrentUserUtils.SelectedTool !== InkTool.None ||
@@ -95,7 +92,6 @@ export interface ViewBoxAnnotatableProps {
export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() {
class Component extends Touchable<P> {
@observable _annotationKeySuffix = () => "annotations";
-
@observable _isAnyChildContentActive = false;
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document() { return this.props.Document; }
@@ -187,7 +183,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
const targetDataDoc = this.props.Document[DataSym];
const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
const added = docs.filter(d => !docList.includes(d));
- const effectiveAcl = GetEffectiveAcl(this.dataDoc);
+ const effectiveAcl = GetEffectiveAcl(targetDataDoc);
if (added.length) {
if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 25297e167..0bbe473d7 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -204,58 +204,13 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div className="dash-tooltip">{SelectionManager.Views().length > 1 ? "Pin multiple documents to presentation" : "Pin to presentation"}</div>}>
<div className="documentButtonBar-icon"
style={{ color: "white" }}
- onClick={undoBatch(e => this.props.views().map(view => view && this.pinWithView(view.props.Document)))}>
+ onClick={(e => TabDocView.PinDoc(this.props.views().filter(v => v).map(dv => dv!.rootDoc), {pinDocView: true}))}
+ >
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
</div>
</Tooltip>;
}
- @undoBatch
- @action
- pinWithView = (targetDoc: Doc) => {
- if (targetDoc) {
- TabDocView.PinDoc(targetDoc);
- setTimeout(() => {
- const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetDoc.type as any) || targetDoc._viewType === CollectionViewType.Stacking;
- const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG);
- if (scrollable) {
- const scroll = targetDoc._scrollTop;
- activeDoc.presPinView = true;
- activeDoc.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeDoc.presPinView = true;
- activeDoc.presStartTime = targetDoc._currentTimecode;
- activeDoc.presEndTime = NumCast(targetDoc._currentTimecode) + 0.1;
- activeDoc.presPinTimecode = targetDoc._currentTimecode;
- } else if (pannable) {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeDoc.presPinView = true;
- activeDoc.presPinViewX = x;
- activeDoc.presPinViewY = y;
- activeDoc.presPinViewScale = scale;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const width = targetDoc._clipWidth;
- activeDoc.presPinClipWidth = width;
- activeDoc.presPinView = true;
- }
- });
- }
- }
-
- @computed
- get pinWithViewButton() {
- const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: "auto", width: 17, transform: 'translate(0, 1px)' }} />;
- const targetDoc = this.view0?.props.Document;
- return !targetDoc ? (null) : <Tooltip title={<><div className="dash-tooltip">{"Pin with current view"}</div></>}>
- <div className="documentButtonBar-icon" onClick={() => this.pinWithView(targetDoc)}>
- {presPinWithViewIcon}
- </div>
- </Tooltip>;
- }
-
@computed
get shareButton() {
const targetDoc = this.view0?.props.Document;
@@ -380,9 +335,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div className="documentButtonBar-button">
{this.pinButton}
</div>
- {/* <div className="documentButtonBar-button">
- {this.pinWithViewButton}
- </div> */}
{!Doc.UserDoc()["documentLinksButton-fullMenu"] ? (null) : <div className="documentButtonBar-button">
{this.shareButton}
</div>}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 29e088143..b666b5977 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -33,548 +33,549 @@ import React = require("react");
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> {
- static Instance: DocumentDecorations;
- private _resizeHdlId = "";
- private _keyinput = React.createRef<HTMLInputElement>();
- private _resizeBorderWidth = 16;
- private _linkBoxHeight = 20 + 3; // link button height + margin
- private _titleHeight = 20;
- private _resizeUndo?: UndoManager.Batch;
- private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border
- private _snapX = 0; _snapY = 0; // last snapped location of resize border
- private _dragHeights = new Map<Doc, { start: number, lowest: number }>();
- private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = [];
-
- @observable private _accumulatedTitle = "";
- @observable private _titleControlString: string = "#title";
- @observable private _edtingTitle = false;
- @observable private _hidden = false;
- @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set.
- @observable public Interacting = false;
- @observable public pushIcon: IconProp = "arrow-alt-circle-up";
- @observable public pullIcon: IconProp = "arrow-alt-circle-down";
- @observable public pullColor: string = "white";
-
- constructor(props: any) {
- super(props);
- DocumentDecorations.Instance = this;
- reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false));
- }
-
- @computed
- get Bounds() {
- const views = SelectionManager.Views();
- return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) =>
- !rect ? bounds :
- {
- x: Math.min(rect.left, bounds.x),
- y: Math.min(rect.top, bounds.y),
- r: Math.max(rect.right, bounds.r),
- b: Math.max(rect.bottom, bounds.b),
- c: views.length === 1 ? rect.center : undefined
- },
- { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) });
- }
-
- @action
- titleBlur = () => {
- this._edtingTitle = false;
- if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) {
- this._titleControlString = this._accumulatedTitle;
- } else if (this._titleControlString.startsWith("#")) {
- const titleFieldKey = this._titleControlString.substring(1);
- UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => {
- if (titleFieldKey === "title") {
- d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-");
- if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) {
- Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc);
- }
- if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) {
- Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc);
- }
- }
- //@ts-ignore
- const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle);
- Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
-
- if (d.rootDoc.syncLayoutFieldWithTitle) {
- const title = titleField.toString();
- const curKey = Doc.LayoutFieldKey(d.rootDoc);
- if (curKey !== title && d.dataDoc[title] === undefined) {
- d.rootDoc.layout = FormattedTextBox.LayoutString(title);
- setTimeout(() => {
- const val = d.dataDoc[curKey];
- d.dataDoc[curKey] = undefined;
- d.dataDoc[title] = val;
- });
- }
- }
- }), "title blur");
- }
- }
-
- titleEntered = (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
- e.stopPropagation();
- (e.target as any).blur();
- }
- }
-
- @action onTitleDown = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => {
- !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString);
- this._edtingTitle = true;
- this._keyinput.current && setTimeout(this._keyinput.current.focus);
- }));
- }
-
- onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction);
-
- @action
- onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
- const dragDocView = SelectionManager.Views()[0];
- const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 };
- const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction);
- dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top);
- dragData.moveDocument = dragDocView.props.moveDocument;
- dragData.isDocDecorationMove = true;
- dragData.canEmbed = dragTitle;
- this._hidden = this.Interacting = true;
- DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, {
- dragComplete: action(e => {
- dragData.canEmbed && SelectionManager.DeselectAll();
- this._hidden = this.Interacting = false;
- }),
- hideSource: true
- });
- return true;
- }
-
- _deleteAfterIconify = false;
- _iconifyBatch: UndoManager.Batch | undefined;
- onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
- if (this.canDelete) {
+ static Instance: DocumentDecorations;
+ private _resizeHdlId = "";
+ private _keyinput = React.createRef<HTMLInputElement>();
+ private _resizeBorderWidth = 16;
+ private _linkBoxHeight = 20 + 3; // link button height + margin
+ private _titleHeight = 20;
+ private _resizeUndo?: UndoManager.Batch;
+ private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border
+ private _snapX = 0; _snapY = 0; // last snapped location of resize border
+ private _dragHeights = new Map<Doc, { start: number, lowest: number }>();
+ private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = [];
+
+ @observable private _accumulatedTitle = "";
+ @observable private _titleControlString: string = "#title";
+ @observable private _edtingTitle = false;
+ @observable private _hidden = false;
+ @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set.
+ @observable public Interacting = false;
+ @observable public pushIcon: IconProp = "arrow-alt-circle-up";
+ @observable public pullIcon: IconProp = "arrow-alt-circle-down";
+ @observable public pullColor: string = "white";
+
+ constructor(props: any) {
+ super(props);
+ DocumentDecorations.Instance = this;
+ reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false));
+ }
+
+ @computed
+ get Bounds() {
+ const views = SelectionManager.Views();
+ return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) =>
+ !rect ? bounds :
+ {
+ x: Math.min(rect.left, bounds.x),
+ y: Math.min(rect.top, bounds.y),
+ r: Math.max(rect.right, bounds.r),
+ b: Math.max(rect.bottom, bounds.b),
+ c: views.length === 1 ? rect.center : undefined
+ },
+ { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) });
+ }
+
+ @action
+ titleBlur = () => {
+ this._edtingTitle = false;
+ if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) {
+ this._titleControlString = this._accumulatedTitle;
+ } else if (this._titleControlString.startsWith("#")) {
+ const titleFieldKey = this._titleControlString.substring(1);
+ UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => {
+ if (titleFieldKey === "title") {
+ d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-");
+ if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) {
+ Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc);
+ }
+ if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) {
+ Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc);
+ }
+ }
+ //@ts-ignore
+ const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle);
+ Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
+
+ if (d.rootDoc.syncLayoutFieldWithTitle) {
+ const title = titleField.toString();
+ const curKey = Doc.LayoutFieldKey(d.rootDoc);
+ if (curKey !== title && d.dataDoc[title] === undefined) {
+ d.rootDoc.layout = FormattedTextBox.LayoutString(title);
+ setTimeout(() => {
+ const val = d.dataDoc[curKey];
+ d.dataDoc[curKey] = undefined;
+ d.dataDoc[title] = val;
+ });
+ }
+ }
+ }), "title blur");
+ }
+ }
+
+ titleEntered = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.stopPropagation();
+ (e.target as any).blur();
+ }
+ }
+
+ @action onTitleDown = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => {
+ !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString);
+ this._edtingTitle = true;
+ this._keyinput.current && setTimeout(this._keyinput.current.focus);
+ }));
+ }
+
+ onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction);
+
+ @action
+ onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
+ const dragDocView = SelectionManager.Views()[0];
+ const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 };
+ const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction);
+ dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top);
+ dragData.moveDocument = dragDocView.props.moveDocument;
+ dragData.isDocDecorationMove = true;
+ dragData.canEmbed = dragTitle;
+ this._hidden = this.Interacting = true;
+ DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, {
+ dragComplete: action(e => {
+ dragData.canEmbed && SelectionManager.DeselectAll();
+ this._hidden = this.Interacting = false;
+ }),
+ hideSource: true
+ });
+ return true;
+ }
+
+ _deleteAfterIconify = false;
+ _iconifyBatch: UndoManager.Batch | undefined;
+ onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
+
const views = SelectionManager.Views().slice().filter(v => v);
if (forceDeleteOrIconify === false && this._iconifyBatch) return;
this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
if (!this._iconifyBatch) {
- this._iconifyBatch = UndoManager.StartBatch("iconifying");
+ this._iconifyBatch = UndoManager.StartBatch("iconifying");
} else {
- forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
+ forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
}
var iconifyingCount = views.length;
const finished = action((force?: boolean) => {
- if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
- if (this._deleteAfterIconify) {
- views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document));
- SelectionManager.DeselectAll();
- }
- this._iconifyBatch?.end();
- this._iconifyBatch = undefined;
- }
+ if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
+ if (this._deleteAfterIconify) {
+ views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document));
+ SelectionManager.DeselectAll();
+ }
+ this._iconifyBatch?.end();
+ this._iconifyBatch = undefined;
+ }
});
if (forceDeleteOrIconify) finished(forceDeleteOrIconify);
else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
- }
- }
- onMaximizeDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, () => {
- DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]);
- return true;
- }, emptyFunction, this.onMaximizeClick, false, false);
- }
-
- onMaximizeClick = (e: any): void => {
- const selectedDocs = SelectionManager.Views();
- if (selectedDocs.length) {
- if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key
- const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
- CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right");
- } else if (e.shiftKey) { // open centered in a new workspace with Shift Key
- const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
- alias.context = undefined;
- alias.x = -alias[WidthSym]() / 2;
- alias.y = -alias[HeightSym]() / 2;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right");
- } else if (e.altKey) { // open same document in new tab
- CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right");
- } else {
- var openDoc = selectedDocs[0].props.Document;
- if (openDoc.layoutKey === "layout_icon") {
- openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
- Doc.deiconifyView(openDoc);
- }
- LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document));
- }
- }
- SelectionManager.DeselectAll();
- }
-
- onIconifyClick = (): void => {
- SelectionManager.Views().forEach(dv => dv?.iconify());
- SelectionManager.DeselectAll();
- }
-
- onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false);
-
- onRadiusDown = (e: React.PointerEvent): void => {
- this._resizeUndo = UndoManager.StartBatch("DocDecs set radius");
- setupMoveUpEvents(this, e, (e, down) => {
- const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1]));
- SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)).
- map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`);
- return false;
- }, (e) => this._resizeUndo?.end(), (e) => { });
- }
-
- @action
- onRotateDown = (e: React.PointerEvent): void => {
- const rotateUndo = UndoManager.StartBatch("rotatedown");
- const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
- const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 };
- setupMoveUpEvents(this, e,
- (e: PointerEvent, down: number[], delta: number[]) => {
- const previousPoint = { X: e.clientX, Y: e.clientY };
- const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
- const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint);
- if (selectedInk.length) {
- angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint);
- } else {
- SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI);
- }
- return false;
- },
- () => {
- rotateUndo?.end();
- UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
- },
- emptyFunction);
- }
-
- @action
- onPointerDown = (e: React.PointerEvent): void => {
- DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc);
- this._inkDragDocs = DragManager.docsBeingDragged
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => {
- if (InkStrokeProperties.Instance._lock) {
- Doc.SetNativeHeight(doc, NumCast(doc._height));
- Doc.SetNativeWidth(doc, NumCast(doc._width));
- }
- return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) });
- });
- setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
- this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
- this._resizeHdlId = e.currentTarget.className;
- const bounds = e.currentTarget.getBoundingClientRect();
- this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX;
- this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY;
- this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
- this._snapX = e.pageX;
- this._snapY = e.pageY;
- const ffviewSet = new Set<CollectionFreeFormView>();
- SelectionManager.Views().forEach(docView => {
- const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- ffview && ffviewSet.add(ffview);
- this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) });
- });
- Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false));
- }
-
- onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
- const first = SelectionManager.Views()[0];
- let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
- var fixedAspect = Doc.NativeAspect(first.layoutDoc);
- InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
- .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
-
- const resizeHdl = this._resizeHdlId.split(" ")[0];
- if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles
- const project = (p: number[], a: number[], b: number[]) => {
- const atob = [b[0] - a[0], b[1] - a[1]];
- const atop = [p[0] - a[0], p[1] - a[1]];
- const len = atob[0] * atob[0] + atob[1] * atob[1];
- let dot = atop[0] * atob[0] + atop[1] * atob[1];
- const t = dot / len;
- dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
- return [a[0] + atob[0] * t, a[1] + atob[1] * t];
- };
- const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]);
- thisPt = DragManager.snapDragAspect(drag, fixedAspect);
- } else {
- thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY);
- }
-
- move[0] = thisPt.x - this._snapX;
- move[1] = thisPt.y - this._snapY;
- this._snapX = thisPt.x;
- this._snapY = thisPt.y;
- let dragBottom = false, dragRight = false, dragBotRight = false;
- let dX = 0, dY = 0, dW = 0, dH = 0;
- switch (this._resizeHdlId.split(" ")[0]) {
- case "": break;
- case "documentDecorations-topLeftResizer":
- dX = -1;
- dY = -1;
- dW = -move[0];
- dH = -move[1];
- break;
- case "documentDecorations-topRightResizer":
- dW = move[0];
- dY = -1;
- dH = -move[1];
- break;
- case "documentDecorations-topResizer":
- dY = -1;
- dH = -move[1];
- dragBottom = true;
- break;
- case "documentDecorations-bottomLeftResizer":
- dX = -1;
- dW = -move[0];
- dH = move[1];
- break;
- case "documentDecorations-bottomRightResizer":
- dW = move[0];
- dH = move[1];
- dragBotRight = true;
- break;
- case "documentDecorations-bottomResizer":
- dH = move[1];
- dragBottom = true;
- break;
- case "documentDecorations-leftResizer":
- dX = -1;
- dW = -move[0];
- break;
- case "documentDecorations-rightResizer":
- dW = move[0];
- dragRight = true;
- break;
- }
-
- SelectionManager.Views().forEach(action((docView: DocumentView) => {
- if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(docView.rootDoc);
- const nwidth = docView.nativeWidth;
- const nheight = docView.nativeHeight;
- const docheight = doc._height || 0;
- const docwidth = doc._width || 0;
- const width = docwidth;
- let height = (docheight || (nheight / nwidth * width));
- height = !height || isNaN(height) ? 20 : height;
- const scale = docView.props.ScreenToLocalTransform().Scale;
- const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable;
- if (nwidth && nheight) {
- if (nwidth / nheight !== width / height && !dragBottom) {
- height = nheight / nwidth * width;
- }
- if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
- if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
- else dW = dH * nwidth / nheight;
- }
- }
- let actualdW = Math.max(width + (dW * scale), 20);
- let actualdH = Math.max(height + (dH * scale), 20);
- const fixedAspect = (nwidth && nheight && !doc._fitWidth);
- if (fixedAspect) {
- if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
- if (dragRight && modifyNativeDim) {
- doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
- } else {
- if (!doc._fitWidth) {
- actualdH = nheight / nwidth * actualdW;
- doc._height = actualdH;
- }
- else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
+ }
+ onMaximizeDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, () => {
+ DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]);
+ return true;
+ }, emptyFunction, this.onMaximizeClick, false, false);
+ }
+
+ onMaximizeClick = (e: any): void => {
+ const selectedDocs = SelectionManager.Views();
+ if (selectedDocs.length) {
+ if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key
+ const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
+ CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right");
+ } else if (e.shiftKey) { // open centered in a new workspace with Shift Key
+ const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
+ alias.context = undefined;
+ alias.x = -alias[WidthSym]() / 2;
+ alias.y = -alias[HeightSym]() / 2;
+ CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right");
+ } else if (e.altKey) { // open same document in new tab
+ CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right");
+ } else {
+ var openDoc = selectedDocs[0].props.Document;
+ if (openDoc.layoutKey === "layout_icon") {
+ openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
+ Doc.deiconifyView(openDoc);
}
- doc._width = actualdW;
- }
- else {
- if (dragBottom && (modifyNativeDim ||
- (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
- doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc);
- doc._autoHeight = false;
+ LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document));
+ }
+ }
+ SelectionManager.DeselectAll();
+ }
+
+ onIconifyClick = (): void => {
+ SelectionManager.Views().forEach(dv => dv?.iconify());
+ SelectionManager.DeselectAll();
+ }
+
+ onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false);
+
+ onRadiusDown = (e: React.PointerEvent): void => {
+ this._resizeUndo = UndoManager.StartBatch("DocDecs set radius");
+ setupMoveUpEvents(this, e, (e, down) => {
+ const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1]));
+ SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)).
+ map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`);
+ return false;
+ }, (e) => this._resizeUndo?.end(), (e) => { });
+ }
+
+ @action
+ onRotateDown = (e: React.PointerEvent): void => {
+ const rotateUndo = UndoManager.StartBatch("rotatedown");
+ const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
+ const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 };
+ setupMoveUpEvents(this, e,
+ (e: PointerEvent, down: number[], delta: number[]) => {
+ const previousPoint = { X: e.clientX, Y: e.clientY };
+ const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
+ const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint);
+ if (selectedInk.length) {
+ angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint);
} else {
- if (!doc._fitWidth) {
- actualdW = nwidth / nheight * actualdH;
- doc._width = actualdW;
- }
- else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
+ SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI);
}
- if (!modifyNativeDim) {
- actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH);
- doc._height = actualdH;
+ return false;
+ },
+ () => {
+ rotateUndo?.end();
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ },
+ emptyFunction);
+ }
+
+ @action
+ onPointerDown = (e: React.PointerEvent): void => {
+ DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc);
+ this._inkDragDocs = DragManager.docsBeingDragged
+ .filter(doc => doc.type === DocumentType.INK)
+ .map(doc => {
+ if (InkStrokeProperties.Instance._lock) {
+ Doc.SetNativeHeight(doc, NumCast(doc._height));
+ Doc.SetNativeWidth(doc, NumCast(doc._width));
}
- else doc._height = actualdH;
- }
- } else {
- dH && (doc._height = actualdH);
- dW && (doc._width = actualdW);
- dH && (doc._autoHeight = false);
- }
- doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
- doc.y = (doc.y || 0) + dY * (actualdH - docheight);
- doc._lastModified = new DateField();
- }
- const val = this._dragHeights.get(docView.layoutDoc);
- if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) });
- }));
- return false;
- }
-
- @action
- onPointerUp = (e: PointerEvent): void => {
- this._resizeHdlId = "";
- this.Interacting = false;
- this._resizeUndo?.end();
- SnappingManager.clearSnapLines();
-
- // detect autoHeight gesture and apply
- SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) }))
- .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20)
- .forEach(pair => pair.doc._autoHeight = true);
- //need to change points for resize, or else rotation/control points will fail.
- this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
- .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
- Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth)
- ({
- X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
- Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
- })));
- Doc.SetNativeWidth(doc, undefined);
- Doc.SetNativeHeight(doc, undefined);
+ return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) });
+ });
+
+ setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
+ this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+ this._resizeHdlId = e.currentTarget.className;
+ const bounds = e.currentTarget.getBoundingClientRect();
+ this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX;
+ this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY;
+ this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
+ this._snapX = e.pageX;
+ this._snapY = e.pageY;
+ const ffviewSet = new Set<CollectionFreeFormView>();
+ SelectionManager.Views().forEach(docView => {
+ const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ ffview && ffviewSet.add(ffview);
+ this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) });
});
- }
-
- @computed
- get selectionTitle(): string {
- if (SelectionManager.Views().length === 1) {
- const selected = SelectionManager.Views()[0];
- if (selected.ComponentView?.getTitle?.()) {
- return selected.ComponentView.getTitle();
+ Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false));
+ }
+
+ onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
+ const first = SelectionManager.Views()[0];
+ let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
+ var fixedAspect = Doc.NativeAspect(first.layoutDoc);
+ InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
+ .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
+
+ const resizeHdl = this._resizeHdlId.split(" ")[0];
+ if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles
+ const project = (p: number[], a: number[], b: number[]) => {
+ const atob = [b[0] - a[0], b[1] - a[1]];
+ const atop = [p[0] - a[0], p[1] - a[1]];
+ const len = atob[0] * atob[0] + atob[1] * atob[1];
+ let dot = atop[0] * atob[0] + atop[1] * atob[1];
+ const t = dot / len;
+ dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]);
+ return [a[0] + atob[0] * t, a[1] + atob[1] * t];
+ };
+ const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]);
+ thisPt = DragManager.snapDragAspect(drag, fixedAspect);
+ } else {
+ thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY);
}
- if (this._titleControlString.startsWith("=")) {
- return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || "";
+
+ move[0] = thisPt.x - this._snapX;
+ move[1] = thisPt.y - this._snapY;
+ this._snapX = thisPt.x;
+ this._snapY = thisPt.y;
+ let dragBottom = false, dragRight = false, dragBotRight = false;
+ let dX = 0, dY = 0, dW = 0, dH = 0;
+ switch (this._resizeHdlId.split(" ")[0]) {
+ case "": break;
+ case "documentDecorations-topLeftResizer":
+ dX = -1;
+ dY = -1;
+ dW = -move[0];
+ dH = -move[1];
+ break;
+ case "documentDecorations-topRightResizer":
+ dW = move[0];
+ dY = -1;
+ dH = -move[1];
+ break;
+ case "documentDecorations-topResizer":
+ dY = -1;
+ dH = -move[1];
+ dragBottom = true;
+ break;
+ case "documentDecorations-bottomLeftResizer":
+ dX = -1;
+ dW = -move[0];
+ dH = move[1];
+ break;
+ case "documentDecorations-bottomRightResizer":
+ dW = move[0];
+ dH = move[1];
+ dragBotRight = true;
+ break;
+ case "documentDecorations-bottomResizer":
+ dH = move[1];
+ dragBottom = true;
+ break;
+ case "documentDecorations-leftResizer":
+ dX = -1;
+ dW = -move[0];
+ break;
+ case "documentDecorations-rightResizer":
+ dW = move[0];
+ dragRight = true;
+ break;
+ }
+
+ SelectionManager.Views().forEach(action((docView: DocumentView) => {
+ if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
+ if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
+ const doc = Document(docView.rootDoc);
+ const nwidth = docView.nativeWidth;
+ const nheight = docView.nativeHeight;
+ const docheight = doc._height || 0;
+ const docwidth = doc._width || 0;
+ const width = docwidth;
+ let height = (docheight || (nheight / nwidth * width));
+ height = !height || isNaN(height) ? 20 : height;
+ const scale = docView.props.ScreenToLocalTransform().Scale;
+ const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable;
+ if (nwidth && nheight) {
+ if (nwidth / nheight !== width / height && !dragBottom) {
+ height = nheight / nwidth * width;
+ }
+ if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
+ if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
+ else dW = dH * nwidth / nheight;
+ }
+ }
+ let actualdW = Math.max(width + (dW * scale), 20);
+ let actualdH = Math.max(height + (dH * scale), 20);
+ const fixedAspect = (nwidth && nheight && !doc._fitWidth);
+ if (fixedAspect) {
+ if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
+ if (dragRight && modifyNativeDim) {
+ doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
+ } else {
+ if (!doc._fitWidth) {
+ actualdH = nheight / nwidth * actualdW;
+ doc._height = actualdH;
+ }
+ else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
+ }
+ doc._width = actualdW;
+ }
+ else {
+ if (dragBottom && (modifyNativeDim ||
+ (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
+ doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc);
+ doc._autoHeight = false;
+ } else {
+ if (!doc._fitWidth) {
+ actualdW = nwidth / nheight * actualdH;
+ doc._width = actualdW;
+ }
+ else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
+ }
+ if (!modifyNativeDim) {
+ actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH);
+ doc._height = actualdH;
+ }
+ else doc._height = actualdH;
+ }
+ } else {
+ dH && (doc._height = actualdH);
+ dW && (doc._width = actualdW);
+ dH && (doc._autoHeight = false);
+ }
+ doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
+ doc.y = (doc.y || 0) + dY * (actualdH - docheight);
+ doc._lastModified = new DateField();
+ }
+ const val = this._dragHeights.get(docView.layoutDoc);
+ if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) });
+ }));
+ return false;
+ }
+
+ @action
+ onPointerUp = (e: PointerEvent): void => {
+ this._resizeHdlId = "";
+ this.Interacting = false;
+ this._resizeUndo?.end();
+ SnappingManager.clearSnapLines();
+
+ // detect autoHeight gesture and apply
+ SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) }))
+ .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20)
+ .forEach(pair => pair.doc._autoHeight = true);
+ //need to change points for resize, or else rotation/control points will fail.
+ this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
+ .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => {
+ Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth)
+ ({
+ X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width,
+ Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height
+ })));
+ Doc.SetNativeWidth(doc, undefined);
+ Doc.SetNativeHeight(doc, undefined);
+ });
+ }
+
+ @computed
+ get selectionTitle(): string {
+ if (SelectionManager.Views().length === 1) {
+ const selected = SelectionManager.Views()[0];
+ if (selected.ComponentView?.getTitle?.()) {
+ return selected.ComponentView.getTitle();
+ }
+ if (this._titleControlString.startsWith("=")) {
+ return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || "";
+ }
+ if (this._titleControlString.startsWith("#")) {
+ return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-";
+ }
+ return this._accumulatedTitle;
}
- if (this._titleControlString.startsWith("#")) {
- return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-";
+ return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-";
+ }
+
+ @computed get hasIcons() {
+ return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon");
+ }
+
+ render() {
+ const bounds = this.Bounds;
+ const seldoc = SelectionManager.Views().slice(-1)[0];
+ if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ return (null);
}
- return this._accumulatedTitle;
- }
- return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-";
- }
-
- @computed get canDelete() {
- return SelectionManager.Views().some(docView => {
- if (docView.rootDoc.stayInCollection) return false;
- const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
- //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
- return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin);
- });
- }
- @computed get hasIcons() {
- return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon");
- }
-
- render() {
- const bounds = this.Bounds;
- const seldoc = SelectionManager.Views().slice(-1)[0];
- if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
- return (null);
- }
- const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup;
- const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle;
- const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton);
- const canDelete = this.canDelete;
- const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
- <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
- <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()}
- onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} >
- <FontAwesomeIcon icon={icon as any} />
- </div>
- </Tooltip>);
-
- const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
- const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} key="title" /> :
- this._edtingTitle ?
- <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
- type="text" name="dynbox" autoComplete="on"
- value={this._accumulatedTitle}
- onBlur={e => this.titleBlur()}
- onChange={action(e => this._accumulatedTitle = e.target.value)}
- onKeyDown={this.titleEntered} /> :
- <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} >
- <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
- </div>;
-
-
- const leftBounds = this.props.boundsLeft;
- const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop;
- bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
- bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
- const borderRadiusDraggerWidth = 15;
- bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
- bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
-
- const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox;
- const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : "";
-
- const rotation = NumCast(seldoc.rootDoc._jitterRotation);
-
- return (<div className={`documentDecorations${colorScheme}`}>
- <div className="documentDecorations-background" style={{
- transform: `rotate(${rotation}deg)`,
- transformOrigin: "top left",
- width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
- left: bounds.x - this._resizeBorderWidth / 2,
- top: bounds.y - this._resizeBorderWidth / 2,
- pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? "none" : "all",
- display: SelectionManager.Views().length <= 1 ? "none" : undefined
- }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} />
- {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <>
- <div className="documentDecorations-container" key="container" style={{
- transform: ` translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`,
- transformOrigin: `8px 26px`,
- width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
- }}>
- {!canDelete ? <div /> : topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")}
- {titleArea}
- {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
- {hideResizers ? (null) :
- <>
- <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="l" className={`documentDecorations-leftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="c" className={`documentDecorations-centerCont ${resizerScheme}`}></div>
- <div key="r" className={`documentDecorations-rightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="bl" className={`documentDecorations-bottomLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
- <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
-
- {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
- topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")}
- <div key="rot" className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`}
- onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown}
- onContextMenu={e => e.preventDefault()}>{useRotation && "⟲"}</div>
- </>
- }
- {seldoc?.Document.type === DocumentType.FONTICON ? (null) :
- <div className="link-button-container" key="links"
- style={{
- transform: ` translate(${- this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
- }}>
- <DocumentButtonBar views={SelectionManager.Views} />
- </div>}
- </div >
- </>}
- </div >
- );
- }
+ // hide the decorations if the parent chooses to hide it or if the document itself hides it
+ const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup;
+ const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle;
+ const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar;
+ // if multiple documents have been opened at the same time, then don't show open button
+ const hideOpenButton = seldoc.props.hideOpenButton || seldoc.rootDoc.hideOpenButton ||
+ SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton);
+ const hideDeleteButton = seldoc.props.hideDeleteButton || seldoc.rootDoc.hideDeleteButton ||
+ SelectionManager.Views().some(docView => {
+ const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
+ return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin);
+ });
+ const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
+ <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
+ <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()}
+ onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} >
+ <FontAwesomeIcon icon={icon as any} />
+ </div>
+ </Tooltip>);
+
+ const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
+ const titleArea = hideTitle ? (null) :
+ this._edtingTitle ?
+ <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
+ type="text" name="dynbox" autoComplete="on"
+ value={this._accumulatedTitle}
+ onBlur={e => this.titleBlur()}
+ onChange={action(e => this._accumulatedTitle = e.target.value)}
+ onKeyDown={this.titleEntered} /> :
+ <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} >
+ <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
+ </div>;
+
+
+ const leftBounds = this.props.boundsLeft;
+ const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop;
+ bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
+ bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
+ const borderRadiusDraggerWidth = 15;
+ bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
+ bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
+
+ const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox;
+ const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : "";
+
+ const rotation = NumCast(seldoc.rootDoc._jitterRotation);
+
+ return (<div className={`documentDecorations${colorScheme}`}>
+ <div className="documentDecorations-background" style={{
+ transform: `rotate(${rotation}deg)`,
+ transformOrigin: "top left",
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
+ left: bounds.x - this._resizeBorderWidth / 2,
+ top: bounds.y - this._resizeBorderWidth / 2,
+ pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? "none" : "all",
+ display: SelectionManager.Views().length <= 1 ? "none" : undefined
+ }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} />
+ {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <>
+ <div className="documentDecorations-container" key="container" style={{
+ transform: ` translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`,
+ transformOrigin: `8px 26px`,
+ width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
+ }}>
+ {hideDeleteButton ? <div /> : topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")}
+ {titleArea}
+ {hideOpenButton ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
+ {hideResizers ? (null) :
+ <>
+ <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="l" className={`documentDecorations-leftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="c" className={`documentDecorations-centerCont ${resizerScheme}`}></div>
+ <div key="r" className={`documentDecorations-rightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="bl" className={`documentDecorations-bottomLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+ <div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
+
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
+ topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")}
+ <div key="rot" className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`}
+ onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown}
+ onContextMenu={e => e.preventDefault()}>{useRotation && "⟲"}</div>
+ </>
+ }
+
+ {hideDocumentButtonBar ? (null) :
+ <div className="link-button-container" key="links"
+ style={{
+ transform: ` translate(${- this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `,
+ }}>
+ <DocumentButtonBar views={SelectionManager.Views} />
+ </div>}
+ </div >
+ </>}
+ </div >
+ );
+ }
} \ No newline at end of file
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index a118cc144..6afe64868 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -25,6 +25,7 @@ import { RadialMenu } from "./nodes/RadialMenu";
import HorizontalPalette from "./Palette";
import { Touchable } from "./Touchable";
import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
+import { InkTranscription } from "./InkTranscription";
@observer
export class GestureOverlay extends Touchable {
@@ -127,7 +128,7 @@ export class GestureOverlay extends Touchable {
// pen is also a touch, but with a radius of 0.5 (at least with the surface pens)
// and this seems to be the only way of differentiating pen and touch on touch events
if (pt.radiusX > 1 && pt.radiusY > 1) {
- createInkGroup();
+ InkTranscription.Instance.createInkGroup();
Doc.UserDoc().activeInkTool = InkTool.None;
this.prevPoints.set(pt.identifier, pt);
}
@@ -496,7 +497,7 @@ export class GestureOverlay extends Touchable {
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
setupMoveUpEvents(this, e, returnFalse, returnFalse, action((e: PointerEvent, doubleTap?: boolean) => {
if (doubleTap) {
- createInkGroup();
+ InkTranscription.Instance.createInkGroup();
CurrentUserUtils.SelectedTool = InkTool.None;
return;
}
@@ -644,7 +645,7 @@ export class GestureOverlay extends Touchable {
(controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y));
if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
this._points = controlPoints;
- this.dispatchGesture(GestureUtils.Gestures.Stroke);
+ this.dispatchGesture(GestureUtils.Gestures.Stroke);
// TODO: nda - check inks to group here
checkInksToGroup();
}
@@ -846,7 +847,7 @@ export class GestureOverlay extends Touchable {
@computed get elements() {
const selView = SelectionManager.Views().lastElement();
- const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc.viewScale, 1) / (selView?.props.ScreenToLocalTransform().Scale || 1);
+ const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1) / (selView?.props.ScreenToLocalTransform().Scale || 1);
const rect = this._overlayRef.current?.getBoundingClientRect();
const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true);
B.left = B.left - width / 2;
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index b15e4260d..ae35bc980 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -13,7 +13,6 @@ import { Colors } from "./global/globalEnums";
import { InkingStroke } from "./InkingStroke";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { DocumentView } from "./nodes/DocumentView";
-
export interface InkHandlesProps {
inkDoc: Doc;
inkView: DocumentView;
@@ -103,7 +102,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
r={screenSpaceLineWidth * 2}
fill={Colors.MEDIUM_BLUE}
strokeWidth={1}
- stroke={Colors.MEDIUM_BLUE}
+ stroke={Colors.BLACK}
onPointerDown={e => this.onHandleDown(e, pts.I)}
pointerEvents="all"
cursor="default"
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 0ac1770cc..8ab54918c 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -2,16 +2,20 @@ import * as iink from 'iink-js';
import { action, observable } from 'mobx';
import * as React from 'react';
import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc';
-import { InkData, InkField } from "../../fields/InkField";
+import { InkData, InkField, InkTool } from "../../fields/InkField";
import { Cast, DateCast, NumCast } from '../../fields/Types';
import { aggregateBounds } from '../../Utils';
import { DocumentType } from "../documents/DocumentTypes";
import { DocumentManager } from "../util/DocumentManager";
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { InkingStroke } from './InkingStroke';
-import './InkTranscription.scss';
+import { CurrentUserUtils } from '../util/CurrentUserUtils';
+import "./InkTranscription.scss";
+/**
+ * Class component that handles inking in writing mode
+ */
export class InkTranscription extends React.Component {
static Instance: InkTranscription;
@@ -42,8 +46,8 @@ export class InkTranscription extends React.Component {
protocol: 'WEBSOCKET',
server: {
host: 'cloud.myscript.com',
- applicationKey: '7277ec34-0c2e-4ee1-9757-ccb657e3f89f',
- hmacKey: 'f5cb18f2-1f95-4ddb-96ac-3f7c888dffc1',
+ applicationKey: process.env.IINKJS_APP,
+ hmacKey: process.env.IINKJS_HMAC,
websocket: {
pingEnabled: false,
autoReconnect: true
@@ -103,7 +107,15 @@ export class InkTranscription extends React.Component {
return this._textRef = r;
}
- transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean, ffView?: CollectionFreeFormView) => {
+ /**
+ * Handles processing Dash Doc data for ink transcription.
+ *
+ * @param groupDoc the group which contains the ink strokes we want to transcribe
+ * @param containingLayout the layout which contains the group
+ * @param inkDocs the ink docs contained within the selected group
+ * @param math boolean whether to do math transcription or not
+ */
+ transcribeInk = (groupDoc: Doc | undefined, containingLayout: Doc, inkDocs: Doc[], math: boolean) => {
if (!groupDoc) return;
const validInks = inkDocs.filter(s => s.type === DocumentType.INK);
@@ -129,6 +141,13 @@ export class InkTranscription extends React.Component {
}
}
+ /**
+ * Converts the Dash Ink Data to JSON.
+ *
+ * @param stroke The dash ink data
+ * @param time the time of the stroke
+ * @returns json object representation of ink data
+ */
inkJSON = (stroke: InkData, time: number) => {
return {
"pointerType": "PEN",
@@ -140,28 +159,17 @@ export class InkTranscription extends React.Component {
};
}
- mmToPixel = (mm: number) => {
- return ((96 * mm) / 25.4);
- }
-
- calcBounds = (coords: any) => {
- // find max and min x values and subtract
- const max = Math.max(...coords);
- const min = Math.min(...coords);
- return max - min;
- }
-
+ /**
+ * Creates subgroups for each word for the whole text transcription
+ * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs)
+ */
subgroupsTranscriptions = (wordInkDocMap: Map<string, Doc[]>) => {
- // loop through the words in wordInkDocMap
- // for each word, get the inkDocs
-
// iterate through the keys of wordInkDocMap
wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => {
const selected = inkDocs.slice();
if (!selected) {
return;
}
- // TODO: nda - probably have to cast this to an actual Doc
const ctx = await Cast(selected[0].context, Doc);
if (!ctx) {
return;
@@ -171,56 +179,16 @@ export class InkTranscription extends React.Component {
if (!docView) return;
const marqViewRef = docView._marqueeViewRef.current;
if (!marqViewRef) return;
- // 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);
-
- if (marqViewRef) {
- marqViewRef._downX = aggregBounds.x;
- marqViewRef._downY = aggregBounds.y;
- marqViewRef._lastX = aggregBounds.r;
- marqViewRef._lastY = aggregBounds.b;
- }
-
- // set the vals for bounds in marqueeView
-
- 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;
- }));
-
- docView.props.removeDocument?.(selected);
- const newCollection = marqViewRef?.getCollection(selected, undefined, true);
- if (newCollection) {
- newCollection.height = newCollection[HeightSym]();
- newCollection.width = newCollection[WidthSym]();
- newCollection.title = word;
- }
- // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
- newCollection && docView.props.addDocument?.(newCollection);
+ this.groupInkDocs(selected, docView, word);
});
}
+ /**
+ * Event listener function for when the 'exported' event is heard.
+ *
+ * @param e the event objects
+ * @param ref the ref to the editor
+ */
exportInk = (e: any, ref: any) => {
const exports = e.detail.exports;
if (exports) {
@@ -280,6 +248,88 @@ export class InkTranscription extends React.Component {
}
}
}
+
+/**
+ * Creates the ink grouping once the user leaves the writing mode.
+ */
+createInkGroup() {
+ // TODO nda - if document being added to is a inkGrouping then we can just add to that group
+ if (CurrentUserUtils.SelectedTool === 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;
+ const newCollection = this.groupInkDocs(selected, ffView);
+ ffView.unprocessedDocs = [];
+
+ InkTranscription.Instance.transcribeInk(newCollection, ffView.layoutDoc, selected, false);
+ });
+ }
+ CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
+}
+
+/**
+ * Creates the groupings for a given list of ink docs on a specific doc view
+ * @param selected: the list of ink docs to create a grouping of
+ * @param docView: the view in which we want the grouping to be created
+ * @param word: optional param if the group we are creating is a word (subgrouping individual words)
+ * @returns a new collection Doc or undefined if the grouping fails
+ */
+ groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined {
+ const bounds: { x: number, y: number, width?: number, height?: number }[] = []
+
+ // calculate the necessary bounds from the selected ink docs
+ 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 });
+ }))
+
+ // calculate the aggregated bounds
+ const aggregBounds = aggregateBounds(bounds, 0, 0);
+ const marqViewRef = docView._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;
+ }
+
+ // map through all the selected ink strokes and create the groupings
+ 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;
+ }));
+ docView.props.removeDocument?.(selected);
+ // Gets a collection based on the selected nodes using a marquee view ref
+ const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+ if (newCollection) {
+ newCollection.height = newCollection[HeightSym]();
+ newCollection.width = newCollection[WidthSym]();
+ // if the grouping we are creating is an individual word
+ if (word) {
+ newCollection.title = word;
+ }
+ }
+
+ // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
+ newCollection && docView.props.addDocument?.(newCollection);
+ return newCollection;
+}
render() {
return (
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index f67f37bfb..fcc4aea13 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -207,7 +207,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
future = () => LightboxView._future;
tourMap = () => LightboxView._tourMap;
- fitToBox = () => LightboxView._docTarget === LightboxView.LightboxDoc;
render() {
let downx = 0, downy = 0;
return !LightboxView.LightboxDoc ? (null) :
@@ -241,7 +240,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
DataDoc={undefined}
LayoutTemplate={LightboxView.LightboxDocTemplate}
addDocument={undefined}
- // fitContentsToDoc={this.fitToBox} // bcz: why do we want this? when we initially open a colletion, we shrinkwrap it which allows for user navigation. if we later encounter a collection, it's not clear to me that we want to make it either shrinkwrap or fitContents...
isDocumentActive={returnFalse}
isContentActive={returnTrue}
addDocTab={this.addDocTab}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 8560ccb29..49c2dcf34 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,7 +8,9 @@ import { AssignAllExtensions } from "../../extensions/General/Extensions";
import { Docs } from "../documents/Documents";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { LinkManager } from "../util/LinkManager";
+import { RecordingApi } from "../util/RecordingApi";
import { CollectionView } from "./collections/CollectionView";
+import { DashboardView } from './DashboardView';
import { MainView } from "./MainView";
AssignAllExtensions();
@@ -36,5 +38,6 @@ AssignAllExtensions();
const expires = "expires=" + d.toUTCString();
document.cookie = `loadtime=${loading};${expires};path=/`;
new LinkManager();
+ new RecordingApi;
ReactDOM.render(<MainView />, document.getElementById('root'));
})(); \ No newline at end of file
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 15cd2c144..a695577d0 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -20,6 +20,8 @@
position: relative;
width: 100%;
height: 100%;
+ display: flex;
+ flex-direction: column;
}
// add nodes menu. Note that the + button is actually an input label, not an actual button.
@@ -29,6 +31,11 @@
left: calc(100% + 5px);
z-index: 1;
pointer-events: none;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
}
.mainView-snapLines {
@@ -148,7 +155,8 @@
}
}
-.mainView-innerContent, .mainView-innerContent-Dark {
+.mainView-innerContent,
+.mainView-innerContent-Dark {
display: contents;
flex-direction: row;
position: relative;
@@ -181,8 +189,8 @@
.mainView-libraryHandle {
background-color: $light-gray;
}
-.mainView-innerContent-Dark
-{
+
+.mainView-innerContent-Dark {
.propertiesView {
background-color: #252525;
@@ -208,6 +216,7 @@
background: #353535;
}
}
+
.mainView-container-Dark {
.contextMenu-cont {
background: $medium-gray;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f0011d65f..0f7e3188a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -37,6 +37,7 @@ import { CollectionView, CollectionViewType } from './collections/CollectionView
import "./collections/TreeView.scss";
import { ComponentDecorations } from './ComponentDecorations';
import { ContextMenu } from './ContextMenu';
+import { DashboardView } from './DashboardView';
import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
@@ -83,7 +84,7 @@ export class MainView extends React.Component {
@computed private get dashboardTabHeight() { return 27; } // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
@computed private get topOfDashUI() { return Number(DASHBOARD_SELECTOR_HEIGHT.replace("px", "")); }
- @computed private get topOfHeaderBarDoc() { return this.topOfDashUI + this.topMenuHeight(); }
+ @computed private get topOfHeaderBarDoc() { return this.topOfDashUI; }
@computed private get topOfSidebarDoc() { return this.topOfDashUI + this.topMenuHeight(); }
@computed private get topOfMainDoc() { return this.topOfDashUI + this.topMenuHeight() + this.headerBarDocHeight(); }
@computed private get topOfMainDocContent() { return this.topOfMainDoc + this.dashboardTabHeight; }
@@ -179,7 +180,7 @@ export class MainView extends React.Component {
fa.faClone, fa.faCloudUploadAlt, fa.faCommentAlt, fa.faCompressArrowsAlt, fa.faCut, fa.faEllipsisV, fa.faEraser, fa.faExclamation, fa.faFileAlt,
fa.faFileAudio, fa.faFileVideo, fa.faFilePdf, fa.faFilm, fa.faFilter, fa.faFont, fa.faGlobeAmericas, fa.faGlobeAsia, fa.faHighlighter, fa.faLongArrowAltRight, fa.faMousePointer,
fa.faMusic, fa.faObjectGroup, fa.faPause, fa.faPen, fa.faPenNib, fa.faPhone, fa.faPlay, fa.faPortrait, fa.faRedoAlt, fa.faStamp, fa.faStickyNote, fa.faArrowsAltV,
- fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faFlag,
+ fa.faTimesCircle, fa.faThumbtack, fa.faTree, fa.faTv, fa.faUndoAlt, fa.faVideoSlash, fa.faVideo, fa.faAsterisk, fa.faBrain, fa.faImage, fa.faPaintBrush, fa.faTimes, fa.faFlag,
fa.faEye, fa.faArrowsAlt, fa.faQuoteLeft, fa.faSortAmountDown, fa.faAlignLeft, fa.faAlignCenter, fa.faAlignRight, fa.faHeading, fa.faRulerCombined,
fa.faFillDrip, fa.faLink, fa.faUnlink, fa.faBold, fa.faItalic, fa.faClipboard, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, fa.faSubscript,
fa.faIndent, fa.faEyeDropper, fa.faPaintRoller, fa.faBars, fa.faBrush, fa.faShapes, fa.faEllipsisH, fa.faHandPaper, fa.faMap, fa.faUser, faHireAHelper as any,
@@ -226,17 +227,7 @@ export class MainView extends React.Component {
if (received && !this.userDoc) {
reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(Doc.UserDoc()), { fireImmediately: true });
} else {
- if (received && CurrentUserUtils._urlState.sharing) {
- reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
- initialized => initialized && received && DocServer.GetRefField(received).then(docField => {
- if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) {
- CollectionDockingView.AddSplit(docField, "right");
- }
- }),
- );
- }
- const activeDash = PromiseValue(this.userDoc.activeDashboard);
- activeDash.then(dash => {
+ PromiseValue(this.userDoc.activeDashboard).then(dash => {
if (dash instanceof Doc) CurrentUserUtils.openDashboard(this.userDoc, dash);
else CurrentUserUtils.createNewDashboard(this.userDoc);
});
@@ -286,7 +277,7 @@ export class MainView extends React.Component {
headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
@computed get headerBarDocView() {
- return <div style={{ height: this.headerBarDocHeight() }}>
+ return <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
<DocumentView key="headerBarDoc"
Document={this.headerBarDoc}
DataDoc={undefined}
@@ -297,11 +288,12 @@ export class MainView extends React.Component {
styleProvider={DefaultStyleProvider}
rootSelected={returnTrue}
removeDocument={returnFalse}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
ScreenToLocalTransform={this.headerBarScreenXf}
childHideResizeHandles={returnTrue}
+ dontRegisterView={true}
hideResizeHandles={true}
PanelWidth={this.headerBarDocWidth}
PanelHeight={this.headerBarDocHeight}
@@ -502,15 +494,6 @@ export class MainView extends React.Component {
</>;
}
- @computed get headerBar() {
- return !this.userDoc ? (null) :
- <div className="mainView-dashboardArea" style={{
- height: this.headerBarDocHeight(),
- width: "100%",
- }} >
- {this.headerBarDocView}
- </div>;
- }
@computed get mainDashboardArea() {
return !this.userDoc ? (null) :
@@ -592,6 +575,7 @@ export class MainView extends React.Component {
searchFilterDocs={returnEmptyDoclist}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined} />
+ {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{this.userDoc?.presentationMode}</div> : <></>}
</div>;
}
@computed get snapLines() {
@@ -677,12 +661,20 @@ export class MainView extends React.Component {
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.LinkEditorDocView ? <LinkMenu clearLinkEditor={action(() => DocumentLinksButton.LinkEditorDocView = undefined)} docView={DocumentLinksButton.LinkEditorDocView} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)}
- <div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 2001 }} >
- <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
- </div>
- <GestureOverlay >
- {this.mainDashboardArea}
- </GestureOverlay>
+
+ {((page:string) => {
+ switch (page) {
+ case "dashboard":
+ default:return <>
+ <div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 1999 }} >
+ <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
+ </div>
+ <GestureOverlay> {this.mainDashboardArea} </GestureOverlay>
+ </>;
+ case "home": return <DashboardView/>;
+ } })(StrCast(Doc.UserDoc().activePage))
+ }
+
<PreviewCursor />
<TaskCompletionBox />
<ContextMenu />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index e15624e23..20b99788c 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -132,14 +132,14 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
if (savedAnnoMap.size === 0) return undefined;
const savedAnnos = Array.from(savedAnnoMap.values())[0];
if (savedAnnos.length && (savedAnnos[0] as any).marqueeing) {
- const scale = this.props.scaling?.() || 1;
+ const scale = (this.props.scaling?.() || 1);
const anno = savedAnnos[0];
const containerOffset = this.props.containerOffset?.() || [0, 0];
const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: "Annotation on " + this.props.rootDoc.title });
- marqueeAnno.x = (parseInt(anno.style.left || "0") - containerOffset[0]) / scale;
- marqueeAnno.y = (parseInt(anno.style.top || "0") - containerOffset[1]) / scale + NumCast(this.props.scrollTop);
- marqueeAnno._height = parseInt(anno.style.height || "0") / scale;
- marqueeAnno._width = parseInt(anno.style.width || "0") / scale;
+ marqueeAnno.x = NumCast(this.props.docView.props.Document.panXMin) + (parseInt(anno.style.left || "0") - containerOffset[0]) / scale/ NumCast(this.props.docView.props.Document._viewScale,1);
+ marqueeAnno.y = NumCast(this.props.docView.props.Document.panYMin) + (parseInt(anno.style.top || "0") - containerOffset[1]) / scale/ NumCast(this.props.docView.props.Document._viewScale,1) + NumCast(this.props.scrollTop);
+ marqueeAnno._height = parseInt(anno.style.height || "0") / scale/ NumCast(this.props.docView.props.Document._viewScale,1);
+ marqueeAnno._width = parseInt(anno.style.width || "0") / scale/ NumCast(this.props.docView.props.Document._viewScale,1);
anno.remove();
savedAnnoMap.clear();
return marqueeAnno;
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 555f4298d..302e7a5e3 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,3 +1,13 @@
+.overlayView {
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ /* background-color: pink; */
+ z-index: 100000;
+}
+
.overlayWindow-outerDiv {
border-radius: 5px;
overflow: hidden;
@@ -46,4 +56,6 @@
.overlayView-doc {
z-index: 9002; //so that it appears above chroma
position: absolute;
+ top: 0;
+ left: 0;
} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 95e146df4..ab5dc74c9 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,8 +1,10 @@
+import { docs } from "googleapis/build/src/apis/docs";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
+import { computedFn } from "mobx-utils";
import * as React from "react";
import ReactLoading from 'react-loading';
-import { Doc, HeightSym, WidthSym } from "../../fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { Cast, NumCast } from "../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, Utils } from "../../Utils";
@@ -150,6 +152,10 @@ export class OverlayView extends React.Component {
(doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc));
return true;
}
+
+ docScreenToLocalXf = computedFn(function docScreenToLocalXf(this: any, doc: Doc) {
+ return () => new Transform(-NumCast(doc.x), -NumCast(doc.y), 1);
+ }.bind(this));
@computed get overlayDocs() {
return CurrentUserUtils.OverlayDocs?.map(d => {
@@ -183,7 +189,7 @@ export class OverlayView extends React.Component {
offsetx = NumCast(d.x) - e.clientX;
offsety = NumCast(d.y) - e.clientY;
};
- return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: 0, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
+ return <div className="overlayView-doc" ref={dref} key={d[Id]} onPointerDown={onPointerDown} style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
<DocumentView
Document={d}
rootSelected={returnTrue}
@@ -192,9 +198,9 @@ export class OverlayView extends React.Component {
removeDocument={this.removeOverlayDoc}
PanelWidth={d[WidthSym]}
PanelHeight={d[HeightSym]}
- ScreenToLocalTransform={() => new Transform(-NumCast(d.x), -NumCast(d.y), 1)}
+ ScreenToLocalTransform={this.docScreenToLocalXf(d)}
renderDepth={1}
- isDocumentActive={returnFalse}
+ isDocumentActive={returnTrue}
isContentActive={returnTrue}
whenChildContentsActiveChanged={emptyFunction}
focus={DocUtils.DefaultFocus}
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 24857d8ee..74055f057 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -3,25 +3,23 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, Opt } from "../../fields/Doc";
+import { Id } from "../../fields/FieldSymbols";
import { InkField } from '../../fields/InkField';
import { RichTextField } from '../../fields/RichTextField';
import { BoolCast, StrCast } from "../../fields/Types";
+import { ImageField } from "../../fields/URLField";
import { DocUtils } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { SelectionManager } from '../util/SelectionManager';
import { undoBatch } from '../util/UndoManager';
import { CollectionViewType } from './collections/CollectionView';
+import { Colors } from "./global/globalEnums";
import { InkingStroke } from './InkingStroke';
import { DocumentView } from './nodes/DocumentView';
+import { VideoBox } from "./nodes/VideoBox";
+import { pasteImageBitmap } from "./nodes/WebBoxRenderer";
import './PropertiesButtons.scss';
import React = require("react");
-import { Colors } from "./global/globalEnums";
-import { CollectionFreeFormView } from "./collections/collectionFreeForm";
-import { DocumentManager } from "../util/DocumentManager";
-import { pasteImageBitmap } from "./nodes/WebBoxRenderer";
-import { VideoBox } from "./nodes/VideoBox";
-import { Id } from "../../fields/FieldSymbols";
-import { ImageField } from "../../fields/URLField";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -73,7 +71,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
return this.propertyToggleBtn("Lock\xA0View", "_lockedTransform", on => `${on ? "Unlock" : "Lock"} panning of view`, on => "lock");
}
@computed get fitContentButton() {
- return this.propertyToggleBtn("View All", "_fitToBox", on => `${on ? "Don't" : "Do"} fit content to container visible area`, on => "eye");
+ return this.propertyToggleBtn("View All", "_fitContentsToBox", on => `${on ? "Don't" : "Do"} fit content to container visible area`, on => "eye");
}
@computed get fitWidthButton() {
return this.propertyToggleBtn("Fit\xA0Width", "_fitWidth", on => `${on ? "Don't" : "Do"} fit content to width of container`, on => "arrows-alt-h");
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index bcfd2dd56..16eb95cf4 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -300,7 +300,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
Document={layoutDoc}
DataDoc={this.dataDoc}
renderDepth={1}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
rootSelected={returnFalse}
styleProvider={DefaultStyleProvider}
docViewPath={returnEmptyDoclist}
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index 2b045aa6c..4fecfa4d9 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -123,8 +123,9 @@ export class ScriptingRepl extends React.Component {
let stopProp = true;
switch (e.key) {
case "Enter": {
+ e.stopPropagation();
const docGlobals: { [name: string]: any } = {};
- DocumentManager.Instance.DocumentViews.forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document);
+ Array.from(DocumentManager.Instance.DocumentViews).forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document);
const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals);
const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals });
if (!script.compiled) {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index de2106e4a..d2687df17 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -26,6 +26,7 @@ import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView";
import { CollectionViewType } from './CollectionView';
import { TabDocView } from './TabDocView';
import React = require("react");
+import { SelectionManager } from '../../util/SelectionManager';
const _global = (window /* browser */ || global /* node */) as any;
@observer
@@ -50,9 +51,8 @@ export class CollectionDockingView extends CollectionSubView() {
public _flush: UndoManager.Batch | undefined;
private _ignoreStateChange = "";
public tabMap: Set<any> = new Set();
- public get initialized() { return this._goldenLayout !== null; }
public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; }
- @observable private _goldenLayout: any = null;
+ private _goldenLayout: any = null;
constructor(props: SubCollectionViewProps) {
super(props);
@@ -118,6 +118,7 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
public static OpenFullScreen(doc: Doc) {
+ SelectionManager.DeselectAll();
const instance = CollectionDockingView.Instance;
if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") {
return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc);
@@ -171,12 +172,6 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
@action
public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) {
- if (document.type === DocumentType.PRES) {
- const docs = Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null).data, listSpec(Doc), []);
- if (docs.includes(document)) {
- docs.splice(docs.indexOf(document), 1);
- }
- }
if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document);
const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
@@ -185,6 +180,7 @@ export class CollectionDockingView extends CollectionSubView() {
return true;
}
const instance = CollectionDockingView.Instance;
+ const glayRoot = instance._goldenLayout.root;
if (!instance) return false;
const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
@@ -192,71 +188,75 @@ export class CollectionDockingView extends CollectionSubView() {
stack.addChild(docContentConfig, undefined);
stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]);
} else {
- const newItemStackConfig = { type: 'stack', content: [docContentConfig] };
- const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
- if (instance._goldenLayout.root.contentItems.length === 0) { // if no rows / columns
- instance._goldenLayout.root.addChild(newContentItem);
- } else if (instance._goldenLayout.root.contentItems[0].isStack) {
- instance._goldenLayout.root.contentItems[0].addChild(docContentConfig);
+ const newContentItem = () => {
+ const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
+ newItem.callDownwards('_$init');
+ return newItem;
+ }
+ if (glayRoot.contentItems.length === 0) { // if no rows / columns
+ glayRoot.addChild(newContentItem());
+ } else if (glayRoot.contentItems[0].isStack) {
+ glayRoot.contentItems[0].addChild(docContentConfig);
} else if (
- instance._goldenLayout.root.contentItems.length === 1 &&
- instance._goldenLayout.root.contentItems[0].contentItems.length === 1 &&
- instance._goldenLayout.root.contentItems[0].contentItems[0].contentItems.length === 0) {
- instance._goldenLayout.root.contentItems[0].contentItems[0].addChild(docContentConfig);
+ glayRoot.contentItems.length === 1 &&
+ glayRoot.contentItems[0].contentItems.length === 1 &&
+ glayRoot.contentItems[0].contentItems[0].contentItems.length === 0) {
+ glayRoot.contentItems[0].contentItems[0].addChild(docContentConfig);
}
else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row
switch (pullSide) {
default:
- case "right": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break;
- case "left": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break;
+ case "right": glayRoot.contentItems[0].addChild(newContentItem()); break;
+ case "left": glayRoot.contentItems[0].addChild(newContentItem(), 0); break;
case "top":
case "bottom":
// if not going in a row layout, must add already existing content into column
- const rowlayout = instance._goldenLayout.root.contentItems[0];
+ const rowlayout = glayRoot.contentItems[0];
const newColumn = rowlayout.layoutManager.createContentItem({ type: "column" }, instance._goldenLayout);
- CollectionDockingView.Instance._goldenLayout.saveScrollTops(rowlayout.element);
+ const newItem = newContentItem();
+ instance._goldenLayout.saveScrollTops(rowlayout.element);
rowlayout.parent.replaceChild(rowlayout, newColumn);
if (pullSide === "top") {
newColumn.addChild(rowlayout, undefined, true);
- newColumn.addChild(newContentItem, 0, true);
+ newColumn.addChild(newItem, 0, true);
} else if (pullSide === "bottom") {
- newColumn.addChild(newContentItem, undefined, true);
+ newColumn.addChild(newItem, undefined, true);
newColumn.addChild(rowlayout, 0, true);
}
- CollectionDockingView.Instance._goldenLayout.restoreScrollTops(rowlayout.element);
+ instance._goldenLayout.restoreScrollTops(rowlayout.element);
rowlayout.config.height = 50;
- newContentItem.config.height = 50;
+ newItem.config.height = 50;
}
} else {// if (instance._goldenLayout.root.contentItems[0].isColumn) { // if column
switch (pullSide) {
- case "top": instance._goldenLayout.root.contentItems[0].addChild(newContentItem, 0); break;
- case "bottom": instance._goldenLayout.root.contentItems[0].addChild(newContentItem); break;
+ case "top": glayRoot.contentItems[0].addChild(newContentItem(), 0); break;
+ case "bottom": glayRoot.contentItems[0].addChild(newContentItem()); break;
case "left":
case "right":
default:
// if not going in a row layout, must add already existing content into column
- const collayout = instance._goldenLayout.root.contentItems[0];
+ const collayout = glayRoot.contentItems[0];
const newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
- CollectionDockingView.Instance._goldenLayout.saveScrollTops(collayout.element);
+ const newItem = newContentItem();
+ instance._goldenLayout.saveScrollTops(collayout.element);
collayout.parent.replaceChild(collayout, newRow);
if (pullSide === "left") {
newRow.addChild(collayout, undefined, true);
- newRow.addChild(newContentItem, 0, true);
+ newRow.addChild(newItem, 0, true);
} else {
- newRow.addChild(newContentItem, undefined, true);
+ newRow.addChild(newItem, undefined, true);
newRow.addChild(collayout, 0, true);
}
- CollectionDockingView.Instance._goldenLayout.restoreScrollTops(collayout.element);
+ instance._goldenLayout.restoreScrollTops(collayout.element);
collayout.config.width = 50;
- newContentItem.config.width = 50;
+ newItem.config.width = 50;
}
}
instance._ignoreStateChange = JSON.stringify(instance._goldenLayout.toConfig());
- newContentItem.callDownwards('_$init');
}
return instance.layoutChanged();
@@ -271,7 +271,7 @@ export class CollectionDockingView extends CollectionSubView() {
return true;
}
- async setupGoldenLayout() {
+ setupGoldenLayout = async () => {
const config = StrCast(this.props.Document.dockingConfig);
if (config) {
const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g);
@@ -288,26 +288,19 @@ export class CollectionDockingView extends CollectionSubView() {
this._goldenLayout.unbind('stackCreated', this.stackCreated);
} catch (e) { }
}
+ this.tabMap.clear();
+ this._goldenLayout.destroy();
}
- this.tabMap.clear();
- this._goldenLayout?.destroy();
- runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
- this._goldenLayout.on('tabCreated', this.tabCreated);
- this._goldenLayout.on('tabDestroyed', this.tabDestroyed);
- this._goldenLayout.on('stackCreated', this.stackCreated);
- this._goldenLayout.registerComponent('DocumentFrameRenderer', TabDocView);
- this._goldenLayout.container = this._containerRef.current;
- if (this._goldenLayout.config.maximisedItemId === '__glMaximised') {
- try {
- this._goldenLayout.config.root.getItemsById(this._goldenLayout.config.maximisedItemId)[0].toggleMaximise();
- } catch (e) {
- this._goldenLayout.config.maximisedItemId = null;
- }
- }
- this._goldenLayout.init();
- this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
- this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart);
- this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
+ const glay = this._goldenLayout = new GoldenLayout(JSON.parse(config));
+ glay.on('tabCreated', this.tabCreated);
+ glay.on('tabDestroyed', this.tabDestroyed);
+ glay.on('stackCreated', this.stackCreated);
+ glay.registerComponent('DocumentFrameRenderer', TabDocView);
+ glay.container = this._containerRef.current;
+ glay.init();
+ glay.root.layoutManager.on('itemDropped', this.tabItemDropped);
+ glay.root.layoutManager.on('dragStart', this.tabDragStart);
+ glay.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
}
}
@@ -322,7 +315,7 @@ export class CollectionDockingView extends CollectionSubView() {
}
this._ignoreStateChange = "";
});
- setTimeout(() => this.setupGoldenLayout(), 0);
+ setTimeout(this.setupGoldenLayout);
//window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
}
}
@@ -368,7 +361,8 @@ export class CollectionDockingView extends CollectionSubView() {
const htmlTarget = e.target as HTMLElement;
window.addEventListener("mouseup", this.onPointerUp);
if (!htmlTarget.closest("*.lm_content") && (htmlTarget.closest("*.lm_tab") || htmlTarget.closest("*.lm_stack"))) {
- if (htmlTarget.className !== "lm_close_tab") {
+ const className = typeof htmlTarget.className === "string" ? htmlTarget.className : "";
+ if (!className.includes("lm_close") && !className.includes("lm_maximise")) {
this._flush = UndoManager.StartBatch("golden layout edit");
}
}
@@ -413,29 +407,37 @@ export class CollectionDockingView extends CollectionSubView() {
const docs = !docids ? [] : docids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
const changesMade = this.props.Document.dockcingConfig !== json;
if (changesMade && !this._flush) {
- this.props.Document.dockingConfig = json;
- this.props.Document.data = new List<Doc>(docs);
+ UndoManager.RunInBatch(() => {
+ this.props.Document.dockingConfig = json;
+ this.props.Document.data = new List<Doc>(docs);
+ }, "state changed");
}
return changesMade;
}
tabDestroyed = (tab: any) => {
+ Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc);
+ Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true);
+ const dview = CollectionDockingView.Instance.props.Document;
+ const fieldKey = CollectionDockingView.Instance.props.fieldKey;
+ Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc);
this.tabMap.delete(tab);
tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
this.stateChanged();
}
tabCreated = (tab: any) => {
+ this.tabMap.add(tab);
tab.contentItem.element[0]?.firstChild?.firstChild?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content)
}
stackCreated = (stack: any) => {
stack.header?.element.on('mousedown', (e: any) => {
if (e.target === stack.header?.element[0] && e.button === 2) {
- const emptyPane = CurrentUserUtils.EmptyPane;
- emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
+ const dashboard= CurrentUserUtils.ActiveDashboard;
+ dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1;
const docToAdd = Docs.Create.FreeformDocument([], {
- _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`,
+ _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}`,
});
this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
CollectionDockingView.AddSplit(docToAdd, "", stack);
@@ -452,14 +454,17 @@ export class CollectionDockingView extends CollectionSubView() {
alert('cant delete the last stack');
}
}));
+
+ stack.header?.controlsContainer.find('.lm_maximise') //get the close icon
+ .click(() => setTimeout(this.stateChanged));
stack.header?.controlsContainer.find('.lm_popout') //get the popout icon
.off('click') //unbind the current click handler
.click(action(() => {
// stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size
- const emptyPane = CurrentUserUtils.EmptyPane;
- emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1;
+ const dashboard = CurrentUserUtils.ActiveDashboard;
+ dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1;
const docToAdd = Docs.Create.FreeformDocument([], {
- _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`
+ _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}`
});
this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
CollectionDockingView.AddSplit(docToAdd, "", stack);
diff --git a/src/client/views/collections/CollectionDockingViewMenu.scss b/src/client/views/collections/CollectionDockingViewMenu.scss
deleted file mode 100644
index 4157f0f7e..000000000
--- a/src/client/views/collections/CollectionDockingViewMenu.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-
-.dockingViewButtonSelector {
- div {
- overflow: visible !important;
- }
-
- display: inline-block;
- width:100%;
- height:100%;
-}
-.dockingViewButtonSelector-flyout {
- position: relative;
- z-index: 9999;
- background-color: #eeeeee;
- box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
- color: black;
-
- padding: 10px;
- border-radius: 3px;
- display: inline-block;
- height: 100%;
- width: 100%;
- border-radius: 3px;
-
- hr {
- height: 1px;
- margin: 0px;
- background-color: gray;
- border-top: 0px;
- border-bottom: 0px;
- border-right: 0px;
- border-left: 0px;
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionDockingViewMenu.tsx b/src/client/views/collections/CollectionDockingViewMenu.tsx
deleted file mode 100644
index 1cab293a8..000000000
--- a/src/client/views/collections/CollectionDockingViewMenu.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Tooltip } from "@material-ui/core";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { DocumentButtonBar } from "../DocumentButtonBar";
-import { DocumentView } from "../nodes/DocumentView";
-const higflyout = require("@hig/flyout");
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-@observer
-export class CollectionDockingViewMenu extends React.Component<{ views: () => DocumentView[], Stack: any }> {
- customStylesheet(styles: any) {
- return {
- ...styles,
- panel: {
- ...styles.panel,
- minWidth: "100px"
- },
- };
- }
- _ref = React.createRef<HTMLDivElement>();
-
- @computed get flyout() {
- return (
- <div className="dockingViewButtonSelector-flyout" title=" " ref={this._ref}>
- <DocumentButtonBar views={this.props.views} stack={this.props.Stack} />
- </div>
- );
- }
-
- @observable _tooltipOpen: boolean = false;
- render() {
- return <Tooltip open={this._tooltipOpen} onClose={action(() => this._tooltipOpen = false)} title={<><div className="dash-tooltip">Tap for toolbar, drag to create alias in another pane</div></>} placement="bottom">
- <span className="dockingViewButtonSelector"
- onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}
- onPointerDown={action(e => {
- this.props.views()[0]?.select(false);
- this._tooltipOpen = false;
- })} >
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}>
- <FontAwesomeIcon icon={"arrows-alt"} size={"sm"} />
- </Flyout>
- </span>
- </Tooltip >;
- }
-}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 23fd4206c..39f6466d6 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -252,8 +252,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
};
_clusterCommand = {
params: ["target"], title: "fit content",
- script: "self.target._fitToBox = !self.target._fitToBox;",
- immediate: undoBatch((source: Doc[]) => this.target._fitToBox = !this.target._fitToBox),
+ script: "self.target._fitContentsToBox = !self.target._fitContentsToBox;",
+ immediate: undoBatch((source: Doc[]) => this.target._fitContentsToBox = !this.target._fitContentsToBox),
initialize: emptyFunction
};
_fitContentCommand = {
@@ -457,7 +457,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
const isPinned = targetDoc && Doc.isDocPinned(targetDoc);
return !targetDoc ? (null) : <Tooltip key="pin" title={<div className="dash-tooltip">{Doc.isDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}</div>} placement="top">
<button className="antimodeMenu-button" style={{ backgroundColor: isPinned ? "121212" : undefined, borderLeft: "1px solid gray" }}
- onClick={e => TabDocView.PinDoc(targetDoc, { unpin: isPinned })}>
+ onClick={e => TabDocView.PinDoc(targetDoc, { /* unpin: isPinned*/ })}>
<FontAwesomeIcon className="colMenu-icon" size="lg" icon="map-pin" />
</button>
</Tooltip>;
@@ -507,7 +507,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
activeDoc.presPinViewY = y;
activeDoc.presPinViewScale = scale;
} else if (targetDoc.type === DocumentType.VID) {
- activeDoc.presPinTimecode = targetDoc._currentTimecode;
activeDoc.presPinView = true;
} else if (targetDoc.type === DocumentType.COMPARISON) {
const width = targetDoc._clipWidth;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 8bdcfeea9..372629ddf 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -465,6 +465,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
_stayInCollection: true,
useLinkSmallAnchor: true,
hideLinkButton: true,
+ _isLinkButton: true,
annotationOn: rootDoc,
_timelineLabel: true,
borderRounding: anchorEndTime === undefined ? "100%" : undefined
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 2f002736d..73572299a 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -30,11 +30,10 @@
width: 90%;
margin: 5px;
font-size: 11px;
- background-color: $light-blue;
color: $medium-blue;
padding: 10px;
- border-radius: 10px;
- border: solid 2px $medium-blue;
+ border-radius: 5px;
+ border: solid .5px $medium-blue;
}
}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 4f6f45d2f..7573b938a 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -135,7 +135,7 @@ export class CollectionTimeView extends CollectionSubView() {
onClick={this.contentsDown}>
<CollectionFreeFormView {...this.props}
engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
childClickScript={this._childClickedScript}
viewDefDivClick={this._viewDefDivClick}
childFreezeDimensions={true}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index dab3d490d..022b9fa24 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -4,15 +4,15 @@ import { Tooltip } from '@material-ui/core';
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
import { clamp } from 'lodash';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
-import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
+import { DataSym, Doc, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { FieldId } from "../../../fields/RefField";
import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils";
+import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -27,14 +27,16 @@ import { Colors, Shadows } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { MainView } from '../MainView';
import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView";
+import { DashFieldView } from '../nodes/formattedText/DashFieldView';
import { PinProps, PresBox, PresMovement } from '../nodes/trails';
import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
import { CollectionDockingView } from './CollectionDockingView';
-import { CollectionDockingViewMenu } from './CollectionDockingViewMenu';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TabDocView.scss";
import React = require("react");
+import { listSpec } from '../../../fields/Schema';
+import { ScriptField } from '../../../fields/ScriptField';
const _global = (window /* browser */ || global /* node */) as any;
interface TabDocViewProps {
@@ -82,7 +84,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const iconWrap = document.createElement("div");
const closeWrap = document.createElement("div");
-
titleEle.size = StrCast(doc.title).length + 3;
titleEle.value = doc.title;
titleEle.onkeydown = (e: KeyboardEvent) => {
@@ -93,26 +94,33 @@ export class TabDocView extends React.Component<TabDocViewProps> {
Doc.GetProto(doc).title = e.currentTarget.value;
}));
- const dragBtnDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction);
- };
-
-
if (tab.element[0].children[1].children.length === 1) {
- iconWrap.className = "lm_iconWrap";
- iconWrap.id = "lm_iconWrap";
- closeWrap.className = "lm_iconWrap";
- closeWrap.id = "lm_closeWrap";
- closeWrap.onclick = (e: MouseEvent) => {
- tab.header.parent.contentItem.remove();
- Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc);
- Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true);
+ iconWrap.className = "lm_iconWrap lm_moreInfo";
+ const dragBtnDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, action(e => {
+ if (this.view) {
+ SelectionManager.SelectView(this.view, false);
+ let child = this.view.ContentDiv!.children[0];
+ while (child.children.length) {
+ const next = Array.from(child.children).find(c => c.className?.toString().includes("SVGAnimatedString") || typeof (c.className) === "string");
+ if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
+ if (next?.className?.toString().includes(DashFieldView.name)) break;
+ if (next) child = next;
+ else break;
+ }
+ simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
+ }
+ else { this._activated = true;
+ setTimeout(() =>this.view && SelectionManager.SelectView(this.view, false));
+ }
+ }));
};
+
const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />;
- const closeIcon = <FontAwesomeIcon icon={"times"} />;
+ const closeIcon = <FontAwesomeIcon icon={"eye"} />;
ReactDOM.render(docIcon, iconWrap);
ReactDOM.render(closeIcon, closeWrap);
- // tab.element[0].append(closeWrap);
+ tab.reactComponents = [iconWrap, closeWrap];
tab.element[0].prepend(iconWrap);
tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }),
({ layer, color }) => {
@@ -123,7 +131,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
titleEle.style.backgroundColor = "transparent";
iconWrap.style.color = textColor;
closeWrap.style.color = textColor;
- moreInfoDrag.style.backgroundColor = textColor;
tab.element[0].style.background = !layer ? color : "dimgrey";
}, { fireImmediately: true });
}
@@ -156,19 +163,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
// toggle.style.textTransform = selected ? "uppercase" : "";
}));
- //attach the selection doc buttons menu to the drag handle
- const stack: HTMLDivElement = tab.contentItem.parent;
- const header: HTMLDivElement = tab;
- stack.onscroll = action((e: any) => {
- console.log('scrolling...');
- });
- const moreInfoDrag = document.createElement("div");
- moreInfoDrag.className = "lm_iconWrap";
- tab._disposers.buttonDisposer = reaction(() => this.view, view =>
- view && [ReactDOM.render(<span><CollectionDockingViewMenu views={() => [view]} Stack={stack} /></span>, moreInfoDrag), tab._disposers.buttonDisposer?.()],
- { fireImmediately: true });
- // tab.reactComponents = [moreInfoDrag];
- // tab.element[0].children[3].before(moreInfoDrag);
// highlight the tab when the tab document is brushed in any part of the UI
tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => {
@@ -185,20 +179,21 @@ export class TabDocView extends React.Component<TabDocViewProps> {
UndoManager.RunInBatch(() => tab.contentItem.remove(), "delete tab");
});
}
- CollectionDockingView.Instance.tabMap.add(tab);
}
/**
* Adds a document to the presentation view
**/
@action
- public static async PinDoc(doc: Doc, pinProps?: PinProps) {
- if (pinProps?.unpin) console.log('TODO: Remove UNPIN from this location');
- //add this new doc to props.Document
+ public static PinDoc(docs: Doc|Doc[], pinProps?: PinProps) {
+ const docList = ((docs instanceof Doc) ? [docs]: docs);
+ const batch = UndoManager.StartBatch("pinning doc");
+
+ // all docs will be added to the ActivePresentation as stored on CurrentUserUtils
const curPres = CurrentUserUtils.ActivePresentation;
- if (curPres) {
+ curPres && docList.forEach(doc => {
+ // Edge Case 1: Cannot pin document to itself
if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; }
- const batch = UndoManager.StartBatch("pinning doc");
const pinDoc = Doc.MakeAlias(doc);
pinDoc.presentationTargetDoc = doc;
pinDoc.title = doc.title + " - Slide";
@@ -218,6 +213,48 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const size: number = PresBox.Instance?._selectedArray.size;
const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
+ // If pinWithView option set then update scale and x / y props of slide
+ if (pinProps?.pinWithView) {
+ const viewProps = pinProps.pinWithView;
+ pinDoc.presPinView = true;
+ pinDoc.presPinViewX = viewProps.bounds.left + viewProps.bounds.width / 2;
+ pinDoc.presPinViewY = viewProps.bounds.top + viewProps.bounds.height / 2;
+ pinDoc.presPinViewScale = viewProps.scale;
+ pinDoc.contentBounds = new List<number>([viewProps.bounds.left, viewProps.bounds.top, viewProps.bounds.left+viewProps.bounds.width, viewProps.bounds.top+viewProps.bounds.height]);
+ }
+ if (pinProps?.pinDocView) {
+ const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(pinDoc.type as any) || pinDoc._viewType === CollectionViewType.Stacking;
+ const pannable: boolean = ((pinDoc.type === DocumentType.COL && doc._viewType === CollectionViewType.Freeform) || doc.type === DocumentType.IMG);
+ if (scrollable) {
+ const scroll = doc._scrollTop;
+ pinDoc.presPinView = true;
+ pinDoc.presPinViewScroll = scroll;
+ } else if ([DocumentType.AUDIO, DocumentType.VID].includes(doc.type as any)) {
+ pinDoc.presPinView = true;
+ pinDoc.presStartTime = doc._currentTimecode;
+ pinDoc.presEndTime = NumCast(doc._currentTimecode) + 0.1;
+ } else if (pannable) {
+ pinDoc.presPinView = true;
+ pinDoc.presPinViewX = pinDoc._panX;
+ pinDoc.presPinViewY = pinDoc._panY;
+ pinDoc.presPinViewScale = pinDoc._viewScale;
+ const pw = NumCast(pinProps.panelWidth);
+ const ph = NumCast(pinProps.panelHeight);
+ const ps = NumCast(pinDoc._viewScale);
+ if (pw && ph && ps) {
+ pinDoc.contentBounds = new List<number>([
+ NumCast(pinDoc.panX)-pw/2/ps,
+ NumCast(pinDoc.panY)-ph/2/ps,
+ NumCast(pinDoc.panX)+pw/2/ps,
+ NumCast(pinDoc.panY)+ph/2/ps]);
+ }
+ } else if (doc.type === DocumentType.COMPARISON) {
+ const width = doc._clipWidth;
+ pinDoc.presPinClipWidth = width;
+ pinDoc.presPinView = true;
+ }
+ }
+ pinDoc.onClick = ScriptField.MakeFunction("navigateToDoc(self.presentationTargetDoc, self)");
Doc.AddDocToList(curPres, "data", pinDoc, presSelected);
if (!pinProps?.audioRange && duration !== undefined) {
pinDoc.mediaStart = "manual";
@@ -236,20 +273,16 @@ export class TabDocView extends React.Component<TabDocViewProps> {
pinDoc.presMovement = PresMovement.None;
}
if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
- const dview = CollectionDockingView.Instance.props.Document;
- const fieldKey = CollectionDockingView.Instance.props.fieldKey;
- const tabdocs = await DocListCastAsync(dview[fieldKey]);
- runInAction(() => {
- if (!pinProps?.hidePresBox && !tabdocs?.includes(curPres)) {
- tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs
- CollectionDockingView.AddSplit(curPres, "right");
- }
- PresBox.Instance?._selectedArray.clear();
- pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array
- DocumentManager.Instance.jumpToDocument(doc, false, undefined, []);
- batch.end();
- });
+ PresBox.Instance?._selectedArray.clear();
+ pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array
+ });
+ if (!Array.from(CollectionDockingView.Instance.tabMap).map(d => d.DashDoc).includes(curPres)) {
+ const docs = Cast(Cast(Doc.UserDoc().myOverlayDocs, Doc, null).data, listSpec(Doc), []);
+ if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
+ CollectionDockingView.AddSplit(curPres, "right");
+ setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
}
+ setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
componentDidMount() {
@@ -272,7 +305,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
componentWillUnmount() {
this._tabReaction?.();
this._view && DocumentManager.Instance.RemoveView(this._view);
- this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab);
this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged);
}
@@ -420,9 +452,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}} ref={ref => {
if (this._mainCont = ref) {
if (this._lastTab) {
- console.log("DUP tab")
this._view && DocumentManager.Instance.RemoveView(this._view);
- CollectionDockingView.Instance.tabMap.delete(this._lastTab);
}
this._lastTab = this.tab;
(this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document);
@@ -535,11 +565,11 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
docFilters={CollectionDockingView.Instance.childDocFilters}
docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters}
searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
/>
<div className="miniOverlay" onPointerDown={this.miniDown} >
<div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} />
</div>
</div>;
}
-}
+} \ No newline at end of file
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 23b6a7f72..59dc5671b 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -216,8 +216,11 @@ export class TreeView extends React.Component<TreeViewProps> {
this.props.hierarchyIndex !== undefined && this.props.RemFromMap?.(this.doc, this.props.hierarchyIndex);
}
+ componentDidUpdate() {
+ this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
+ }
+
componentDidMount() {
- // TODO: [AL] add these
this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
}
@@ -468,7 +471,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const docs = expandKey === "aliases" ? this.childAliases : expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs;
let downX = 0, downY = 0;
return <>
- {!docs?.length ? (null) : <div className={'treeView-sorting'} style={{ background: sortings[sorting]?.color }} >
+ {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? (null) : <div className={'treeView-sorting'} style={{ background: sortings[sorting]?.color }} >
{sortings[sorting]?.label}
</div>}
<ul key={expandKey + "more"} title="click to change sort order" className={this.doc.treeViewHideTitle ? "no-indent" : ""}
@@ -606,13 +609,11 @@ export class TreeView extends React.Component<TreeViewProps> {
return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
}
- // TODO: currently doc focus works, but can't seem to edit title
- // onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptCast(this.doc.treeChildClick));
onChildClick = () => {
- return this.props.onChildClick?.() ?? (ScriptField.MakeFunction(`DocFocusOrOpen(self)`)! || this._editTitleScript?.());
+ return this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!);
}
- onChildDoubleClick = () => (!this.props.treeView.outlineMode && this._openScript?.()) || ScriptCast(this.doc.treeChildDoubleClick);
+ onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick,(!this.props.treeView.outlineMode ? this._openScript?.():null));
refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
ignoreEvent = (e: any) => {
@@ -795,7 +796,7 @@ export class TreeView extends React.Component<TreeViewProps> {
isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
hideTitle={asText}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
onClick={this.onChildClick}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 12ad6b02b..52e99f26b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,7 +9,7 @@ import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fiel
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
-import { createSchema, listSpec } from "../../../../fields/Schema";
+import { listSpec } from "../../../../fields/Schema";
import { ScriptField } from "../../../../fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
import { ImageField } from "../../../../fields/URLField";
@@ -26,6 +26,7 @@ import { DragManager, dropActionType } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
import { InteractionUtils } from "../../../util/InteractionUtils";
import { LinkManager } from "../../../util/LinkManager";
+import { RecordingApi } from "../../../util/RecordingApi";
import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
import { SearchUtil } from "../../../util/SearchUtil";
import { SelectionManager } from "../../../util/SelectionManager";
@@ -41,6 +42,7 @@ import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveIn
import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
+import { FieldViewProps } from "../../nodes/FieldView";
import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { PresBox } from "../../nodes/trails/PresBox";
import { VideoBox } from "../../nodes/VideoBox";
@@ -50,26 +52,14 @@ import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
import { TreeViewType } from "../CollectionTreeView";
import { CollectionViewType } from "../CollectionView";
+import { TabDocView } from "../TabDocView";
import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { FieldView, FieldViewProps } from "../../nodes/FieldView";
-
-export const panZoomSchema = createSchema({
- _panX: "number",
- _panY: "number",
- _currentTimecode: "number",
- _timecodeToShow: "number",
- _currentFrame: "number",
- _useClusters: "boolean",
- _viewTransition: "string",
- _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- _fitToBox: "boolean",
- scrollHeight: "number" // this will be set when the collection is an annotation overlay for a PDF/Webpage
-});
+import e = require("connect-flash");
+
export type collectionFreeformViewProps = {
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -137,16 +127,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x))
};
}
- @computed get fitToContent() { return (this.props.fitContentsToDoc?.() || this.Document._fitToBox) && !this.isAnnotationOverlay; }
- @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); }
- @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); }
- @computed get nativeHeight() { return this.fitToContent ? 0 : Doc.NativeHeight(this.Document); }
+ @computed get fitContentsToBox() { return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; }
+ @computed get contentBounds() {
+ const cb = Cast(this.rootDoc.contentBounds, listSpec("number"));
+ return cb ? {x:cb[0], y:cb[1], r:cb[2], b: cb[3]} :
+ this.props.contentBounds?.() ?? aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10));
+ }
+ @computed get nativeWidth() { return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); }
+ @computed get nativeHeight() { return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); }
@computed get cachedCenteringShiftX(): number {
- const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
+ const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling;
return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
- const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
+ const scaling = this.fitContentsToBox || !this.contentScaling ? 1 : this.contentScaling;
return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling;// shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
@@ -171,11 +165,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.layoutDoc._panY = vals.bounds.cy;
this.layoutDoc._viewScale = vals.scale;
}
- freeformData = (force?: boolean) => !this._firstRender && (this.fitToContent || force) ? this.fitToContentVals : undefined;
- reverseNativeScaling = () => this.fitToContent ? true : false;
- panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
- panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
- zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
+ freeformData = (force?: boolean) => !this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined;
+ reverseNativeScaling = () => this.fitContentsToBox ? true : false;
+ // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
+ // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
+ panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
+ panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
+ zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
@@ -1002,6 +998,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
+ // set the current respective FFview to the tab being panned.
+ (Doc.UserDoc()?.presentationMode === 'recording') && RecordingApi.Instance.setRecordingFFView(this);
+ // TODO: make this based off the specific recording FFView
+ (Doc.UserDoc()?.presentationMode === 'none') && RecordingApi.Instance.setPlayFFView(this);
+ if (Doc.UserDoc()?.presentationMode === 'watching') {
+ RecordingApi.Instance.pauseVideoAndMovements();
+ Doc.UserDoc().presentationMode = 'none';
+ // RecordingApi.Instance.pauseMovements()
+ }
+
if (!this.isAnnotationOverlay && clamp) {
// this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
@@ -1228,7 +1234,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Document={childLayout}
renderDepth={this.props.renderDepth + 1}
replica={entry.replica}
- dataTransition={entry.transition}
suppressSetHeight={this.layoutEngine ? true : false}
renderCutoffProvider={this.renderCutoffProvider}
ContainingCollectionView={this.props.CollectionView}
@@ -1268,7 +1273,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
pointerEvents={this.pointerEvents}
jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
- //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
+ //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
/>;
}
addDocTab = action((doc: Doc, where: string) => {
@@ -1411,9 +1416,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica)
}));
- if (this.props.isAnnotationOverlay) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
- if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1));
- else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey]));
+ if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) { // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
+ if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
+ else this.props.Document[this.scaleFieldKey] = Math.max(1,this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey]));
}
this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
@@ -1490,51 +1495,71 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}));
}
- replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
+ static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) {
if (oldDiv.childNodes && newDiv) {
for (let i = 0; i < oldDiv.childNodes.length; i++) {
this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement);
}
}
if (oldDiv instanceof HTMLCanvasElement) {
- const canvas = oldDiv;
- const img = document.createElement('img'); // create a Image Element
- img.src = canvas.toDataURL(); //image source
- 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);
+ if (oldDiv.className === "collectionFreeFormView-grid") {
+ const newCan = newDiv as HTMLCanvasElement;
+ const parEle = newCan.parentElement as HTMLElement;
+ parEle.removeChild(newCan);
+ parEle.appendChild(document.createElement('div'))
+ } else {
+ const canvas = oldDiv;
+ const img = document.createElement('img'); // create a Image Element
+ img.src = canvas.toDataURL(); //image source
+ 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);
+ }
}
}
- updateIcon = () => {
- const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
+ updateIcon = () => CollectionFreeFormView.UpdateIcon(
+ this.layoutDoc[Id] + "-icon" + (new Date()).getTime(),
+ this.props.docViewPath().lastElement().ContentDiv!,
+ this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](),
+ this.props.PanelWidth(), this.props.PanelHeight(), 0, 1, false, "",
+ (iconFile, nativeWidth, nativeHeight) => {
+ this.dataDoc.icon = new ImageField(iconFile);
+ this.dataDoc["icon-nativeWidth"] = nativeWidth;
+ this.dataDoc["icon-nativeHeight"] = nativeHeight;
+ });
+
+ public static UpdateIcon(
+ filename:string, docViewContent:HTMLElement,
+ width: number, height: number,
+ panelWidth:number, panelHeight: number,
+ scrollTop:number,
+ realNativeHeight: number,
+ noSuffix: boolean,
+ replaceRootFilename: string| undefined,
+ cb:(iconFile:string, nativeWidth:number, nativeHeight:number) => any)
+ {
const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
- newDiv.style.width = (this.layoutDoc[WidthSym]()).toString();
- newDiv.style.height = (this.layoutDoc[HeightSym]()).toString();
+ newDiv.style.width = width.toString();
+ newDiv.style.height = height.toString();
this.replaceCanvases(docViewContent, newDiv);
- const htmlString = this._mainCont && new XMLSerializer().serializeToString(newDiv);
- const nativeWidth = this.layoutDoc[WidthSym]();
- const nativeHeight = this.layoutDoc[HeightSym]();
-
- CreateImage(
- "",
+ const htmlString = new XMLSerializer().serializeToString(newDiv);
+ const nativeWidth = width;
+ const nativeHeight = height;
+ return CreateImage(
+ Utils.prepend(""),
document.styleSheets,
htmlString,
nativeWidth,
- nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
- NumCast(this.layoutDoc._scrollTop)
+ nativeWidth * panelHeight / panelWidth,
+ scrollTop * panelHeight / realNativeHeight
).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.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc["icon-nativeWidth"] = nativeWidth;
- this.dataDoc["icon-nativeHeight"] = nativeHeight;
- }), 500));
+ (async (data_url: any) => {
+ const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
+ cb(returnedFilename as string, nativeWidth, nativeHeight);
})
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
@@ -1605,8 +1630,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : [];
appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
!Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
- appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
- appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
+ appearanceItems.push({ description: `${this.fitContentsToBox ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitContentsToBox = !this.fitContentsToBox, icon: !this.fitContentsToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
+ appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, {pinDocView:true, panelWidth: this.props.PanelWidth(), panelHeight:this.props.PanelHeight()}), icon: "map-pin" });
+ //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
this.props.ContainingCollectionView &&
appearanceItems.push({ description: "Ungroup collection", event: this.promoteCollection, icon: "table" });
@@ -1762,7 +1788,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isAnnotationOverlay={this.isAnnotationOverlay}>
<div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
{this.layoutDoc._backgroundGridShow ?
- <CollectionFreeFormBackgroundGrid
+ <div><CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!?
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
panX={this.panX}
@@ -1772,7 +1798,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isAnnotationOverlay={this.isAnnotationOverlay}
cachedCenteringShiftX={this.cachedCenteringShiftX}
cachedCenteringShiftY={this.cachedCenteringShiftY}
- /> : (null)}
+ /></div> : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 7865f2381..051da795f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,7 @@ import { Transform } from "../../../util/Transform";
import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { ContextMenu } from "../../ContextMenu";
import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
-import { PresBox } from "../../nodes/trails/PresBox";
+import { PinViewProps, PresBox } from "../../nodes/trails/PresBox";
import { PresMovement } from "../../nodes/trails/PresEnums";
import { VideoBox } from "../../nodes/VideoBox";
import { pasteImageBitmap } from "../../nodes/WebBoxRenderer";
@@ -30,6 +30,7 @@ import { TreeView } from "../TreeView";
import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu";
import "./MarqueeView.scss";
import React = require("react");
+import { TabDocView } from "../TabDocView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -43,6 +44,14 @@ interface MarqueeViewProps {
ungroup?: () => void;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
}
+
+export interface MarqueeViewBounds {
+ left: number;
+ top: number;
+ width: number;
+ height: number;
+}
+
@observer
export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
{
@@ -61,7 +70,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const topLeft = this.Transform.transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
// nda - args to transformDirection is just x and y diff btw downX/Y and lastX/Y
const size = this.Transform.transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
+ const bounds:MarqueeViewBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
+ return bounds;
}
get inkDoc() { return this.props.Document; }
get ink() { return Cast(this.props.Document.ink, InkField); }
@@ -264,7 +274,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
document.removeEventListener("wheel", hideMarquee);
};
if (PresBox.startMarquee) {
- this.pinMarqueeView();
+ this.pinWithView();
PresBox.startMarquee = false;
}
if (!this._commandExecuted && (Math.abs(this.Bounds.height * this.Bounds.width) > 100) && !PresBox.startMarquee) {
@@ -385,98 +395,23 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.hideMarquee();
}
+ /**
+ * This triggers the TabDocView.PinDoc method which is the universal method
+ * used to pin documents to the currently active presentation trail.
+ *
+ * This one is unique in that it includes the bounds associated with marquee view.
+ */
@undoBatch
@action
- pinMarqueeView = async () => {
- const doc = this.props.Document;
- const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc;
- if (curPres) {
- console.log(curPres);
- if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; }
- const pinDoc = Doc.MakeAlias(doc);
- pinDoc.presentationTargetDoc = doc;
- pinDoc.presMovement = PresMovement.Zoom;
- pinDoc.groupWithUp = false;
- pinDoc.context = curPres;
- pinDoc.title = doc.title + " - Slide";// these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
- pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
- pinDoc.treeViewHeaderWidth = "100%"; // forces the header to grow to be the same size as its largest sibling.
- pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
- pinDoc.treeViewFieldKey = "data"; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
- pinDoc.treeViewExpandedView = "data";// in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
- pinDoc.treeViewGrowsHorizontally = true;// the document expands horizontally when displayed as a tree view header
- pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
-
- const presArray = PresBox.Instance?.sortArray();
- const size = PresBox.Instance?._selectedArray.size;
- const presSelected = presArray && size ? presArray[size - 1] : undefined;
- Doc.AddDocToList(curPres, "data", pinDoc, presSelected);
- if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
- const dview = CollectionDockingView.Instance.props.Document;
- const fieldKey = CollectionDockingView.Instance.props.fieldKey;
- const tabdocs = await DocListCastAsync(dview[fieldKey]);
- if (!tabdocs?.includes(curPres)) {
- tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs
- CollectionDockingView.AddSplit(curPres, "right");
- }
- PresBox.Instance?._selectedArray.clear();
- pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Updates selected array
- const index = PresBox.Instance?.childDocs.indexOf(pinDoc);
- index && (curPres._itemIndex = index);
- const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height);
- pinDoc.presPinView = true;
- pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2;
- pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2;
- pinDoc.presPinViewScale = scale;
- }
- this.hideMarquee();
- }
-
- @undoBatch
- @action
- pinWithView = async (e: KeyboardEvent | React.PointerEvent | undefined) => {
- const doc = this.props.Document;
- const curPres = Cast(Doc.UserDoc().activePresentation, Doc) as Doc;
- if (curPres) {
- if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; }
- const pinDoc = Doc.MakeAlias(doc);
- pinDoc.presentationTargetDoc = doc;
- pinDoc.presMovement = PresMovement.Zoom;
- pinDoc.groupWithUp = false;
- pinDoc.context = curPres;
- pinDoc.title = doc.title + " - Slide";
- pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
- pinDoc.treeViewHeaderWidth = "100%"; // forces the header to grow to be the same size as its largest sibling.
- pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
- pinDoc.treeViewFieldKey = "data"; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
- pinDoc.treeViewExpandedView = "data";// in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
- pinDoc.treeViewGrowsHorizontally = true;// the document expands horizontally when displayed as a tree view header
- pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
-
- const presArray = PresBox.Instance?.sortArray();
- const size = PresBox.Instance?._selectedArray.size;
- const presSelected = presArray && size ? presArray[size - 1] : undefined;
- Doc.AddDocToList(curPres, "data", pinDoc, presSelected);
- if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; const dview = CollectionDockingView.Instance.props.Document;
- const fieldKey = CollectionDockingView.Instance.props.fieldKey;
- const tabdocs = await DocListCastAsync(dview[fieldKey]);
- if (!tabdocs?.includes(curPres)) {
- tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs
- CollectionDockingView.AddSplit(curPres, "right");
- }
- PresBox.Instance?._selectedArray.clear();
- pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Updates selected array
- const index = PresBox.Instance?.childDocs.indexOf(pinDoc);
- index && (curPres._itemIndex = index);
- if (e instanceof KeyboardEvent ? e.key === "c" : true) {
- const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height);
- pinDoc.presPinView = true;
- pinDoc.presPinViewX = this.Bounds.left + this.Bounds.width / 2;
- pinDoc.presPinViewY = this.Bounds.top + this.Bounds.height / 2;
- pinDoc.presPinViewScale = scale;
- }
- }
- MarqueeOptionsMenu.Instance.fadeOut(true);
+ pinWithView = async () => {
+ const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height);
+ const doc = this.props.Document;
+ const viewOptions:PinViewProps = {
+ bounds: this.Bounds,
+ scale: scale
+ };
+ TabDocView.PinDoc(doc, {pinWithView: viewOptions});
+ MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
}
@@ -499,6 +434,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}));
this.props.removeDocument?.(selected);
}
+ // TODO: nda - this is the code to actually get a new grouped collection
const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t" ? Docs.Create.StackingDocument : undefined, group);
this.props.addDocument?.(newCollection);
this.props.selectDocuments([newCollection]);
@@ -581,7 +517,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.y = NumCast(d.y) - this.Bounds.top;
return d;
});
- const summary = Docs.Create.TextDocument("", { _backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
+ const summary = Docs.Create.TextDocument("", { _backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: "overview" });
const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: "transparent" });
DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summary of:summarized by", "");
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index c0a33a5e0..9b1cb1601 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -153,6 +153,7 @@ export class CollectionLinearView extends CollectionSubView() {
PanelWidth={nested ? doc[WidthSym] : this.dimension}
PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension}
renderDepth={this.props.renderDepth + 1}
+ dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)}
focus={emptyFunction}
styleProvider={this.props.styleProvider}
docViewPath={returnEmptyDoclist}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index e2dfb25e2..b7ba94940 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -257,7 +257,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
- fitContentsToDoc={this.props.fitContentsToDoc}
+ fitContentsToBox={this.props.fitContentsToBox}
PanelWidth={width}
PanelHeight={height}
rootSelected={this.rootSelected}
@@ -272,6 +272,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
+ dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 3010e36aa..338639a83 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -264,13 +264,14 @@ export class CollectionMultirowView extends CollectionSubView() {
isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
- fitContentsToDoc={this.props.fitContentsToDoc}
+ fitContentsToBox={this.props.fitContentsToBox}
focus={this.props.focus}
docFilters={this.childDocFilters}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
ContainingCollectionView={this.props.CollectionView}
+ dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index b731479a5..f45068b6a 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -402,7 +402,7 @@ export class CollectionSchemaView extends CollectionSubView() {
<DocumentView
Document={this.previewDocument}
DataDoc={undefined}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
freezeDimensions={true}
dontCenter={"y"}
focus={DocUtils.DefaultFocus}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 3ddcf803d..a75e7a0c4 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -102,6 +102,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
this.props.itemHandler?.(this.props.linkDoc);
} else {
+ const focusDoc = Cast(this.props.linkDoc.anchor1, Doc, null)?.annotationOn === this.props.sourceDoc ? Cast(this.props.linkDoc.anchor1, Doc, null) :
+ Cast(this.props.linkDoc.anchor2, Doc, null)?.annotationOn === this.props.sourceDoc ? Cast(this.props.linkDoc.anchor12, Doc, null) : undefined;
+
+ if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true);
LinkManager.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false);
}
});
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 1d06f368f..145b6d5a6 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -4,8 +4,6 @@ import { action, computed, IReactionDisposer, observable, runInAction } from "mo
import { observer } from "mobx-react";
import { DateField } from "../../../fields/DateField";
import { Doc, DocListCast } from "../../../fields/Doc";
-import { documentSchema } from "../../../fields/documentSchemas";
-import { makeInterface } from "../../../fields/Schema";
import { ComputedField } from "../../../fields/ScriptField";
import { Cast, DateCast, NumCast } from "../../../fields/Types";
import { AudioField, nullAudio } from "../../../fields/URLField";
@@ -296,11 +294,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// button for starting and stopping the recording
- Record = (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
@@ -338,27 +335,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// for dictation button, creates a text document for dictation
onFile = (e: any) => {
- 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");
- const overlayDoc = Doc.UserDoc().myOverlayDocs as Doc;
- if (DocListCast(overlayDoc[Doc.LayoutFieldKey(overlayDoc)]).includes(this.rootDoc)) {
- newDoc.x = this.rootDoc.x;
- newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddDocToList(overlayDoc, undefined, newDoc);
- } else {
- 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");
+ const overlayDoc = Doc.UserDoc().myOverlayDocs as Doc;
+ if (DocListCast(overlayDoc[Doc.LayoutFieldKey(overlayDoc)]).includes(this.rootDoc)) {
+ newDoc.x = this.rootDoc.x;
+ newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddDocToList(overlayDoc, undefined, newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ }), false);
}
@@ -371,21 +369,21 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// 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._pauseEnd = new Date().getTime();
+ this._paused = false;
+ this._recorder.resume();
+ }), false);
}
@@ -504,19 +502,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// UI for recording, initially displayed when new audio created in Dash
@computed get recordingControls() {
return <div className="audiobox-recorder">
- <div className="audiobox-dictation" onClick={this.onFile}>
+ <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" onClick={this.Record}>
+ <div className="record-button" onPointerDown={this.Record}>
<FontAwesomeIcon
size="2x"
icon="stop" />
</div>
- <div className="record-button" onClick={this._paused ? this.recordPlay : this.recordPause}>
+ <div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}>
<FontAwesomeIcon
size="2x"
icon={this._paused ? "play" : "pause"} />
@@ -526,7 +524,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
</div>
:
- <div className="audiobox-start-record">
+ <div className="audiobox-start-record" onPointerDown={this.Record}>
<FontAwesomeIcon icon="microphone" />
RECORD
</div>}
@@ -655,7 +653,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
ref={this.setupTimelineDrop}
className="audiobox-container"
onContextMenu={this.specificContextMenu}
- onClick={!this.path && !this._recorder ? this.recordAudioAnnotation : undefined}
style={{ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined }}
>
{!this.path ? this.recordingControls : this.playbackControls}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 5a0ab9110..bedc97575 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -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";
@@ -170,7 +170,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
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,
display: this.ZInd === -99 ? "none" : undefined
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d4e8ffc7f..70732e74c 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -41,6 +41,7 @@ import { WebBox } from "./WebBox";
import React = require("react");
import XRegExp = require("xregexp");
import { MapBox } from "./MapBox/MapBox";
+import { RecordingBox } from "./RecordingBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -225,7 +226,7 @@ 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
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 073f734c0..60d16f044 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -85,7 +85,7 @@ export interface DocComponentView {
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
@@ -112,7 +112,8 @@ export interface DocumentViewSharedProps {
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;
@@ -163,6 +164,9 @@ export interface DocumentViewProps extends DocumentViewSharedProps {
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
@@ -760,7 +764,7 @@ 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) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
!Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" });
const existingOnClick = cm.findByDescription("OnClick...");
const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
@@ -830,10 +834,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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.UserDoc().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.UserDoc().noviceMode && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
+ !Doc.UserDoc().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" });
}
@@ -956,7 +960,7 @@ 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]}>
@@ -1200,6 +1204,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@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; }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 79c1f1c40..67cf18d8b 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -7,6 +7,7 @@ import { List } from "../../../fields/List";
import { WebField } from "../../../fields/URLField";
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.
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 28834a202..e3708d7b9 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -206,7 +206,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
facetValues.strings.map(val => {
const num = val ? Number(val) : Number.NaN;
if (Number.isNaN(num)) {
- num && nonNumbers++;
+ val && nonNumbers++;
} else {
minVal = Math.min(num, minVal);
maxVal = Math.max(num, maxVal);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ab4ed6b33..8c27b3508 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -357,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, InkTool.Write].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.SelectedTool)) {
setupMoveUpEvents(this, e, action(e => {
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX, e.clientY];
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 152288d8b..6c7e174f7 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -121,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 = () => {
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 9f1c019fe..5f4c17ee6 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();
}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index db7dcb09d..bdf0f9d64 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -45,11 +45,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
_stack: CollectionStackingView | 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">
@@ -73,10 +71,10 @@ 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={returnAll}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index f2ca6c96e..4eb07fd8d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -26,6 +26,7 @@ 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>() {
@@ -147,34 +148,23 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
updateIcon = () => {
- return; // currently we render pdf icons as text labels
+ // currently we render pdf icons as text labels
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 nativeWidth = this.layoutDoc[WidthSym]();
- const nativeHeight = this.layoutDoc[HeightSym]();
-
- CreateImage(
- "",
- document.styleSheets,
- htmlString,
- nativeWidth,
- nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
- NumCast(this.layoutDoc._scrollTop) * this.props.PanelHeight() / NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"])
- ).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.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc["icon-nativeWidth"] = nativeWidth;
- this.dataDoc["icon-nativeHeight"] = nativeHeight;
- }), 500));
- })
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
+ 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);
});
}
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.tsx b/src/client/views/nodes/VideoBox.tsx
index ef3b0d105..5d2fae1ed 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -7,7 +7,7 @@ import * as rp from 'request-promise';
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { InkTool } from "../../../fields/InkField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
+import { AudioField, ImageField, RecordingField, 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";
@@ -28,8 +28,9 @@ import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
import "./VideoBox.scss";
-import { Image } from "wikijs";
import { List } from "../../../fields/List";
+import { RecordingApi } from "../../util/RecordingApi";
+import { RecordingBox } from "./RecordingBox";
const path = require('path');
/**
@@ -46,936 +47,1264 @@ const path = require('path');
@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 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; // <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 _audioPlayer: HTMLAudioElement | null = null;
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
- private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- private _playRegionTimer: any = null; // timeout for playback
- private _controlsFadeTimer: any = null; // timeout for controls fade
-
- public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
-
- @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; // 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;
-
- @computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
- // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
- @observable rawDuration: number = 0;
-
-
- @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 ?? "";
- }
-
-
- @computed private get timeline() { return this._stackedTimeline; }
- private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
- public get player(): HTMLVideoElement | null { return this._videoRef; }
-
-
- componentDidMount() {
- this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
- if (this.youtubeVideoId) {
- const youtubeaspect = 400 / 315;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
- }
- }
- this.player && this.setPlayheadTime(0);
- }
-
- componentWillUnmount() {
- this.removeCurrentlyPlaying();
- this.Pause();
- Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
- }
-
-
- // plays video
- @action public Play = (update: boolean = true) => {
- if (this._playRegionTimer) return;
- this._playing = true;
- 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;
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
+ /**
+ * Uploads an image buffer to the server and stores with specified filename. by default the image
+ * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+ * @param imageUri the bytes of the image
+ * @param returnedFilename the base filename to store the image on the server
+ * @param nosuffix optionally suppress creating multiple resolution images
+ */
+ public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+ try {
+ const posting = Utils.prepend("/uploadURI");
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log("VideoBox :" + e);
+ }
+ }
+
+ static _youtubeIframeCounter: number = 0;
+ static heightPercent = 80; // height of video relative to videoBox when timeline is open
+ static numThumbnails = 20;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _youtubePlayer: YT.Player | undefined = undefined;
+ 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 _audioPlayer: HTMLAudioElement | null = null;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _playRegionTimer: any = null; // timeout for playback
+ private _controlsFadeTimer: any = null; // timeout for controls fade
+
+ public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
+
+ @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; // 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;
+
+ @computed get links() { return DocListCast(this.dataDoc.links); }
+ @computed get heightPercent() { return NumCast(this.layoutDoc._timelineHeightPercent, 100); } // current percent of video relative to VideoBox height
+ // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ @observable rawDuration: number = 0;
+
+
+ @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 ?? "";
+ }
+
+
+ @computed private get timeline() { return this._stackedTimeline; }
+ private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
+ public get player(): HTMLVideoElement | null { return this._videoRef; }
+
+
+ componentDidMount() {
+ this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
+ if (this.youtubeVideoId) {
+ const youtubeaspect = 400 / 315;
+ const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+ const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+ if (!nativeWidth || !nativeHeight) {
+ if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+ Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
+ }
+ }
+ this.player && this.setPlayheadTime(0);
+ }
+
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this.Pause();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ }
+
+
+ // plays video
+ @action public Play = (update: boolean = true) => {
+ if (this._playRegionTimer) return;
+ this._playing = true;
+ 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 {
+ const posting = Utils.prepend("/uploadURI");
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log("VideoBox :" + e);
+ }
}
+ static _youtubeIframeCounter: number = 0;
+ static heightPercent = 80; // height of video relative to videoBox when timeline is open
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _youtubePlayer: YT.Player | undefined = undefined;
+ 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 _audioPlayer: HTMLAudioElement | null = null;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); // outermost div
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _playRegionTimer: any = null; // timeout for playback
+ @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; // 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;
+
+ // pauses video
+ @action public Pause = (update: boolean = true) => {
+ this._playing = false;
+ this.removeCurrentlyPlaying();
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));
+ update && this.player?.pause();
+ update && this._audioPlayer?.pause();
+ update && this._youtubePlayer?.pauseVideo();
+ this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
+ this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
} catch (e) {
- console.log("Video Play Exception:", e);
- }
- }
- this.updateTimecode();
- }
-
- // goes to time
- @action public Seek(time: number) {
- try {
- this._youtubePlayer?.seekTo(Math.round(time), true);
- } catch (e) {
- console.log("Video Seek Exception:", e);
- }
- this.player && (this.player.currentTime = time);
- this._audioPlayer && (this._audioPlayer.currentTime = time);
- }
-
- // pauses video
- @action public Pause = (update: boolean = true) => {
- this._playing = false;
- this.removeCurrentlyPlaying();
- try {
- update && this.player?.pause();
- update && this._audioPlayer?.pause();
- update && this._youtubePlayer?.pauseVideo();
- this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
- this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
- } catch (e) {
- console.log("Video Pause Exception:", e);
- }
- this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
- this._playTimer = undefined;
- this.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 = () => {
- 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) {
- console.log("Video FullScreen Exception:", e);
- }
- }
-
- // fades out controls in fullscreen after mouse stops moving
- @action controlsFade = (e: PointerEvent) => {
- e.stopPropagation();
- this._controlsVisible = true;
- clearTimeout(this._controlsFadeTimer);
- 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) {
- this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
- }
-
- if (!this._videoRef) {
- const b = Docs.Create.LabelDocument({
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
- _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true
- });
- this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
- Networking.PostToServer("/youtubeScreenshot", {
- id: this.youtubeVideoId,
- timecode: this.layoutDoc._currentTimecode
- }).then(response => {
- const resolved = response?.accessPaths?.agnostic?.client;
- if (resolved) {
- this.props.removeDocument?.(b);
- this.createRealSummaryLink(resolved);
- }
- });
- } else {
- //convert to desired file format
- const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
- // if you want to preview the captured image,
- const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, "");
- const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
- const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
- returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
- }
- }
-
- updateIcon = () => {
- const makeIcon = (returnedfilename: string) => {
- this.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
- this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
- };
- this.Snapshot(undefined, undefined, makeIcon);
- }
-
- // creates link for snapshot
- createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
- const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
- const width = NumCast(this.layoutDoc._width) || 1;
- const height = NumCast(this.layoutDoc._height);
- const imageSummary = Docs.Create.ImageDocument(url, {
- _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
- x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
- _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
- });
- Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
- Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
- link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() =>
- (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
- }
-
-
- getAnchor = () => {
- const timecode = Cast(this.layoutDoc._currentTimecode, "number", null);
- const marquee = AnchorMenu.Instance.GetAnchor?.();
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow"/* videoStart */, "_timecodeToHide" /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
- }
-
-
- // sets video info on load
- videoLoad = action(() => {
- const aspect = this.player!.videoWidth / this.player!.videoHeight;
- Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth);
- Doc.SetNativeHeight(this.dataDoc, this.player!.videoHeight);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / aspect;
- if (Number.isFinite(this.player!.duration)) {
- this.rawDuration = this.player!.duration;
- } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + "-duration"]);
- });
-
-
- // updates video time
- @action
- updateTimecode = () => {
- this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
- try {
- this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
- } catch (e) {
- console.log("Video Timecode Exception:", e);
- }
- }
-
-
- // 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;
+ console.log("Video Pause Exception:", e);
+ }
+ this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
+ this._playTimer = undefined;
+ this.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 = () => {
+ if (document.fullscreenElement === this._contentRef) {
+ this._fullScreen = false;
+ this.player && this._contentRef && document.exitFullscreen();
}
else {
- Promise.all(thumbnailPromises).then(thumbnails => { this.dataDoc.thumbnails = new List<string>(thumbnails); });
- }
- }
-
- 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);
- 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();
- }
- }
-
- // 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);
- if (this._fullScreen) {
- document.addEventListener('pointermove', this.controlsFade);
- this._controlsVisible = true;
- this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000)
- }
- else {
- document.removeEventListener('pointermove', this.controlsFade);
- }
+ this._fullScreen = true;
+ this.player && this._contentRef && this._contentRef.requestFullscreen();
+ }
+ try {
+ this._youtubePlayer && this.props.addDocTab(this.rootDoc, "add");
+ } catch (e) {
+ console.log("Video FullScreen Exception:", e);
+ }
+ }
+
+ // fades out controls in fullscreen after mouse stops moving
+ @action controlsFade = (e: PointerEvent) => {
+ e.stopPropagation();
+ this._controlsVisible = true;
+ clearTimeout(this._controlsFadeTimer);
+ 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) {
+ this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+ }
+
+ if (!this._videoRef) {
+ const b = Docs.Create.LabelDocument({
+ x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
+ _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
+ _isLinkButton: true
+ });
+ this.props.addDocument?.(b);
+ DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
+ Networking.PostToServer("/youtubeScreenshot", {
+ id: this.youtubeVideoId,
+ timecode: this.layoutDoc._currentTimecode
+ }).then(response => {
+ const resolved = response?.accessPaths?.agnostic?.client;
+ if (resolved) {
+ this.props.removeDocument?.(b);
+ this.createRealSummaryLink(resolved);
+ }
+ });
+ } else {
+ //convert to desired file format
+ const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, "");
+ const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
+ const filename = basename(encodedFilename);
+ VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
+ returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ }
+ }
+
+ updateIcon = () => {
+ const makeIcon = (returnedfilename: string) => {
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
+ this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+ };
+ this.Snapshot(undefined, undefined, makeIcon);
+ }
+
+ // creates link for snapshot
+ createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+ const width = NumCast(this.layoutDoc._width) || 1;
+ const height = NumCast(this.layoutDoc._height);
+ const imageSummary = Docs.Create.ImageDocument(url, {
+ _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+ x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
+ _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
});
- }
- }
-
-
- // context menu
- specificContextMenu = (e: React.MouseEvent): void => {
- const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
- if (field) {
- const url = field.url.href;
- const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
- subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
- this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
- description: "Screen Capture", event: (async () => {
- runInAction(() => this._screenCapture = !this._screenCapture);
- this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
- }), icon: "expand-arrows-alt"
+ Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSummary);
+ const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
+ link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
+ setTimeout(() =>
+ (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
+ }
+
+
+ // 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; }
+
+
+ // updates video time
+ @action
+ updateTimecode = () => {
+ this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+ try {
+ this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+ } catch (e) {
+ console.log("Video Timecode Exception:", e);
+ }
+ }
+
+
+ // 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); });
+ }
+ }
+
+ 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);
+ 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();
+ }
+ }
+
+ // 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);
+ if (this._fullScreen) {
+ document.addEventListener('pointermove', this.controlsFade);
+ this._controlsVisible = true;
+ this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000)
+ }
+ else {
+ document.removeEventListener('pointermove', this.controlsFade);
+ }
+ });
+ }
+ }
+
+
+ // context menu
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+ if (field) {
+ const url = field.url.href;
+ const subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Full Screen", event: this.FullScreen, icon: "expand" });
+ subitems.push({ description: "Take Snapshot", event: this.Snapshot, icon: "expand-arrows-alt" });
+ this.rootDoc.type === DocumentType.SCREENSHOT && subitems.push({
+ description: "Screen Capture", event: (async () => {
+ runInAction(() => this._screenCapture = !this._screenCapture);
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ }), icon: "expand-arrows-alt"
+ });
+ subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
+ subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
+ subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
+ // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
+ subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
+ ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
+ }
+ }
+
+
+ // ref for updating time
+ setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
+
+ // renders the video and audio
+ @computed get content() {
+ const field = Cast(this.dataDoc[this.fieldKey], VideoField);
+ const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
+ const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+ return !field ? <div key="loading">Loading</div> :
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", 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 ? 'visible' : 'hidden', opacity: this._controlsVisible ? 1 : 0 }}>
+ {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={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}>
+ <source src={field.url.href} type="video/mp4" />
+ Not supported.
+ </video>
+ {!this.audiopath || this.audiopath === field.url.href ? (null) :
+ <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
+ <source src={this.audiopath} type="audio/mpeg" />
+ Not supported.
+ </audio>}
+ </div>
+ </div>;
+ }
+
+
+ @action youtubeIframeLoaded = (e: any) => {
+ if (!this._youtubeContentCreated) {
+ this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
+ return;
+ }
+ else this._youtubeContentCreated = false;
+
+ this.loadYouTube(e.target);
+ }
+
+ loadYouTube = (iframe: any) => {
+ let started = true;
+ const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+ if (started && event.data === YT.PlayerState.PLAYING) {
+ started = false;
+ this._youtubePlayer?.unMute();
+ //this.Pause();
+ return;
+ }
+ this.player && this.setPlayheadTime(0);
+ }
+
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this.Pause();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ }
+
+
+ // plays video
+ @action public Play = (update: boolean = true) => {
+ // 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;
+ const eleTime = this.player?.currentTime || 0;
+ if (this.timeline) {
+ let start = eleTime >= this.timeline.trimEnd || eleTime <= this.timeline.trimStart ? this.timeline.trimStart : eleTime;
+
+ if (this._finished) {
+ // restarts video if reached end on previous play
+ this._finished = false;
+ start = this.timeline.trimStart;
+ }
+
+ try {
+ this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
+ update && this.player && this.playFrom(start, undefined, true);
+ update && this._audioPlayer?.play();
+ update && this._youtubePlayer?.playVideo();
+ this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
+ } catch (e) {
+ console.log("Video Play Exception:", e);
+ }
+ }
+ this.updateTimecode();
+ }
+
+ // goes to time
+ @action public Seek(time: number) {
+ try {
+ this._youtubePlayer?.seekTo(Math.round(time), true);
+ } catch (e) {
+ console.log("Video Seek Exception:", e);
+ }
+ 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();
+ update && this._youtubePlayer?.pauseVideo();
+ this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
+ this._youtubePlayer?.seekTo(this._youtubePlayer?.getCurrentTime(), true);
+ } catch (e) {
+ console.log("Video Pause Exception:", e);
+ }
+ this._youtubePlayer && SelectionManager.DeselectAll(); // if we don't deselect the player, then we get an annoying YouTube spinner I guess telling us we're paused.
+ this._playTimer = undefined;
+ this.updateTimecode();
+ if (!this._finished) clearTimeout(this._playRegionTimer); // if paused in the middle of playback, prevents restart on next play
+ }
+
+ // toggles video full screen
+ @action public FullScreen = () => {
+ 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) {
+ console.log("Video FullScreen Exception:", e);
+ }
+ }
+
+
+ // 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) {
+ this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+ }
+
+ if (!this._videoRef) {
+ const b = Docs.Create.LabelDocument({
+ x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y, 1),
+ _width: 150, _height: 50, title: (this.layoutDoc._currentTimecode || 0).toString(),
+ _isLinkButton: true
+ });
+ this.props.addDocument?.(b);
+ DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, "video snapshot");
+ Networking.PostToServer("/youtubeScreenshot", {
+ id: this.youtubeVideoId,
+ timecode: this.layoutDoc._currentTimecode
+ }).then(response => {
+ const resolved = response?.accessPaths?.agnostic?.client;
+ if (resolved) {
+ this.props.removeDocument?.(b);
+ this.createRealSummaryLink(resolved);
+ }
+ });
+ } else {
+ //convert to desired file format
+ const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+ const retitled = StrCast(this.rootDoc.title).replace(/[ -\.]/g, "");
+ const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
+ const filename = basename(encodedFilename);
+ VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
+ returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ }
+ }
+
+ updateIcon = () => {
+ const makeIcon = (returnedfilename: string) => {
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
+ this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+ };
+ this.Snapshot(undefined, undefined, makeIcon);
+ }
+
+ // creates link for snapshot
+ createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
+ const width = NumCast(this.layoutDoc._width) || 1;
+ const height = NumCast(this.layoutDoc._height);
+ const imageSummary = Docs.Create.ImageDocument(url, {
+ _nativeWidth: Doc.NativeWidth(this.layoutDoc), _nativeHeight: Doc.NativeHeight(this.layoutDoc),
+ x: NumCast(this.layoutDoc.x) + width, y: NumCast(this.layoutDoc.y), _isLinkButton: true,
+ _width: 150, _height: height / width * 150, title: "--snapshot" + NumCast(this.layoutDoc._currentTimecode) + " image-"
+ });
+ Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSummary);
+ const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, "video snapshot");
+ link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
+ setTimeout(() =>
+ (downX !== undefined && downY !== undefined) && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, "move", true));
+ }
+
+
+ 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"]);
});
- subitems.push({ description: (this.layoutDoc.dontAutoFollowLinks ? "" : "Don't") + " follow links when encountered", event: () => this.layoutDoc.dontAutoFollowLinks = !this.layoutDoc.dontAutoFollowLinks, icon: "expand-arrows-alt" });
- subitems.push({ description: (this.layoutDoc.dontAutoPlayFollowedLinks ? "" : "Don't") + " play when link is selected", event: () => this.layoutDoc.dontAutoPlayFollowedLinks = !this.layoutDoc.dontAutoPlayFollowedLinks, icon: "expand-arrows-alt" });
- subitems.push({ description: (this.layoutDoc.autoPlayAnchors ? "Don't auto play" : "Auto play") + " anchors onClick", event: () => this.layoutDoc.autoPlayAnchors = !this.layoutDoc.autoPlayAnchors, icon: "expand-arrows-alt" });
- // subitems.push({ description: "Toggle Native Controls", event: action(() => VideoBox._nativeControls = !VideoBox._nativeControls), icon: "expand-arrows-alt" });
- // subitems.push({ description: "Start Trim All", event: () => this.startTrim(TrimScope.All), icon: "expand-arrows-alt" });
- // subitems.push({ description: "Start Trim Clip", event: () => this.startTrim(TrimScope.Clip), icon: "expand-arrows-alt" });
- // subitems.push({ description: "Stop Trim", event: () => this.finishTrim(), icon: "expand-arrows-alt" });
- subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
- }
- }
-
-
- // ref for updating time
- setAudioRef = (e: HTMLAudioElement | null) => this._audioPlayer = e;
-
- // renders the video and audio
- @computed get content() {
- const field = Cast(this.dataDoc[this.fieldKey], VideoField);
- const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
- return !field ? <div key="loading">Loading</div> :
- <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", 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 ? 'visible' : 'hidden', opacity: this._controlsVisible ? 1 : 0 }}>
- {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={this._fullScreen ? () => this.playing() ? this.Pause() : this.Play() : e => e.preventDefault()}>
- <source src={field.url.href} type="video/mp4" />
- Not supported.
- </video>
- {!this.audiopath || this.audiopath === field.url.href ? (null) :
- <audio ref={this.setAudioRef} className={`audiobox-control${this.props.isContentActive() ? "-interactive" : ""}`}>
- <source src={this.audiopath} type="audio/mpeg" />
- Not supported.
- </audio>}
- </div>
- </div>;
- }
-
-
- @action youtubeIframeLoaded = (e: any) => {
- if (!this._youtubeContentCreated) {
- this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
- return;
- }
- else this._youtubeContentCreated = false;
-
- this.loadYouTube(e.target);
- }
-
- loadYouTube = (iframe: any) => {
- let started = true;
- const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
- if (started && event.data === YT.PlayerState.PLAYING) {
- started = false;
- this._youtubePlayer?.unMute();
- //this.Pause();
- return;
- }
- if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
- });
- const onYoutubePlayerReady = (event: any) => {
- this._disposers.reactionDisposer?.();
- this._disposers.youtubeReactionDisposer?.();
- this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
- this._disposers.youtubeReactionDisposer = reaction(
- () => CurrentUserUtils.SelectedTool === 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);
- else {
- (YT as any)?.ready(() => {
- this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
- events: {
- 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
- 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
- }
- });
+
+
+ // updates video time
+ @action
+ updateTimecode = () => {
+ this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+ try {
+ this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+ } catch (e) {
+ console.log("Video Timecode Exception:", e);
+ }
+ }
+
+
+ // 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;
+ 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" : "" }}>
+ {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={(e: React.PointerEvent) => { e.stopPropagation(); }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ />
+ </div>
+ :
+ <div>/</div>}
+
+ <div className="timecode-end">
+ {formatTime(this.timeline.clipDuration)}
+ </div>
+ </div>
+ }
+
+ }
+ ContextMenu.Instance.addItem({description: "Options...", subitems: subitems, icon: "video" });
+ }
+ }
+
+ {
+ !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)); }}
+ />
+ </>
+ }
+ </>
+ }
+
+ @action youtubeIframeLoaded = (e: any) => {
+ if (!this._youtubeContentCreated) {
+ this._forceCreateYouTubeIFrame = !this._forceCreateYouTubeIFrame;
+ return;
+ }
+ else this._youtubeContentCreated = false;
+
+ this.loadYouTube(e.target);
+ }
+
+ loadYouTube = (iframe: any) => {
+ let started = true;
+ const onYoutubePlayerStateChange = (event: any) => runInAction(() => {
+ if (started && event.data === YT.PlayerState.PLAYING) {
+ started = false;
+ this._youtubePlayer?.unMute();
+ //this.Pause();
+ return;
+ }
+ if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
+ });
+ const onYoutubePlayerReady = (event: any) => {
+ this._disposers.reactionDisposer?.();
+ this._disposers.youtubeReactionDisposer?.();
+ this._disposers.reactionDisposer = reaction(() => this.layoutDoc._currentTimecode, () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)));
+ this._disposers.youtubeReactionDisposer = reaction(
+ () => CurrentUserUtils.SelectedTool === 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);
+ else {
+ (YT as any)?.ready(() => {
+ this._youtubePlayer = new YT.Player(`${this.youtubeVideoId + this._youtubeIframeId}-player`, {
+ events: {
+ 'onReady': this.props.dontRegisterView ? undefined : onYoutubePlayerReady,
+ 'onStateChange': this.props.dontRegisterView ? undefined : onYoutubePlayerStateChange,
+ }
+ });
+ });
+ }
+ }
+
+
+ // for play button
+
+ onPlayDown = () => {
+ console.log("PLAY DOWN");
+ this._playing ? this.Pause() : this.Play();
+ }
+
+ // for fullscreen button
+ onFullDown = (e: React.PointerEvent) => {
+ this.FullScreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ // for snapshot button
+ onSnapshotDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e) => {
+ this.Snapshot(e.clientX, e.clientY);
+ return true;
+ }, emptyFunction, () => this.Snapshot());
+ }
+
+ // for show/hide timeline button, transitions between show/hide
+ @action
+ onTimelineHdlDown = (e: React.PointerEvent) => {
+ this._clicking = true;
+ setupMoveUpEvents(this, e,
+ action(encodeURIComponent => {
+ this._clicking = false;
+ if (this.props.isContentActive()) {
+ // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
+ // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+
+ this.layoutDoc._timelineHeightPercent = 80;
+ }
+ return false;
+ }), emptyFunction,
+ () => {
+ this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+ setTimeout(action(() => this._clicking = false), 500);
+ }, this.props.isContentActive(), this.props.isContentActive());
+ }
+
+
+ // removes video from currently playing display
+ @action
+ removeCurrentlyPlaying = () => {
+ if (CollectionStackedTimeline.CurrentlyPlaying) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
+ }
+ }
+
+ // adds video to currently playing display
+ @action
+ addCurrentlyPlaying = () => {
+ if (!CollectionStackedTimeline.CurrentlyPlaying) {
+ CollectionStackedTimeline.CurrentlyPlaying = [];
+ }
+ if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ }
+ }
+
+
+ @computed get youtubeContent() {
+ this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
+ this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
+ const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
+ return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
+ onPointerLeave={this.updateTimecode}
+ onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
+ }
+
+
+ // for annotating, adds doc with time info
+ @action.bound
+ addDocWithTimecode(doc: Doc | Doc[]): boolean {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ const curTime = NumCast(this.layoutDoc._currentTimecode);
+ docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
+ return this.addDocument(doc);
+ }
+
+
+ // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
+ @action
+ playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
+ clearTimeout(this._playRegionTimer);
+ if (Number.isNaN(this.player?.duration)) {
+ setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
+ }
+ 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();
+ 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) {
+ Doc.UserDoc().presentationMode = 'none';
+ 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();
});
- }
- }
-
-
- // for play button
- onPlayDown = () => this._playing ? this.Pause() : this.Play();
-
- // for fullscreen button
- onFullDown = (e: React.PointerEvent) => {
- this.FullScreen();
- e.stopPropagation();
- e.preventDefault();
- }
-
- // for snapshot button
- onSnapshotDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, (e) => {
- this.Snapshot(e.clientX, e.clientY);
- return true;
- }, emptyFunction, () => this.Snapshot());
- }
-
- // for show/hide timeline button, transitions between show/hide
- @action
- onTimelineHdlDown = (e: React.PointerEvent) => {
- this._clicking = true;
- setupMoveUpEvents(this, e,
- action(encodeURIComponent => {
- this._clicking = false;
- if (this.props.isContentActive()) {
- // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
- // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
-
- this.layoutDoc._timelineHeightPercent = 80;
- }
- return false;
- }), emptyFunction,
- () => {
- this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
- setTimeout(action(() => this._clicking = false), 500);
- }, this.props.isContentActive(), this.props.isContentActive());
- }
-
-
- // removes video from currently playing display
- @action
- removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
- index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
- }
- }
-
- // adds video to currently playing display
- @action
- addCurrentlyPlaying = () => {
- if (!CollectionStackedTimeline.CurrentlyPlaying) {
- CollectionStackedTimeline.CurrentlyPlaying = [];
- }
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
- }
- }
-
-
- @computed get youtubeContent() {
- this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
- this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
- const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
- return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
- }
-
-
- // for annotating, adds doc with time info
- @action.bound
- addDocWithTimecode(doc: Doc | Doc[]): boolean {
- const docs = doc instanceof Doc ? [doc] : doc;
- const curTime = NumCast(this.layoutDoc._currentTimecode);
- docs.forEach(doc => doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1);
- return this.addDocument(doc);
- }
-
-
- // play back the audio from seekTimeInSeconds, fullPlay tells whether clip is being played to end vs link range
- @action
- playFrom = (seekTimeInSeconds: number, endTime?: number, fullPlay: boolean = false) => {
- clearTimeout(this._playRegionTimer);
- this._playRegionTimer = undefined;
- if (Number.isNaN(this.player?.duration)) {
- setTimeout(() => this.playFrom(seekTimeInSeconds, endTime), 500);
- }
- 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();
- 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);
+
+ // 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;
+ }
}
- }));
- }
-
-
- // 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?.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);
- }
- }
-
-
- // starts marquee selection
- marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
- }
- }
-
- // ends marquee selection
- @action
- finishMarquee = () => {
- this._marqueeing = undefined;
- this.props.select(true);
- }
-
- timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
-
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
-
- setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
-
- timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
-
- playing = () => this._playing;
-
- 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;
- 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" : "" }}>
- {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={(e: React.PointerEvent) => { e.stopPropagation(); }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+
+
+ // 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?.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);
+ }
+ }
+
+
+ // starts marquee selection
+ marqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
+
+ // ends marquee selection
+ @action
+ finishMarquee = () => {
+ this._marqueeing = undefined;
+ this.props.select(true);
+ }
+
+ timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+
+ setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
+ timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
+ playing = () => this._playing;
+
+ 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
+ @computed get uIButtons() {
+ const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ return <div className="videoBox-ui" style={this._fullScreen || this.heightPercent === 100 ? { fontSize: "40px", minWidth: "80%" } : {}}>
+ <div className="videobox-button"
+ title={this._playing ? "play" : "pause"}
+ onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
+ </div>
+
+ {this.timeline && <div className="timecode-controls">
+ <div className="timecode-current">
+ {formatTime(curTime)}
+ </div>
+
+ {this._fullScreen || this.heightPercent === 100 ?
+ <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={(e: React.PointerEvent) => { e.stopPropagation(); }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ />
+ </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 && <div className="videobox-button"
+ title={"show timeline"}
+ onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" />
+ </div>}
+
+ {!this._fullScreen && <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 show-slider"
+ 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>}
-
- <div className="timecode-end">
- {formatTime(this.timeline.clipDuration)}
- </div>
- </div>
- }
-
- <div className="videobox-button"
- title={"full screen"}
- onPointerDown={this.onFullDown}>
- <FontAwesomeIcon icon="expand" />
- </div>
-
- {
- !this._fullScreen && width > 300 && <div className="videobox-button"
- title={"show timeline"}
- onPointerDown={this.onTimelineHdlDown}>
- <FontAwesomeIcon icon="eye" />
- </div>
- }
-
- {
- !this._fullScreen && width > 300 && <div className="videobox-button"
- title={this.timeline?.IsTrimming !== TrimScope.None ? "finish trimming" : "start trim"}
- onPointerDown={this.onClipPointerDown}>
- <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? "check" : "cut"} />
- </div>
- }
-
- <div className="videobox-button"
- title={this._muted ? "unmute" : "mute"}
- onPointerDown={(e) => { e.stopPropagation(); this.toggleMute(); }}>
- <FontAwesomeIcon icon={this._muted ? "volume-mute" : "volume-up"} />
- </div>
- {
- width > 300 && <input type="range" style={{ width: `min(25%, 50px)` }} step="0.1" min="0" max="1" value={this._muted ? 0 : this._volume}
- className="toolbar-slider volume"
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
- />
- }
-
- {
- !this._fullScreen && this.heightPercent !== 100 && width > 300 &&
- <>
- <div className="videobox-button" title="zoom">
- <FontAwesomeIcon icon="search-plus" />
- </div>
- <input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
- className="toolbar-slider zoom"
- onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => { this.zoom(Number(e.target.value)); }}
- />
- </>
- }
- </>
- }
-
- // renders CollectionStackedTimeline
- @computed get renderTimeline() {
- return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
- <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
- fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + "-dictation"}
- mediaPath={this.audiopath}
- renderDepth={this.props.renderDepth + 1}
- startTag={"_timecodeToShow" /* videoStart */}
- endTag={"_timecodeToHide" /* videoEnd */}
- bringToFront={emptyFunction}
- CollectionView={undefined}
- playFrom={this.playFrom}
- setTime={this.setPlayheadTime}
- playing={this.playing}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}
- removeDocument={this.removeDocument}
- ScreenToLocalTransform={this.timelineScreenToLocal}
- Play={this.Play}
- Pause={this.Pause}
- playLink={this.playLink}
- PanelHeight={this.timelineHeight}
- rawDuration={this.rawDuration}
- />
- </div>;
- }
-
- // renders annotation layer
- @computed get annotationLayer() {
- return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
- }
-
- savedAnnotations = () => this._savedAnnotations;
- render() {
- const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
- const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
- return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
- style={{
- pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined,
- borderRadius,
- overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
- }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
- <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
- <div style={{
- position: "absolute", transition: this.transition,
- width: this.panelWidth(),
- height: this.panelHeight(),
- top: 0,
- left: (this.props.PanelWidth() - this.panelWidth()) / 2
- }}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- renderDepth={this.props.renderDepth + 1}
- fieldKey={this.annotationKey}
- CollectionView={undefined}
- isAnnotationOverlay={true}
- annotationLayerHostsContent={true}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- ScreenToLocalTransform={this.screenToLocalTransform}
- docFilters={this.timelineDocFilter}
- select={emptyFunction}
- scaling={returnOne}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
- </CollectionFreeFormView>
- </div>
- {this.annotationLayer}
- {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
- <MarqueeAnnotator
- rootDoc={this.rootDoc}
- scrollTop={0}
- down={this._marqueeing}
- scaling={this.marqueeFitScaling}
- docView={this.props.docViewPath().slice(-1)[0]}
- containerOffset={this.marqueeOffset}
- addDocument={this.addDocWithTimecode}
- finishMarquee={this.finishMarquee}
- savedAnnotations={this.savedAnnotations}
- annotationLayer={this._annotationLayer.current}
- mainCont={this._mainCont.current}
- />}
- {this.renderTimeline}
- </div>
- </div >);
- }
-}
+
+ {!this._fullScreen && this.heightPercent !== 100 &&
+ <>
+ <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)); }}
+ />
+ </>}
+ </div>;
+ }
+
+ // renders CollectionStackedTimeline
+ @computed get renderTimeline() {
+ return <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+ <CollectionStackedTimeline ref={action((r: any) => this._stackedTimeline = r)} {...this.props}
+ fieldKey={this.annotationKey}
+ dictationKey={this.fieldKey + "-dictation"}
+ mediaPath={this.audiopath}
+ renderDepth={this.props.renderDepth + 1}
+ startTag={"_timecodeToShow" /* videoStart */}
+ endTag={"_timecodeToHide" /* videoEnd */}
+ bringToFront={emptyFunction}
+ CollectionView={undefined}
+ playFrom={this.playFrom}
+ setTime={this.setPlayheadTime}
+ playing={this.playing}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.timelineScreenToLocal}
+ Play={this.Play}
+ Pause={this.Pause}
+ playLink={this.playLink}
+ PanelHeight={this.timelineHeight}
+ rawDuration={this.rawDuration}
+ />
+ </div>;
+ }
+
+ // renders annotation layer
+ @computed get annotationLayer() {
+ return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
+ }
+
+ savedAnnotations = () => this._savedAnnotations;
+ render() {
+ const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
+ const borderRadius = borderRad?.includes("px") ? `${Number(borderRad.split("px")[0]) / this.scaling()}px` : borderRad;
+ return (<div className="videoBox" onContextMenu={this.specificContextMenu} ref={this._mainCont}
+ style={{
+ pointerEvents: this.layoutDoc._lockedPosition ? "none" : undefined,
+ borderRadius,
+ overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? "auto" : undefined
+ }} onWheel={e => { e.stopPropagation(); e.preventDefault(); }}>
+ <div className="videoBox-viewer" onPointerDown={this.marqueeDown} >
+ <div style={{
+ position: "absolute", transition: this.transition,
+ width: this.panelWidth(),
+ height: this.panelHeight(),
+ top: 0,
+ left: (this.props.PanelWidth() - this.panelWidth()) / 2
+ }}>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ renderDepth={this.props.renderDepth + 1}
+ fieldKey={this.annotationKey}
+ CollectionView={undefined}
+ isAnnotationOverlay={true}
+ annotationLayerHostsContent={true}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ docFilters={this.timelineDocFilter}
+ select={emptyFunction}
+ scaling={returnOne}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocWithTimecode}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ {this.annotationLayer}
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? (null) :
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={this.marqueeFitScaling}
+ docView={this.props.docViewPath().slice(-1)[0]}
+ containerOffset={this.marqueeOffset}
+ addDocument={this.addDocWithTimecode}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ mainCont={this._mainCont.current}
+ />}
+ {this.renderTimeline}
+ </div>
+ </div >);
+ }
+ }
VideoBox._nativeControls = false; \ No newline at end of file
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index d9524dd6e..f3f1bcf5c 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -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) {
diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
index cf86b5e07..df17d603f 100644
--- a/src/client/views/nodes/button/FontIconBadge.tsx
+++ b/src/client/views/nodes/button/FontIconBadge.tsx
@@ -6,31 +6,31 @@ import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils
import { DragManager } from "../../../util/DragManager";
import "./FontIconBadge.scss";
-interface FontIconBadgeProps {
- collection: Doc | undefined;
+interface FontIconBadgeProps {
+ 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.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 42c650a09..97e6eddfe 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -521,7 +521,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={ScriptCast(this.Document.badgeValue,null)?.script.run({self:this.Document}).result} />
</div >
);
break;
@@ -708,8 +708,6 @@ ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) {
});
-
-
export function checkInksToGroup() {
// console.log("getting here to inks group");
if (CurrentUserUtils.SelectedTool === InkTool.Write) {
@@ -793,8 +791,7 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
**/
ScriptingGlobals.add(function setActiveInkTool(tool: string, checkResult?: boolean) {
- createInkGroup();
-
+ InkTranscription.Instance?.createInkGroup();
if (checkResult) {
return ((Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool) ?
Colors.MEDIUM_BLUE : "transparent";
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 7a500ac88..16a523b40 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -255,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("#")) {
@@ -382,11 +382,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
- // creates links between terms in a document and documents which have a matching Id
+ // 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 autoLinkTerm = StrCast(target.title).replace(/^@/, "");
const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
var alink: Doc | undefined;
flattened1.forEach((flat, i) => {
@@ -495,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](),
@@ -1411,9 +1411,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)));
}
@@ -1504,9 +1504,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();
@@ -1523,7 +1523,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();
@@ -1557,7 +1557,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();
@@ -1632,7 +1632,7 @@ 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`} />;
};
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index fb49b0698..c66cb502e 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -44,7 +44,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
}
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++) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e32f2826f..1dd6fef9b 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, 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
@@ -98,12 +106,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable private openMovementDropdown: boolean = false;
@observable private openEffectDropdown: boolean = false;
@observable private presentTools: boolean = false;
- // @computed get childDocs() { return DocListCast(this.rootDoc[this.fieldKey]); }
- // TODO: [AL] add childDocs() and treeMap fields
- // @computed get childDocs() { return DocListCast(this.rootDoc.presentationaLinearizedDocuments); }
- //_treeViewMap:Map<number[], Doc>
- @computed get childDocs() { return this.layoutDoc._viewType === CollectionViewType.Tree ? DocListCast(this.rootDoc.presentationLinearizedDocuments) : DocListCast(this.rootDoc[this.fieldKey]); }
- @observable _treeViewMap: Map<number, Doc> = new Map<number, Doc>();
+ @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[] = [];
@@ -132,12 +139,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _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 });
}
this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
@@ -157,8 +158,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();
@@ -169,7 +172,7 @@ 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;
@@ -328,22 +331,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.presStartTime;
+ 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);
}
/**
@@ -401,15 +422,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// 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);
}
@@ -621,14 +644,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
[CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
this.rootDoc._viewType = viewType;
- if ([CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType)) this.layoutDoc._gridGap = 0;
+ if (this.isTreeOrStack) {
+ this.layoutDoc._gridGap = 0;
+ }
});
/**
@@ -702,7 +728,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
return true;
}
- childLayoutTemplate = () => ![CollectionViewType.Stacking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any) ? undefined : this.presElement;
+ childLayoutTemplate = () => !this.isTreeOrStack ? undefined : this.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;
@@ -801,13 +827,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) => {
@@ -976,7 +1002,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)
@@ -1363,7 +1389,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const scroll = targetDoc._scrollTop;
activeItem.presPinView = true;
activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
+ } 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) {
@@ -1386,7 +1412,7 @@ 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) {
+ } 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) {
@@ -1779,16 +1805,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;
@@ -2454,27 +2480,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
for (let i = 0; i < index.length; i++) {
indexNum += (index[i] * (10 ** (-i)));
}
- this._treeViewMap.set(indexNum, treeViewDoc);
- this.props.Document.presentationLinearizedDocuments = new List<Doc>(this.sort(this._treeViewMap)); // this is a flat array of Docs
+ 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[] => {
- var indexNum = 0;
- for (let i = 0; i < index.length; i++) {
- indexNum += (index[i] * (10 ** (-i)));
- }
- console.log(String(index), treeViewDoc)
- this._treeViewMap.delete(indexNum);
- const overlay = (Doc.UserDoc().myOverlayDocs as Doc);
- if (!DocListCast(overlay[Doc.LayoutFieldKey(overlay)]).includes(this.rootDoc)) {
- this.props.Document.presentationLinearizedDocuments = new List<Doc>(this.sort(this._treeViewMap));
+ 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<number, Doc>) => [...treeViewMap.entries()].sort().map(kv => kv[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
@@ -2486,7 +2512,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const presEnd: boolean = !this.layoutDoc.presLoop && (this.itemIndex === this.childDocs.length - 1);
const presStart: boolean = !this.layoutDoc.presLoop && (this.itemIndex === 0);
return CurrentUserUtils.OverlayDocs.includes(this.rootDoc) ?
- <div className="miniPres">
+ <div className="miniPres" onClick={e => e.stopPropagation()}>
<div className="presPanelOverlay" style={{ display: "inline-flex", height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
<Tooltip title={<><div className="dash-tooltip">{"Loop"}</div></>}><div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }}
onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.layoutDoc.presLoop = !this.layoutDoc.presLoop, false, false)}><FontAwesomeIcon icon={"redo-alt"} /></div></Tooltip>
@@ -2530,8 +2556,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
removeDocument={returnFalse}
dontRegisterView={true}
focus={this.selectElement}
+ scriptContext={this}
ScreenToLocalTransform={this.getTransform}
- // TODO: [AL] implement AddToMap, RemFromMap functions (outside this)
AddToMap={this.AddToMap}
RemFromMap={this.RemFromMap}
hierarchyIndex={[]}
@@ -2550,12 +2576,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>;
}
}
-// this func communicates with PresBoxElement to send information of the doc
-ScriptingGlobals.add(function lookupPresBoxField(presBoxDoc: Doc, field: string, presEleDoc: Doc) {
- if (field === 'indexInPres') return DocListCast(presBoxDoc._viewType === CollectionViewType.Tree ? presBoxDoc.presentationLinearizedDocuments : presBoxDoc[StrCast(presBoxDoc.presentationFieldKey)]).indexOf(presEleDoc);
- if (field === 'presCollapsedHeight') return [CollectionViewType.Tree || CollectionViewType.Stacking].includes(presBoxDoc._viewType as any) ? 35 : 31;
- if (field === 'presStatus') return presBoxDoc.presStatus;
- if (field === '_itemIndex') return presBoxDoc._itemIndex;
- if (field === 'presBox') return presBoxDoc;
- 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 789a6d9f3..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: 100%;
- display: grid;
- 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;
- }
+ 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-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-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;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ }
- .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-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-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-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-embeddedMask {
- width: 100%;
- height: 100%;
- position: absolute;
- border-radius: 3px;
- top: 0;
- left: 0;
- z-index: 1;
- overflow: hidden;
- }
+ .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;
- position: relative;
- border-radius: 100px;
- z-index: 300;
- width: 18px;
- height: 18px;
+ .presItem-slideButtons {
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;
- }
+ grid-column: 7;
+ grid-row: 1/3;
+ width: max-content;
+ justify-self: right;
+ justify-content: flex-end;
- .slideButton:hover {
- background-color: rgba(0, 0, 0, 1);
- transform: scale(1.2);
- }
- }
+ .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,7 +194,7 @@ $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-slide.group {
@@ -201,7 +203,7 @@ $slide-active: #5B9FDD;
.presItem-slide.activeGroup {
border-radius: 5px;
- box-shadow: 0 0 0px 1.5px $dark-blue;
+ box-shadow: 0 0 0px 2.5px $dark-blue;
}
.presItem-groupSlideContainer {
@@ -237,38 +239,38 @@ $slide-active: #5B9FDD;
}
.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;
+ 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%;
+ 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;
}
.expandButton {
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index bbabcb2de..50df00612 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, DocListCast, Opt } from "../../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
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,7 +24,7 @@ import { PresBox } from "./PresBox";
import "./PresElementBox.scss";
import { PresMovement } from "./PresEnums";
import React = require("react");
-import { CollectionViewType } from "../../collections/CollectionView";
+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.
@@ -34,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() {
@@ -77,8 +76,8 @@ 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}
docViewPath={returnEmptyDoclist}
rootSelected={returnTrue}
@@ -87,7 +86,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
isContentActive={this.props.isContentActive}
addDocTab={returnFalse}
pinToPres={returnFalse}
- fitContentsToDoc={returnTrue}
+ fitContentsToBox={returnTrue}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
ScreenToLocalTransform={Transform.Identity}
@@ -167,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);
}
@@ -197,7 +196,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
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) {
@@ -261,11 +260,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
@@ -285,196 +285,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((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ isInOverlay = true
+ return
+ }
+ })
+ return isInOverlay
+ }
- @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;
- const isGroup: boolean = BoolCast(targetDoc._isGroup);
- 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 => {
- if (isGroup) {
- this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup;
- } else {
- 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>}
- {isGroup ?
- <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "activeGroup" : "group"}`}
+ removeAllRecordingInOverlay = () => {
+ DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+ if (doc.slides === this.rootDoc) {
+ Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
+ }
+ })
+ }
+
+ @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((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
+
+ }
+ }
+
+ @undoBatch
+ @action
+ startRecording = (activeItem: Doc) => {
+ // Remove every recording that already exists in overlay view
+ DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+ if (doc.slides !== null) {
+ Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
+ }
+ })
+
+ if (activeItem.recording) {
+ // if we already have an existing recording
+ Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), 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((Doc.UserDoc().myOverlayDocs as Doc), 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: 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>
- <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 className="group"></div>
- </div>
- <div className="presItem-groupSlideContainer" style={{ top: 28, height: 'calc(100% - 28px)' }}>
- {this.rootDoc.presExpandGroup ? this.renderGroupSlides : (null)}
- </div>
- <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
- <div className="expandButton"
- onClick={e => {
- if (isGroup) {
- this.rootDoc.presExpandGroup = !this.rootDoc.presExpandGroup;
- }
- }}
- >
- <FontAwesomeIcon icon={"caret-down"} style={{ transform: this.rootDoc.presExpandGroup ? "rotate(180deg)" : "rotate(0deg)" }} />
- </div>
- </div>
- : (null)}
- {miniView || isGroup ? (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 - (this.presBox._viewType === CollectionViewType.Stacking ? 195 : 220)) : toolbarWidth - (this.presBox._viewType === CollectionViewType.Stacking ? 105 : 145), 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 >);
- }
+ 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>
- render() {
- return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
- }
+ {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
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index f706be6c6..0ded1bb3c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -274,6 +274,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
+ @observable private _scrollTimer: any;
+
onScroll = (e: React.UIEvent<HTMLElement>) => {
if (this._mainCont.current && !this._forcedScroll) {
this._ignoreScroll = true; // the pdf scrolled, so we need to tell the Doc to scroll but we don't want the doc to then try to set the PDF scroll pos (which would interfere with the smooth scroll animation)
@@ -281,6 +283,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
this.props.layoutDoc._scrollTop = this._mainCont.current.scrollTop;
}
this._ignoreScroll = false;
+ if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio
+ this._scrollTimer = setTimeout(() => {
+ DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!()!, false);
+ this._scrollTimer = undefined;
+ }, 200);
}
}
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index 923f1892e..c5b340514 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -1,29 +1,44 @@
@import "../global/globalCssVariables";
+
.topbar-container {
- display: flex;
- flex-direction: column;
- width: 100%;
- position: relative;
- font-size: 10px;
- line-height: 1;
- overflow-y: auto;
- overflow-x: visible;
- background: $dark-gray;
- overflow: visible;
- z-index: 1000;
-
- .topbar-bar {
- height: $topbar-height;
- display: grid;
- grid-auto-columns: 33.3% 33.3% 33.3%;
- align-items: center;
- background-color: $dark-gray;
-
- .topBar-icon {
+ display: flex;
+ flex-direction: column;
+ font-size: 10px;
+ line-height: 1;
+ overflow-y: auto;
+ overflow-x: visible;
+ background: $dark-gray;
+ overflow: visible;
+ z-index: 1000;
+ height: $topbar-height;
+ background-color: $dark-gray;
+
+ .topbar-inner-container {
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ display: grid;
+ grid-auto-columns: 33.3% 33.3% 33.3%;
+ align-items: center;
+
+ // &:first-child {
+ // height: 20px;
+ // }
+ }
+
+ .topbar-button-text {
+ color: $white;
+ padding: 10px;
+ size: 15;
+
+ &:hover {
+ font-weight: 500;
+ }
+ }
+
+ .topbar-button-icon {
cursor: pointer;
- font-size: 12.5px;
- font-family: 'Roboto';
width: fit-content;
display: flex;
justify-content: center;
@@ -31,21 +46,22 @@
align-items: center;
justify-self: center;
align-self: center;
- border-radius: $standard-border-radius;
padding: 5px;
transition: linear 0.2s;
- color: $black;
- background-color: $light-gray;
+ color: $white;
&:hover {
- background-color: darken($color: $light-gray, $amount: 20);
+ background-color: darken($color: $light-gray, $amount: 20);
}
- }
+ }
+ .topbar-title {
+ color: $white;
+ font-size: 17;
+ font-weight: 500;
+ }
-
-
- .topbar-center {
+ .topbar-center {
grid-column: 2;
display: inline-flex;
justify-content: center;
@@ -53,42 +69,23 @@
gap: 5px;
.topbar-dashboards {
- display: flex;
- flex-direction: row;
- gap: 5px;
- }
-
- .topbar-lozenge-dashboard {
- display: flex;
-
-
-
- .topbar-dashSelect {
- border: none;
- background-color: $dark-gray;
- color: $white;
- font-family: 'Roboto';
- font-size: 17;
- font-weight: 500;
-
- &:hover {
- cursor: pointer;
- }
- }
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
}
- }
+ }
- .topbar-right {
+ .topbar-right {
grid-column: 3;
position: relative;
display: flex;
justify-content: flex-end;
gap: 5px;
margin-right: 5px;
- }
+ }
- .topbar-left {
+ .topbar-left {
grid-column: 1;
color: black;
font-family: 'Roboto';
@@ -98,123 +95,122 @@
gap: 5px;
.topBar-icon:hover {
- background-color: $close-red;
+ background-color: $close-red;
}
.topbar-lozenge-user,
.topbar-lozenge {
- height: 23;
- font-size: 12;
- color: white;
- font-family: 'Roboto';
- font-weight: 400;
- padding: 4px;
- align-self: center;
- margin-left: 7px;
- display: flex;
- align-items: center;
-
- .topbar-dashSelect {
- border: none;
- background-color: transparent;
- color: black;
- font-family: 'Roboto';
- font-size: 17;
- font-weight: 500;
-
- &:hover {
- cursor: pointer;
- }
- }
+ height: 23;
+ font-size: 12;
+ color: white;
+ font-family: 'Roboto';
+ font-weight: 400;
+ padding: 4px;
+ align-self: center;
+ margin-left: 7px;
+ display: flex;
+ align-items: center;
+
+ .topbar-dashSelect {
+ border: none;
+ background-color: transparent;
+ color: black;
+ font-family: 'Roboto';
+ font-size: 17;
+ font-weight: 500;
+
+ &:hover {
+ cursor: pointer;
+ }
+ }
}
.topbar-logoff {
- border-radius: 3px;
- background: olivedrab;
- color: white;
- display: none;
- margin-left: 5px;
- padding: 1px 2px 1px 2px;
- cursor: pointer;
+ border-radius: 3px;
+ background: olivedrab;
+ color: white;
+ display: none;
+ margin-left: 5px;
+ padding: 1px 2px 1px 2px;
+ cursor: pointer;
}
.topbar-logoff {
- background: red;
+ background: red;
}
.topbar-lozenge-user:hover {
- .topbar-logoff {
- display: inline-block;
- }
+ .topbar-logoff {
+ display: inline-block;
+ }
}
- }
+ }
- .topbar-barChild {
+ .topbar-barChild {
&.topbar-collection {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px
}
&.topbar-input {
- margin: 5px;
- border-radius: 20px;
- border: $dark-gray;
- display: block;
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- /* align-self: stretch; */
- outline: none;
-
- &:focus {
- width: 500px;
- outline: none;
- }
+ margin: 5px;
+ border-radius: 20px;
+ border: $dark-gray;
+ display: block;
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ /* align-self: stretch; */
+ outline: none;
+
+ &:focus {
+ width: 500px;
+ outline: none;
+ }
}
&.topbar-filter {
- align-self: stretch;
+ align-self: stretch;
- button {
- transform: none;
-
- &:hover {
+ button {
transform: none;
- }
- }
+
+ &:hover {
+ transform: none;
+ }
+ }
}
&.topbar-submit {
- margin-left: 2px;
- margin-right: 2px
+ margin-left: 2px;
+ margin-right: 2px
}
&.topbar-close {
- color: $white;
- max-height: $topbar-height;
+ color: $white;
+ max-height: $topbar-height;
}
- }
- }
+ }
}
.topbar-results {
- display: flex;
- flex-direction: column;
- top: 300px;
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: visible;
-
- .no-result {
- width: 500px;
- background: $light-gray;
- padding: 10px;
- height: 50px;
- text-transform: uppercase;
- text-align: left;
- font-weight: bold;
- }
+ display: flex;
+ flex-direction: column;
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: visible;
+
+ .no-result {
+ width: 500px;
+ background: $light-gray;
+ padding: 10px;
+ height: 50px;
+ text-transform: uppercase;
+ text-align: left;
+ font-weight: bold;
+ }
} \ No newline at end of file
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index be248ab92..0db3950a2 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -3,13 +3,18 @@ import { Tooltip } from "@material-ui/core";
import { action } from "mobx";
import { observer } from "mobx-react";
import * as React from 'react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { AclAdmin, Doc, DocListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
-import { StrCast } from '../../../fields/Types';
+import { Cast, StrCast } from '../../../fields/Types';
+import { GetEffectiveAcl } from "../../../fields/util";
import { Utils } from '../../../Utils';
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
+import { DocumentManager } from "../../util/DocumentManager";
+import { SelectionManager } from "../../util/SelectionManager";
import { SettingsManager } from "../../util/SettingsManager";
+import { SharingManager } from "../../util/SharingManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { ContextMenu } from "../ContextMenu";
import { Borders, Colors } from "../global/globalEnums";
import { MainView } from "../MainView";
import "./TopBar.scss";
@@ -20,64 +25,63 @@ import "./TopBar.scss";
*/
@observer
export class TopBar extends React.Component {
- render() {
- const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data);
- return (
- //TODO:glr Add support for light / dark mode
- <div style={{ pointerEvents: "all" }} className="topbar-container">
- <div className="topbar-bar" style={{ background: Colors.DARK_GRAY, borderBottom: Borders.STANDARD }}>
- <div className="topbar-left">
- <div className="topbar-lozenge-user">
- {`${Doc.CurrentUserEmail}`}
+ navigateToHome = () => {
+ CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => {
+ Doc.UserDoc().activePage = "home";
+ CurrentUserUtils.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
+ });
+ }
+ render() {
+ const activeDashboard = Cast(Doc.UserDoc().activeDashboard, Doc, null)
+ return (
+ //TODO:glr Add support for light / dark mode
+ <div style={{ pointerEvents: "all", background: Colors.DARK_GRAY, borderBottom: Borders.STANDARD }} className="topbar-container">
+ <div className="topbar-inner-container">
+ <div className="topbar-left">
+ {activeDashboard ? <div className="topbar-button-text" onClick={e => {
+ ContextMenu.Instance.addItem({ description: "Logout", event: () => window.location.assign(Utils.prepend("/logout")), icon: "edit" });
+ ContextMenu.Instance.displayMenu(e.clientX +5, e.clientY + 10);
+ }}>{Doc.CurrentUserEmail}</div> : (null)}
+ </div>
+ <div className="topbar-center" >
+ <div className="topbar-title" onClick={() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(activeDashboard)!, false)}>
+ {activeDashboard ? StrCast(activeDashboard.title) : "Dash"}
+ </div>
+ <div className="topbar-button-icon" onClick={e => {
+ const dashView = DocumentManager.Instance.getDocumentView(activeDashboard);
+ ContextMenu.Instance.addItem({ description: "Open Dashboard View", event: this.navigateToHome, icon: "edit" });
+ ContextMenu.Instance.addItem({ description: "Snapshot Dashboard", event: async () => {
+ const batch = UndoManager.StartBatch("snapshot");
+ await CurrentUserUtils.snapshotDashboard(Doc.UserDoc());
+ batch.end();
+ }, icon: "edit" });
+ dashView?.showContextMenu(e.clientX+20, e.clientY+30);
+ }}>
+ <FontAwesomeIcon color="white" size="lg" icon="bars" />
+ </div>
+ <Tooltip title={<div className="dash-tooltip">Browsing mode for directly navigating to documents</div>} placement="bottom">
+ <div className="topbar-icon" style={{ background: MainView.Instance._exploreMode ? Colors.LIGHT_BLUE : undefined }} onClick={action(() => MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}>
+ <FontAwesomeIcon color={MainView.Instance._exploreMode ? "red":"white"} icon="eye" size="lg" />
+ </div>
+ </Tooltip>
+ </div>
+ <div className="topbar-right" >
+ <div className="topbar-button-text" onClick={() => {SharingManager.Instance.open(undefined, activeDashboard)}}>
+ {/* TODO: if this is my dashboard, display share
+ if this is a shared dashboard, display "view original or view annotated" */}
+ { Doc.GetProto(CurrentUserUtils.ActiveDashboard)?.author === Doc.CurrentUserEmail ? "Share": "view original" }
+ </div>
+ <div className="topbar-button-icon" onClick={() => window.open(
+ "https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
+ <FontAwesomeIcon icon="question-circle" />
+ </div>
+ <div className="topbar-button-icon" onClick={() => SettingsManager.Instance.open()}>
+ <FontAwesomeIcon icon="cog" />
+ </div>
+ </div>
</div>
- <div className="topbar-icon" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
- {"Log out"}
- </div>
- </div>
- <div className="topbar-center" >
- <div className="topbar-lozenge-dashboard">
- <select className="topbar-dashSelect" onChange={undoBatch(e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)]))}
- value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}
- style={{ color: Colors.WHITE }}>
- {myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)}
- </select>
- </div>
- <div className="topbar-dashboards">
- <Tooltip title={<div className="dash-tooltip">Create a new dashboard </div>} placement="bottom"><div className="topbar-icon" onClick={async () => {
- const batch = UndoManager.StartBatch("new dash");
- await CurrentUserUtils.createNewDashboard(Doc.UserDoc());
- batch.end();
- }}>
- {"New"}<FontAwesomeIcon icon="plus" />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">Work on a copy of the dashboard layout</div>} placement="bottom">
- <div className="topbar-icon" onClick={async () => {
- const batch = UndoManager.StartBatch("snapshot");
- await CurrentUserUtils.snapshotDashboard(Doc.UserDoc());
- batch.end();
- }}>
- {"Snapshot"}<FontAwesomeIcon icon="camera" />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">Browsing mode for directly navigating to documents</div>} placement="bottom">
- <div className="topbar-icon" style={{ background: MainView.Instance._exploreMode ? Colors.LIGHT_BLUE : undefined }} onClick={action(() => MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}>
- {"Explore"}<FontAwesomeIcon icon="map" />
- </div>
- </Tooltip>
- </div>
- </div>
- <div className="topbar-right" >
- <div className="topbar-icon" onClick={() => window.open(
- "https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
- {"Help"}<FontAwesomeIcon icon="question-circle" />
- </div>
- <div className="topbar-icon" onClick={() => SettingsManager.Instance.open()}>
- {"Settings"}<FontAwesomeIcon icon="cog" />
- </div>
- </div>
- </div>
- </div >
- );
- }
+ </div>
+
+ );
+ }
} \ No newline at end of file
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6abc27b23..7b72787d1 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1436,6 +1436,7 @@ ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) { return Doc.cop
ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) { return Doc.delegateDragFactory(dragFactory); });
ScriptingGlobals.add(function copyField(field: any) { return Field.Copy(field); });
ScriptingGlobals.add(function docList(field: any) { return DocListCast(field); });
+ScriptingGlobals.add(function addDocToList(doc: Doc, field: string, added:Doc) { return Doc.AddDocToList(doc,field, added); });
ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { return Doc.SetInPlace(doc, field, value, false); });
ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
ScriptingGlobals.add(function undo() { SelectionManager.DeselectAll(); return UndoManager.Undo(); });
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 7e2aa5681..bf40a0d7b 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -76,6 +76,10 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal
return defaultVal === null ? undefined : defaultVal;
}
+export function DocCast(field: FieldResult, defaultVal?: Doc) {
+ return Cast(field, Doc, null) ?? defaultVal;
+}
+
export function NumCast(field: FieldResult, defaultVal: number | null = 0) {
return Cast(field, "number", defaultVal);
}
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 1d4bbaed0..3e7542e46 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -53,6 +53,7 @@ export abstract class URLField extends ObjectField {
export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg";
@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
+@scriptingGlobal @Deserializable("recording") export class RecordingField extends URLField { }
@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index e532becb5..be39e0709 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -36,8 +36,8 @@ export const documentSchema = createSchema({
_nativeHeight: "number", // "
_width: "number", // width of document in its container's coordinate system
_height: "number", // "
- _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitToBox is set
+ _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
+ _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
_xMargin: "number", // margin added on left/right of most documents to add separation from their container
_yMargin: "number", // margin added on top/bottom of most documents to add separation from their container
_overflow: "string", // sets overflow behvavior for CollectionFreeForm views
@@ -59,7 +59,7 @@ export const documentSchema = createSchema({
borderRounding: "string", // border radius rounding of document
boxShadow: "string", // the amount of shadow around the perimeter of a document
color: "string", // foreground color of document
- fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
+ fitContentsToBox: "boolean",// whether freeform view contents should be zoomed/panned to fill the area of the document view box
fontSize: "string",
hidden: "boolean", // whether a document should not be displayed
isInkMask: "boolean", // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 418464f0e..64baa351c 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -19,7 +19,7 @@ import React = require('react');
@observer
export class AudioUpload extends React.Component {
- @observable public _audioCol: Doc = FieldValue(Cast(Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: "mobile audio", _width: 500, _height: 100 }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
+ @observable public _audioCol: Doc = FieldValue(Cast(Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: "mobile audio", _width: 500, _height: 100 }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
/**
* Handles the onclick functionality for the 'Restart' button
@@ -36,7 +36,7 @@ export class AudioUpload extends React.Component {
title: "mobile audio",
_width: 500,
_height: 100
- }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
+ }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
}
/**
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 2c667ba3e..e7b7056a1 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -17,239 +17,239 @@ import { SolrManager } from "./SearchManager";
const fs = require('fs');
export enum Directory {
- parsed_files = "parsed_files",
- images = "images",
- videos = "videos",
- pdfs = "pdfs",
- text = "text",
- pdf_thumbnails = "pdf_thumbnails",
- audio = "audio",
+ parsed_files = "parsed_files",
+ images = "images",
+ videos = "videos",
+ pdfs = "pdfs",
+ text = "text",
+ pdf_thumbnails = "pdf_thumbnails",
+ audio = "audio",
}
export function serverPathToFile(directory: Directory, filename: string) {
- return normalize(`${filesDirectory}/${directory}/${filename}`);
+ return normalize(`${filesDirectory}/${directory}/${filename}`);
}
export function pathToDirectory(directory: Directory) {
- return normalize(`${filesDirectory}/${directory}`);
+ return normalize(`${filesDirectory}/${directory}`);
}
export function clientPathToFile(directory: Directory, filename: string) {
- return `/files/${directory}/${filename}`;
+ return `/files/${directory}/${filename}`;
}
export default class UploadManager extends ApiManager {
- protected initialize(register: Registration): void {
+ protected initialize(register: Registration): void {
- register({
- method: Method.POST,
- subscription: "/uploadFormData",
- secureHandler: async ({ req, res }) => {
- const form = new formidable.IncomingForm();
- form.keepExtensions = true;
- form.uploadDir = pathToDirectory(Directory.parsed_files);
- return new Promise<void>(resolve => {
- form.parse(req, async (_err, _fields, files) => {
- const results: Upload.FileResponse[] = [];
- for (const key in files) {
- const f = files[key];
- if (!Array.isArray(f)) {
- const result = await DashUploadUtils.upload(f);
- result && !(result.result instanceof Error) && results.push(result);
- }
- }
- _success(res, results);
- resolve();
- });
- });
- }
- });
+ register({
+ method: Method.POST,
+ subscription: "/uploadFormData",
+ secureHandler: async ({ req, res }) => {
+ const form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ form.uploadDir = pathToDirectory(Directory.parsed_files);
+ return new Promise<void>(resolve => {
+ form.parse(req, async (_err, _fields, files) => {
+ const results: Upload.FileResponse[] = [];
+ for (const key in files) {
+ const f = files[key];
+ if (!Array.isArray(f)) {
+ const result = await DashUploadUtils.upload(f);
+ result && !(result.result instanceof Error) && results.push(result);
+ }
+ }
+ _success(res, results);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadYoutubeVideo",
- secureHandler: async ({ req, res }) => {
- //req.readableBuffer.head.data
- return new Promise<void>(async resolve => {
- req.addListener("data", async (args) => {
- console.log(args);
- const payload = String.fromCharCode.apply(String, args);
- const videoId = JSON.parse(payload).videoId;
- const results: Upload.FileResponse[] = [];
- const result = await DashUploadUtils.uploadYoutube(videoId);
- result && !(result.result instanceof Error) && results.push(result);
- _success(res, results);
- resolve();
- });
- });
- }
- });
+ register({
+ method: Method.POST,
+ subscription: "/uploadYoutubeVideo",
+ secureHandler: async ({ req, res }) => {
+ //req.readableBuffer.head.data
+ return new Promise<void>(async resolve => {
+ req.addListener("data", async (args) => {
+ console.log(args);
+ const payload = String.fromCharCode.apply(String, args);
+ const videoId = JSON.parse(payload).videoId;
+ const results: Upload.FileResponse[] = [];
+ const result = await DashUploadUtils.uploadYoutube(videoId);
+ result && !(result.result instanceof Error) && results.push(result);
+ _success(res, results);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: new RouteSubscriber("youtubeScreenshot"),
- secureHandler: async ({ req, res }) => {
- const { id, timecode } = req.body;
- const convert = (raw: string) => {
- const number = Math.floor(Number(raw));
- const seconds = number % 60;
- const minutes = (number - seconds) / 60;
- return `${minutes}m${seconds}s`;
- };
- const suffix = timecode ? `&t=${convert(timecode)}` : ``;
- const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
- const buffer = await captureYoutubeScreenshot(targetUrl);
- if (!buffer) {
- return res.send();
- }
- const resolvedName = `youtube_capture_${id}_${suffix}.png`;
- const resolvedPath = serverPathToFile(Directory.images, resolvedName);
- return new Promise<void>(resolve => {
- writeFile(resolvedPath, buffer, async error => {
- if (error) {
- return res.send();
+ register({
+ method: Method.POST,
+ subscription: new RouteSubscriber("youtubeScreenshot"),
+ secureHandler: async ({ req, res }) => {
+ const { id, timecode } = req.body;
+ const convert = (raw: string) => {
+ const number = Math.floor(Number(raw));
+ const seconds = number % 60;
+ const minutes = (number - seconds) / 60;
+ return `${minutes}m${seconds}s`;
+ };
+ const suffix = timecode ? `&t=${convert(timecode)}` : ``;
+ const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
+ const buffer = await captureYoutubeScreenshot(targetUrl);
+ if (!buffer) {
+ return res.send();
}
- await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
- res.send({
- accessPaths: {
- agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
- }
- } as Upload.FileInformation);
- resolve();
- });
- });
- }
- });
+ const resolvedName = `youtube_capture_${id}_${suffix}.png`;
+ const resolvedPath = serverPathToFile(Directory.images, resolvedName);
+ return new Promise<void>(resolve => {
+ writeFile(resolvedPath, buffer, async error => {
+ if (error) {
+ return res.send();
+ }
+ await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+ res.send({
+ accessPaths: {
+ agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
+ }
+ } as Upload.FileInformation);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadRemoteImage",
- secureHandler: async ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/uploadRemoteImage",
+ secureHandler: async ({ req, res }) => {
- const { sources } = req.body;
- if (Array.isArray(sources)) {
- const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
- return res.send(results);
- }
- res.send();
- }
- });
+ const { sources } = req.body;
+ if (Array.isArray(sources)) {
+ const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
+ return res.send(results);
+ }
+ res.send();
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadDoc",
- secureHandler: ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/uploadDoc",
+ secureHandler: ({ req, res }) => {
- const form = new formidable.IncomingForm();
- form.keepExtensions = true;
- // let path = req.body.path;
- const ids: { [id: string]: string } = {};
- let remap = true;
- const getId = (id: string): string => {
- if (!remap) return id;
- if (id.endsWith("Proto")) return id;
- if (id in ids) {
- return ids[id];
- } else {
- return ids[id] = v4();
- }
- };
- const mapFn = (doc: any) => {
- if (doc.id) {
- doc.id = getId(doc.id);
- }
- for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) { continue; }
- const field = doc.fields[key];
- if (field === undefined || field === null) { continue; }
+ const form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ // let path = req.body.path;
+ const ids: { [id: string]: string } = {};
+ let remap = true;
+ const getId = (id: string): string => {
+ if (!remap) return id;
+ if (id.endsWith("Proto")) return id;
+ if (id in ids) {
+ return ids[id];
+ } else {
+ return ids[id] = v4();
+ }
+ };
+ const mapFn = (doc: any) => {
+ if (doc.id) {
+ doc.id = getId(doc.id);
+ }
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) { continue; }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) { continue; }
- if (field.__type === "Doc") {
- mapFn(field);
- } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
- field.fieldId = getId(field.fieldId);
- } else if (field.__type === "script" || field.__type === "computed") {
- if (field.captures) {
- field.captures.fieldId = getId(field.captures.fieldId);
- }
- } else if (field.__type === "list") {
- mapFn(field);
- } else if (typeof field === "string") {
- const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
- doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- } else if (field.__type === "RichTextField") {
- const re = /("href"\s*:\s*")(.*?)"/g;
- field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- }
- }
- };
- return new Promise<void>(resolve => {
- form.parse(req, async (_err, fields, files) => {
- remap = fields.remap !== "false";
- let id: string = "";
- try {
- for (const name in files) {
- const f = files[name];
- const path_2 = Array.isArray(f) ? "" : f.path;
- const zip = new AdmZip(path_2);
- zip.getEntries().forEach((entry: any) => {
- if (!entry.entryName.startsWith("files/")) return;
- let directory = dirname(entry.entryName) + "/";
- const extension = extname(entry.entryName);
- const base = basename(entry.entryName).split(".")[0];
+ if (field.__type === "Doc") {
+ mapFn(field);
+ } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ field.fieldId = getId(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ field.captures.fieldId = getId(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ mapFn(field);
+ } else if (typeof field === "string") {
+ const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+ doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ } else if (field.__type === "RichTextField") {
+ const re = /("href"\s*:\s*")(.*?)"/g;
+ field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ }
+ }
+ };
+ return new Promise<void>(resolve => {
+ form.parse(req, async (_err, fields, files) => {
+ remap = fields.remap !== "false";
+ let id: string = "";
try {
- zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
- directory = "/" + directory;
+ for (const name in files) {
+ const f = files[name];
+ const path_2 = Array.isArray(f) ? "" : f.path;
+ const zip = new AdmZip(path_2);
+ zip.getEntries().forEach((entry: any) => {
+ if (!entry.entryName.startsWith("files/")) return;
+ let directory = dirname(entry.entryName) + "/";
+ const extension = extname(entry.entryName);
+ const base = basename(entry.entryName).split(".")[0];
+ try {
+ zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+ directory = "/" + directory;
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
- } catch (e) {
- console.log(e);
- }
- });
- const json = zip.getEntry("doc.json");
- try {
- const data = JSON.parse(json.getData().toString("utf8"));
- const datadocs = data.docs;
- id = getId(data.id);
- const docs = Object.keys(datadocs).map(key => datadocs[key]);
- docs.forEach(mapFn);
- await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
- Database.Instance.replace(doc.id, doc, (err, r) => {
- err && console.log(err);
- res();
- }, true);
- })));
- } catch (e) { console.log(e); }
- unlink(path_2, () => { });
- }
- SolrManager.update();
- res.send(JSON.stringify(id || "error"));
- } catch (e) { console.log(e); }
- resolve();
- });
- });
- }
- });
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
+ } catch (e) {
+ console.log(e);
+ }
+ });
+ const json = zip.getEntry("doc.json");
+ try {
+ const data = JSON.parse(json.getData().toString("utf8"));
+ const datadocs = data.docs;
+ id = getId(data.id);
+ const docs = Object.keys(datadocs).map(key => datadocs[key]);
+ docs.forEach(mapFn);
+ await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
+ Database.Instance.replace(doc.id, doc, (err, r) => {
+ err && console.log(err);
+ res();
+ }, true);
+ })));
+ } catch (e) { console.log(e); }
+ unlink(path_2, () => { });
+ }
+ SolrManager.update();
+ res.send(JSON.stringify(id || "error"));
+ } catch (e) { console.log(e); }
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/inspectImage",
- secureHandler: async ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/inspectImage",
+ secureHandler: async ({ req, res }) => {
- const { source } = req.body;
- if (typeof source === "string") {
- return res.send(await DashUploadUtils.InspectImage(source));
- }
- res.send({});
- }
- });
+ const { source } = req.body;
+ if (typeof source === "string") {
+ return res.send(await DashUploadUtils.InspectImage(source));
+ }
+ res.send({});
+ }
+ });
register({
method: Method.POST,
@@ -296,17 +296,17 @@ export default class UploadManager extends ApiManager {
.pipe(createWriteStream(path).on("error", e => console.log("Resizing write: " + e)));
});
- }
- res.send(clientPathToFile(Directory.images, filename + ext));
- });
- }
- });
+ }
+ res.send(clientPathToFile(Directory.images, filename + ext));
+ });
+ }
+ });
- }
+ }
}
function delay(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* On success, returns a buffer containing the bytes of a screenshot
@@ -315,25 +315,25 @@ function delay(ms: number) {
* On failure, returns undefined.
*/
async function captureYoutubeScreenshot(targetUrl: string) {
- // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
- // const page = await browser.newPage();
- // // await page.setViewport({ width: 1920, height: 1080 });
+ // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
+ // const page = await browser.newPage();
+ // // await page.setViewport({ width: 1920, height: 1080 });
- // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
+ // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
- // const videoPlayer = await page.$('.html5-video-player');
- // videoPlayer && await page.focus("video");
- // await delay(7000);
- // const ad = await page.$('.ytp-ad-skip-button-text');
- // await ad?.click();
- // await videoPlayer?.click();
- // await delay(1000);
- // // hide youtube player controls.
- // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
+ // const videoPlayer = await page.$('.html5-video-player');
+ // videoPlayer && await page.focus("video");
+ // await delay(7000);
+ // const ad = await page.$('.ytp-ad-skip-button-text');
+ // await ad?.click();
+ // await videoPlayer?.click();
+ // await delay(1000);
+ // // hide youtube player controls.
+ // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
- // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
- // await browser.close();
+ // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
+ // await browser.close();
- // return buffer;
- return null;
+ // return buffer;
+ return null;
} \ No newline at end of file