aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2022-06-26 09:51:39 -0400
committerbobzel <zzzman@gmail.com>2022-06-26 09:51:39 -0400
commit809a8f766640fe297a4019be52bb8fd4da81e4fe (patch)
tree8196e31d24ddd0d573e518362b27187e3f567f30 /src
parent301f13ebcbc47bd04b56210620aafaaad5f2998e (diff)
parentdccf5909f4a4bec35559b23a2f355ab4c7a94086 (diff)
Merge branch 'master' into mainview-jenny
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin8196 -> 10244 bytes
-rw-r--r--src/client/DocServer.ts2
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts184
-rw-r--r--src/client/util/CurrentUserUtils.ts663
-rw-r--r--src/client/util/DragManager.ts15
-rw-r--r--src/client/util/History.ts2
-rw-r--r--src/client/util/SettingsManager.tsx24
-rw-r--r--src/client/util/SnappingManager.ts4
-rw-r--r--src/client/views/AudioWaveform.scss2
-rw-r--r--src/client/views/ContextMenu.tsx65
-rw-r--r--src/client/views/ContextMenuItem.tsx14
-rw-r--r--src/client/views/DashboardView.tsx9
-rw-r--r--src/client/views/DocumentDecorations.tsx27
-rw-r--r--src/client/views/MainView.tsx34
-rw-r--r--src/client/views/OverlayView.scss1
-rw-r--r--src/client/views/PropertiesView.tsx1
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx10
-rw-r--r--src/client/views/collections/CollectionMenu.tsx4
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss41
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx49
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx4
-rw-r--r--src/client/views/collections/CollectionSubView.tsx6
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx5
-rw-r--r--src/client/views/collections/CollectionView.tsx3
-rw-r--r--src/client/views/collections/TabDocView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx37
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx4
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx1
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx14
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx6
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx1
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx1
-rw-r--r--src/client/views/nodes/AudioBox.tsx22
-rw-r--r--src/client/views/nodes/DataViz.scss0
-rw-r--r--src/client/views/nodes/DataViz.tsx21
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx13
-rw-r--r--src/client/views/nodes/DocumentView.tsx23
-rw-r--r--src/client/views/nodes/VideoBox.scss82
-rw-r--r--src/client/views/nodes/VideoBox.tsx1804
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx34
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx3
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx8
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx11
-rw-r--r--src/client/views/topbar/TopBar.tsx2
-rw-r--r--src/fields/Doc.ts17
-rw-r--r--src/mobile/MobileInterface.tsx2
47 files changed, 1688 insertions, 1590 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 4bf9cdac7..4751acf44 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index dbc4783d8..c9a30b8e3 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -180,7 +180,7 @@ export namespace DocServer {
_isReadOnly = true;
_CreateField = field => _cache[field[Id]] = field;
_UpdateField = emptyFunction;
- _RespondToUpdate = emptyFunction;
+ _RespondToUpdate = emptyFunction; // bcz: option: don't clear RespondToUpdate to continue to receive updates as others change the DB
}
}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index ca942a38a..2343c2f34 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -25,6 +25,7 @@ export enum DocumentType {
EQUATION = "equation", // equation editor
FUNCPLOT = "funcplot", // function plotter
MAP = "map",
+ DATAVIZ = "dataviz",
// special purpose wrappers that either take no data or are compositions of lower level types
LINK = "link",
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7f82280c5..84e6258ac 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,7 +1,8 @@
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
import { action, runInAction } from "mobx";
import { basename } from "path";
import { DateField } from "../../fields/DateField";
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Initializing, Opt, updateCachedAcls, WidthSym } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Field, Initializing, Opt, updateCachedAcls } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { HtmlField } from "../../fields/HtmlField";
import { InkField, PointData } from "../../fields/InkField";
@@ -14,7 +15,7 @@ import { Cast, NumCast, StrCast } from "../../fields/Types";
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";
+import { aggregateBounds, OmitKeys, Utils } from "../../Utils";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { DocServer } from "../DocServer";
import { Networking } from "../Network";
@@ -33,13 +34,14 @@ import { ContextMenuProps } from "../views/ContextMenuItem";
import { DFLT_IMAGE_NATIVE_DIM } from "../views/global/globalCssVariables.scss";
import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke";
import { AudioBox } from "../views/nodes/AudioBox";
+import { FontIconBox } from "../views/nodes/button/FontIconBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { ComparisonBox } from "../views/nodes/ComparisonBox";
+import { DataVizBox } from "../views/nodes/DataViz";
import { DocFocusOptions } from "../views/nodes/DocumentView";
import { EquationBox } from "../views/nodes/EquationBox";
import { FieldViewProps } from "../views/nodes/FieldView";
import { FilterBox } from "../views/nodes/FilterBox";
-import { FontIconBox } from "../views/nodes/button/FontIconBox";
import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
import { FunctionPlotBox } from "../views/nodes/FunctionPlotBox";
import { ImageBox } from "../views/nodes/ImageBox";
@@ -47,7 +49,9 @@ import { KeyValueBox } from "../views/nodes/KeyValueBox";
import { LabelBox } from "../views/nodes/LabelBox";
import { LinkBox } from "../views/nodes/LinkBox";
import { LinkDescriptionPopup } from "../views/nodes/LinkDescriptionPopup";
+import { MapBox } from "../views/nodes/MapBox/MapBox";
import { PDFBox } from "../views/nodes/PDFBox";
+import { RecordingBox } from "../views/nodes/RecordingBox/RecordingBox";
import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
import { ScriptingBox } from "../views/nodes/ScriptingBox";
import { SliderBox } from "../views/nodes/SliderBox";
@@ -57,11 +61,7 @@ import { PresElementBox } from "../views/nodes/trails/PresElementBox";
import { VideoBox } from "../views/nodes/VideoBox";
import { WebBox } from "../views/nodes/WebBox";
import { SearchBox } from "../views/search/SearchBox";
-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 {
@@ -69,53 +69,57 @@ class EmptyBox {
return "";
}
}
-abstract class FInfo {
+export abstract class FInfo {
description: string = "";
- type?: string;
+ fieldType?: string;
values?: Field[];
- layoutField?: boolean; // is this field a layout (or datadoc) field
// format?: string; // format to display values (e.g, decimal places, $, etc)
// parse?: ScriptField; // parse a value from a string
- constructor(d: string, l: boolean = false) { this.description = d; this.layoutField = l; }
+ constructor(d: string) { this.description = d; }
}
-class BoolInfo extends FInfo { type?= "boolean"; values?: boolean[] = [true, false]; }
-class NumInfo extends FInfo { type?= "number"; values?: number[] = []; }
-class StrInfo extends FInfo { type?= "string"; values?: string[] = []; }
-class DocInfo extends FInfo { type?= "Doc"; values?: Doc[] = []; }
-class DimInfo extends FInfo { type?= "DimUnit"; values?= [DimUnit.Pixel, DimUnit.Ratio]; }
-class PEInfo extends FInfo { type?= "pointerEvents"; values?= ["all", "none"]; }
-class DAInfo extends FInfo { type?= "dropActionType"; values?= ["alias", "copy", "move", "same", "proto", "none"]; }
+class BoolInfo extends FInfo { fieldType?= "boolean"; values?: boolean[] = [true, false]; }
+class NumInfo extends FInfo { fieldType?= "number"; values?: number[] = []; constructor(d:string, values?:number[]) { super(d); this.values = values; }}
+class StrInfo extends FInfo { fieldType?= "string"; values?: string[] = []; constructor(d:string, values?:string[]) { super(d); this.values = values; }}
+class DocInfo extends FInfo { fieldType?= "Doc"; values?: Doc[] = []; constructor(d:string, values?:Doc[]) { super(d); this.values = values; }}
+class DimInfo extends FInfo { fieldType?= "DimUnit"; values?= [DimUnit.Pixel, DimUnit.Ratio]; }
+class PEInfo extends FInfo { fieldType?= "pointerEvents"; values?= ["all", "none"]; }
+class DAInfo extends FInfo { fieldType?= "dropActionType"; values?= ["alias", "copy", "move", "same", "proto", "none"]; }
type BOOLt = BoolInfo | boolean;
-type NUMt = NumInfo | number;
-type STRt = StrInfo | string;
-type DOCt = DocInfo | Doc;
-type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
-type PEVt = PEInfo | "none" | "all";
+type NUMt = NumInfo | number;
+type STRt = StrInfo | string;
+type DOCt = DocInfo | Doc;
+type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
+type PEVt = PEInfo | "none" | "all";
type DROPt = DAInfo | dropActionType;
export class DocumentOptions {
+ x?: NUMt = new NumInfo("x coordinate of document in a freeform view");
+ y?: NUMt = new NumInfo("y coordinage of document in a freeform view");
+ z?: NUMt = new NumInfo("whether document is in overlay (1) or not (0)", [1,0]);
system?: BOOLt = new BoolInfo("is this a system created/owned doc");
+ type?: STRt = new StrInfo("type of document", Array.from(Object.keys(DocumentType)));
+ title?: string;
_dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
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
+ userColor?: STRt = new StrInfo("color associated with a Dash user (seen in header fields of shared documents)");
+ color?: STRt = new StrInfo("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);
- _autoHeight?: BOOLt = new BoolInfo("whether document automatically resizes vertically to display contents", true);
- _headerHeight?: NUMt = new NumInfo("height of document header used for displaying title", true);
- _headerFontSize?: NUMt = new NumInfo("font size of header of custom notes", true);
- _headerPointerEvents?: PEVt = new PEInfo("types of events the header of a custom text document can consume", true);
- _panX?: NUMt = new NumInfo("horizontal pan location of a freeform view", true);
- _panY?: NUMt = new NumInfo("vertical pan location of a freeform view", true);
- _width?: NUMt = new NumInfo("displayed width of a document", true);
- _height?: NUMt = new NumInfo("displayed height of document", true);
- _nativeWidth?: NUMt = new NumInfo("native width of document contents (e.g., the pixel width of an image)", true);
- _nativeHeight?: NUMt = new NumInfo("native height of document contents (e.g., the pixel height of an image)", true);
- _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);
- _fitContentsToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
+ _backgroundColor?: STRt = new StrInfo("background color for each template layout doc (overrides backgroundColor)");
+ _autoHeight?: BOOLt = new BoolInfo("whether document automatically resizes vertically to display contents");
+ _headerHeight?: NUMt = new NumInfo("height of document header used for displaying title");
+ _headerFontSize?: NUMt = new NumInfo("font size of header of custom notes");
+ _headerPointerEvents?: PEVt = new PEInfo("types of events the header of a custom text document can consume");
+ _panX?: NUMt = new NumInfo("horizontal pan location of a freeform view");
+ _panY?: NUMt = new NumInfo("vertical pan location of a freeform view");
+ _width?: NUMt = new NumInfo("displayed width of a document");
+ _height?: NUMt = new NumInfo("displayed height of document");
+ _nativeWidth?: NUMt = new NumInfo("native width of document contents (e.g., the pixel width of an image)");
+ _nativeHeight?: NUMt = new NumInfo("native height of document contents (e.g., the pixel height of an image)");
+ _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
+ _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
+ _fitWidth?: BOOLt = new BoolInfo("whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)");
+ _fitContentsToBox?: BOOLt = new BoolInfo("whether a freeformview should zoom/scale to create a shrinkwrapped view of its content");
_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
@@ -155,23 +159,20 @@ export class DocumentOptions {
_timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays)
_timecodeToHide?: number; // the time that a document should be hidden
_timelineLabel?: boolean; // whether the document exists on a timeline
- "_carousel-caption-xMargin"?: number;
- "_carousel-caption-yMargin"?: number;
- "icon-nativeWidth"?: number;
- "icon-nativeHeight"?: number;
- "dragFactory-count"?: number; // number of items created from a drag button (used for setting title with incrementing index)
- x?: number;
- y?: number;
- z?: number; // whether document is in overlay (1) or not (0 or undefined)
+ "_carousel-caption-xMargin"?: NUMt = new NumInfo("x margin of caption inside of a carouself collection");
+ "_carousel-caption-yMargin"?: NUMt = new NumInfo("y margin of caption inside of a carouself collection");
+ "icon-nativeWidth"?: NUMt = new NumInfo("native width of icon view");
+ "icon-nativeHeight"?: NUMt = new NumInfo("native height of icon view");
+ "dragFactory-count"?: NUMt = new NumInfo("number of items created from a drag button (used for setting title with incrementing index)");
lat?: number;
lng?: number;
infoWindowOpen?: boolean;
author?: string;
_layoutKey?: string;
+ fieldValues?: List<any>; // possible field values used by fieldInfos
+ fieldType?: string; // type of afield used by fieldInfos
unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)
- type?: string;
- title?: string;
- "acl-Public"?: string; // public permissions
+ "acl-Public"?: string; // public permissions
"_acl-Public"?: string; // public permissions
version?: string; // version identifier for a document
label?: string;
@@ -194,6 +195,7 @@ export class DocumentOptions {
childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width
childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view)
childLayoutString?: string; // template string for collection to use to render its children
+ childDocumentsActive?: boolean; // whether child documents are active when parent is document active
childDontRegisterViews?: boolean;
childHideLinkButton?: boolean; // hide link buttons on all children
childContextMenuFilters?: List<ScriptField>;
@@ -338,33 +340,6 @@ export class DocumentOptions {
}
export namespace Docs {
- const _docOptions = new DocumentOptions();
-
- export async function setupFieldInfos() {
- return await DocServer.GetRefField("FieldInfos8") as Doc ??
- runInAction(() => {
- const infos = new Doc("FieldInfos8", true);
- const keys = Object.keys(new DocumentOptions());
- for (const key of keys) {
- const options = (_docOptions as any)[key] as FInfo;
- const finfo = new Doc();
- finfo.name = key;
- switch (options.type) {
- case "boolean": finfo.options = new List<boolean>(options.values as any as boolean[]); break;
- case "number": finfo.options = new List<number>(options.values as any as number[]); break;
- case "Doc": finfo.options = new List<Doc>(options.values as any as Doc[]); break;
- default: // string, pointerEvents, dimUnit, dropActionType
- finfo.options = new List<string>(options.values as any as string[]); break;
- }
- finfo.layoutField = options.layoutField;
- finfo.description = options.description;
- finfo.type = options.type;
- infos[key] = finfo;
- }
- return infos;
- });
- }
-
export let newAccount: boolean = false;
export namespace Prototypes {
@@ -386,8 +361,8 @@ export namespace Docs {
[DocumentType.RTF, {
layout: { view: FormattedTextBox, dataField: "text" },
options: {
- _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true,
- links: "@links(self)"
+ _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, treeViewGrowsHorizontally: true,
+ forceReflow: true, links: "@links(self)"
}
}],
[DocumentType.SEARCH, {
@@ -424,7 +399,7 @@ export namespace Docs {
}],
[DocumentType.AUDIO, {
layout: { view: AudioBox, dataField: defaultDataKey },
- options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" }
+ options: { _height: 100, backgroundColor: "lightGray", forceReflow: true, nativeDimModifiable: true, links: "@links(self)" }
}],
[DocumentType.REC, {
layout: { view: VideoBox, dataField: defaultDataKey },
@@ -436,7 +411,7 @@ export namespace Docs {
}],
[DocumentType.MAP, {
layout: { view: MapBox, dataField: defaultDataKey },
- options: { _height: 600, _width: 800, links: "@links(self)" }
+ options: { _height: 600, _width: 800, nativeDimModifiable: true, links: "@links(self)" }
}],
[DocumentType.IMPORT, {
layout: { view: DirectoryImportBox, dataField: defaultDataKey },
@@ -474,11 +449,11 @@ export namespace Docs {
}],
[DocumentType.EQUATION, {
layout: { view: EquationBox, dataField: defaultDataKey },
- options: { links: "@links(self)", hideResizeHandles: true, hideDecorationTitle: true }
+ options: { links: "@links(self)", nativeDimModifiable: true, hideResizeHandles: true, hideDecorationTitle: true }
}],
[DocumentType.FUNCPLOT, {
layout: { view: FunctionPlotBox, dataField: defaultDataKey },
- options: { links: "@links(self)" }
+ options: { nativeDimModifiable: true, links: "@links(self)" }
}],
[DocumentType.BUTTON, {
layout: { view: LabelBox, dataField: "onClick" },
@@ -517,7 +492,7 @@ export namespace Docs {
}],
[DocumentType.COMPARISON, {
layout: { view: ComparisonBox, dataField: defaultDataKey },
- options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: "@links(self)" }
+ options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: "gray", targetDropAction: "alias", links: "@links(self)" }
}],
[DocumentType.GROUPDB, {
data: new List<Doc>(),
@@ -528,6 +503,10 @@ export namespace Docs {
layout: { view: EmptyBox, dataField: defaultDataKey },
options: { links: "@links(self)" }
}],
+ [DocumentType.DATAVIZ, {
+ layout: { view: DataVizBox, dataField: defaultDataKey },
+ options: { _fitWidth: true, nativeDimModifiable: true, links: "@links(self)" }
+ }]
]);
const suffix = "Proto";
@@ -550,15 +529,6 @@ export namespace Docs {
const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix);
// fetch the actual prototype documents from the server
const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds);
- if (!Docs.newAccount) {
- Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeHeightUnfrozen = true; // to avoid having to recreate the DB
- Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeHeightUnfrozen = true;
- Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeHeightUnfrozen = true;
- Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeDimModifiable = true; // to avoid having to recreate the DB
- Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeDimModifiable = true;
- Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeDimModifiable = true;
- }
-
// update this object to include any default values: DocumentOptions for all prototypes
prototypeIds.map(id => {
const existing = actualProtos[id] as Doc;
@@ -661,7 +631,7 @@ export namespace Docs {
const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, "^_");
dataProps["acl-Override"] = "None";
- dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+ dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
dataProps.system = viewProps.system;
dataProps.isPrototype = true;
@@ -678,7 +648,7 @@ export namespace Docs {
const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
const viewFirstProps: { [id: string]: any } = {};
- viewFirstProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+ viewFirstProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
viewFirstProps["acl-Override"] = "None";
viewFirstProps.author = Doc.CurrentUserEmail;
const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
@@ -702,8 +672,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.PRES), new List<Doc>(), options);
}
- export function ScriptingDocument(script: Opt<ScriptField>, options: DocumentOptions = {}, fieldKey?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script,
+ export function ScriptingDocument(script: Opt<ScriptField>|null, options: DocumentOptions = {}, fieldKey?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script: undefined,
{ ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
}
@@ -792,8 +762,8 @@ export namespace Docs {
I.tool = tool;
I["text-align"] = "center";
I.title = "ink";
- I.x = options.x;
- I.y = options.y;
+ I.x = options.x as number;
+ I.y = options.y as number;
I._width = options._width as number;
I._height = options._height as number;
I._fontFamily = "cursive";
@@ -823,7 +793,7 @@ export namespace Docs {
const nwid = options._nativeWidth || undefined;
const nhght = options._nativeHeight || undefined;
if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width);
- return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options);
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url ? url : "http://www.bing.com/"), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -934,6 +904,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) });
}
+ export function DataVizDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), undefined, { title: "Data Viz", ...options });
+ }
+
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: "remove|add", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
@@ -1256,8 +1230,8 @@ export namespace DocUtils {
return DocServer.GetRefField(id).then(field => {
if (field instanceof Doc) {
const alias = Doc.MakeAlias(field);
- alias.x = options.x || 0;
- alias.y = options.y || 0;
+ alias.x = options.x as number || 0;
+ alias.y = options.y as number || 0;
alias._width = (options._width as number) || 300;
alias._height = (options._height as number) || (options._width as number) || 300;
return alias;
@@ -1289,8 +1263,8 @@ export namespace DocUtils {
})) as ContextMenuProps[],
icon: "sticky-note"
});
- const documentList: ContextMenuProps[] = DocListCast(DocListCast(CurrentUserUtils.MyTools?.data).lastElement()?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({
- description: ":" + StrCast(dragDoc.title),
+ const documentList: ContextMenuProps[] = DocListCast(DocListCast(CurrentUserUtils.MyTools?.data)[0]?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({
+ description: ":" + StrCast(dragDoc.title).replace("Untitled ",""),
event: undoBatch((args: { x: number, y: number }) => {
const newDoc = Doc.copyDragFactory(dragDoc);
if (newDoc) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9231ccbd4..5c77041e0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,6 +1,6 @@
import { computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
-import { DataSym, Doc, DocListCast, DocListCastAsync, StrListCast } from "../../fields/Doc";
+import { DataSym, Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
@@ -8,12 +8,12 @@ import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast } from "../../fields/Types";
+import { Cast, DateCast, DocCast, FieldValue, NumCast, PromiseValue, ScriptCast, StrCast } from "../../fields/Types";
import { ImageField, nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { OmitKeys, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { Docs, DocumentOptions, DocUtils } from "../documents/Documents";
+import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
import { DocumentType } from "../documents/DocumentTypes";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
@@ -40,6 +40,7 @@ import { SnappingManager } from "./SnappingManager";
import { UndoManager } from "./UndoManager";
interface Button {
+ // DocumentOptions fields a button can set
title?: string;
toolTip?: string;
icon?: string;
@@ -52,6 +53,8 @@ interface Button {
btnList?: List<string>;
ignoreClick?: boolean;
buttonText?: string;
+
+ // fields that do not correspond to DocumentOption fields
scripts?: { script?: string; onClick?: string; }
funcs?: { [key:string]: string };
subMenu?: Button[];
@@ -78,7 +81,7 @@ export class CurrentUserUtils {
static AssignScripts(doc:Doc, scripts?:{ [key: string]: string;}, funcs?:{[key:string]: string}) {
scripts && Object.keys(scripts).map(key => {
if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && scripts[key]) {
- doc[key] = ScriptField.MakeScript(scripts[key], { dragData: DragManager.DocumentDragData.name, value:"any", scriptContext: "any" }, {"_readOnly_": true});
+ doc[key] = ScriptField.MakeScript(scripts[key], { dragData: DragManager.DocumentDragData.name, value:"any", scriptContext: "any", documentView:Doc.name}, {"_readOnly_": true});
}
});
funcs && Object.keys(funcs).map(key => {
@@ -105,58 +108,62 @@ export class CurrentUserUtils {
}
});
items?.forEach(item => !DocListCast(doc.data).includes(item) && Doc.AddDocToList(Doc.GetProto(doc), "data", item));
+ items && DocListCast(doc.data).forEach(item => !items.includes(item) && Doc.RemoveDocFromList(Doc.GetProto(doc), "data", item));
}
return doc;
}
+ static AssignDocField(doc:Doc, field:string, creator:(reqdOpts:DocumentOptions, items?:Doc[]) => Doc, reqdOpts:DocumentOptions, items?: Doc[], scripts?:{[key:string]:string}, funcs?:{[key:string]:string}) {
+ return this.AssignScripts(this.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
+ }
// initializes experimental advanced template views - slideView, headerView
- static setupExperimentalTemplateButtons(doc: Doc, field="template-experimental-buttons") {
- const tempDocs = DocCast(doc[field]);
- const requiredTypeNameFields:{opts:DocumentOptions, template:() => Doc}[] = [
+ static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) {
+ const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
{
- opts:{type: "slide", icon: "address-card"}, template: () => Docs.Create.MultirowDocument(
+ btnOpts: { title: "slide", icon: "address-card" },
+ templateOpts: { _width: 400, _height: 300, title: "slideView", childDocumentsActive: true, _xMargin: 3, _yMargin: 3, system: true },
+ template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
[
- Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
- Docs.Create.TextDocument("", { title: "text", _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) })
- ],
- { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true }
- )
+ Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
+ Docs.Create.TextDocument("", { title: "text", _fitWidth:true, _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) })
+ ], opts)
},
{
- opts:{type: "mobile", icon: "mobile"}, template: () => this.mobileButton({ title: "NEW MOBILE BUTTON", onClick: undefined, },
- [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }),
- this.mobileTextContainer({},
- [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])
- ]
+ btnOpts: { title: "mobile", icon: "mobile" },
+ templateOpts: { title: "NEW MOBILE BUTTON", onClick: undefined, },
+ template: (opts:DocumentOptions) => this.mobileButton(opts,
+ [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }),
+ this.mobileTextContainer({},
+ [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")])
+ ]
)
},
];
- const requiredTypes = requiredTypeNameFields.map(({ opts, template }) => {
- const docType = DocListCast(tempDocs?.data)?.find(doc => doc.title === opts.type);
- const reqdOpts = {
- dragFactory: template(),
- title: opts.type,
- icon: opts.icon
+ const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => {
+ const tempBtn = DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title);
+ const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory); }' };
+ const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
+ if (templateBtn) {
+ this.AssignOpts(templateBtn,btnOpts);
+ this.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions);
+ }
+ return templateBtn;
};
- const reqdScripts = {onDragStart: 'copyDragFactory(this.dragFactory)'};
const makeTemp = (doc:Doc) => {
doc.isTemplateDoc = makeTemplate(doc);
return doc;
}
- return this.AssignScripts(!docType ? makeTemp(CurrentUserUtils.createToolButton(reqdOpts)) : this.AssignOpts(docType, reqdOpts)!, reqdScripts)!;
+ return this.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: makeTemp(template(templateOpts))}), reqdScripts);
});
- const reqdOpts = {
+ const reqdOpts:DocumentOptions = {
title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true,
- _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true,
+ _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, childDocumentsActive: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
- const reqdScripts = {dropConverter : "convertToButtons(dragData)"};
- const reqdFuncs = {hidden: "IsNoviceMode()"}
- return this.AssignScripts(!tempDocs ?
- (doc[field] = Docs.Create.MasonryDocument(requiredTypes, reqdOpts)) :
- this.AssignOpts(tempDocs, reqdOpts, requiredTypes)!,
- reqdScripts, reqdFuncs);
+ const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
+ const reqdFuncs = { hidden: "IsNoviceMode()" };
+ return this.AssignScripts(this.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs);
}
/// Initializes templates that can be applied to notes
@@ -182,116 +189,77 @@ export class CurrentUserUtils {
/// Initializes collection of templates for notes and click functions
static setupDocTemplates(doc: Doc, field="myTemplates") {
+ this.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"});
const templates = [
+ DocCast(doc.presElement),
CurrentUserUtils.setupNoteTemplates(doc),
CurrentUserUtils.setupClickEditorTemplates(doc)
];
const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, };
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
- return this.AssignScripts(this.AssignOpts(DocCast(doc[field]), reqdOpts, templates) ?? (doc[field] = Docs.Create.TreeDocument(templates, reqdOpts)), reqdScripts);
+ return this.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts);
}
// setup templates for different document types when they are iconified from Document Decorations
static setupDefaultIconTemplates(doc: Doc, field="template-icons") {
- const templateIconsDoc = DocCast(doc[field]);
+ const reqdOpts = { title: "icon templates", _height: 75, system: true };
+ const templateIconsDoc = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
- const makeIconTemplate = (type: DocumentType | undefined, templateField: string, iconTemplate: () => Doc) => {
+ const makeIconTemplate = (type: DocumentType | undefined, templateField: string, iconTemplate: (opts:DocumentOptions) => Doc) => {
const iconFieldName = "icon" + (type ? "_" + type : "");
- if (!templateIconsDoc?.[iconFieldName]) {
- const template = MakeTemplate(iconTemplate(), true, iconFieldName, templateField);
- if (templateIconsDoc) {
- templateIconsDoc[iconFieldName] = template;
- }
- return template;
- }
+ return DocCast(templateIconsDoc[iconFieldName] ?? (templateIconsDoc[iconFieldName] = MakeTemplate(iconTemplate({onClick:deiconifyScript(), system: true}), true, iconFieldName, templateField))) ;
};
const deiconifyScript = () => ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" });
- const labelBox = (extra: object) => Docs.Create.LabelDocument({
- textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px",
- _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, system: true, onClick: deiconifyScript(), ...extra,
- });
- const imageBox = (url: string, extra: object) => Docs.Create.ImageDocument(url, {
- "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, _width: 360 / 4, _height: 270 / 4,
- _showTitle: "title", system: true, onClick: deiconifyScript(), ...extra
- });
- const fontBox = () => Docs.Create.FontIconDocument({
- _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, system: true, onClick: deiconifyScript()
+ const labelBox = (opts: object) => Docs.Create.LabelDocument({
+ textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
});
+ const imageBox = (url: Opt<string>, opts: object) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, _width: 360 / 4, _height: 270 / 4, _showTitle: "title", ...opts });
+ const fontBox = (opts:DocumentOptions) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
const iconTemplates = [
- makeIconTemplate(undefined, "title", () => labelBox({ _backgroundColor: "dimgray" })),
- makeIconTemplate(DocumentType.AUDIO, "title", () => labelBox({ _backgroundColor: "lightgreen" })),
- makeIconTemplate(DocumentType.PDF, "title", () => labelBox({ _backgroundColor: "pink" })),
- makeIconTemplate(DocumentType.WEB, "title", () => labelBox({ _backgroundColor: "brown" })),
- makeIconTemplate(DocumentType.RTF, "text", () => labelBox({ _showTitle: "creationDate" })),
- makeIconTemplate(DocumentType.IMG, "data", () => imageBox("", { _height: undefined, })),
- makeIconTemplate(DocumentType.COL, "icon", () => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", {})),
- makeIconTemplate(DocumentType.VID, "icon", () => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", {})),
- makeIconTemplate(DocumentType.BUTTON, "data", fontBox),
+ makeIconTemplate(undefined, "title", (opts) => labelBox({ ...opts, _backgroundColor: "dimgray" })),
+ makeIconTemplate(DocumentType.AUDIO, "title", (opts) => labelBox({ ...opts, _backgroundColor: "lightgreen" })),
+ makeIconTemplate(DocumentType.PDF, "title", (opts) => labelBox({ ...opts, _backgroundColor: "pink" })),
+ makeIconTemplate(DocumentType.WEB, "title", (opts) => labelBox({...opts, _backgroundColor: "brown" })),
+ makeIconTemplate(DocumentType.RTF, "text", (opts) => labelBox({ ...opts, _showTitle: "creationDate" })),
+ makeIconTemplate(DocumentType.IMG, "data", (opts) => imageBox("", { ...opts, _height: undefined, })),
+ makeIconTemplate(DocumentType.COL, "icon", (opts) => imageBox(undefined, opts)),
+ makeIconTemplate(DocumentType.VID, "icon", (opts) => imageBox(undefined, opts)),
+ makeIconTemplate(DocumentType.BUTTON, "data", (opts) => fontBox(opts)),
//nasty hack .. templates are looked up exclusively by type -- but we want a template for a document with a certain field (transcription) .. so this hack and the companion hack in createCustomView does this for now
- makeIconTemplate("transcription" as any, "transcription", () => labelBox({ _backgroundColor: "orange" })),
+ makeIconTemplate("transcription" as any, "transcription", (opts) => labelBox({ ...opts, _backgroundColor: "orange" })),
// makeIconTemplate(DocumentType.PDF, "icon", () => imageBox("http://www.cs.brown.edu/~bcz/noImage.png", {}))
].filter(d => d).map(d => d!);
-
- const reqdOpts = { title: "icon templates", _height: 75, system: true };
- this.AssignOpts(templateIconsDoc, reqdOpts, iconTemplates) ?? (doc[field] = Docs.Create.TreeDocument(iconTemplates, reqdOpts));
+ this.AssignOpts(DocCast(doc[field]), {}, iconTemplates);
}
- /// initalizes the set of default versions of most document types
+ /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation
static creatorBtnDescriptors(doc: Doc): {
title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc,
backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
}[] {
- const standardOps = () => ({ _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
- const emptyThings:{key:string, // the field name where the empty thing will be stored
- opts:DocumentOptions, // the document options that are required for the empty thing
- funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing
- creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist
- }[] = [
- {key: "emptyPresentation", creator: Docs.Create.PresDocument, opts: { title: "Untitled Presentation", _viewType: CollectionViewType.Stacking, _width: 400, _height: 500, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
- {key: "emptyCollection", creator: (opts) => Docs.Create.FreeformDocument([], opts), opts: { title: "freeform", _width: 150, _height: 100 }},
- {key: "emptyPane", creator: (opts) => Docs.Create.FreeformDocument([], opts), opts: { title: "Untitled Tab", _backgroundGridShow: true, _width: 500, _height: 800 }},
- {key: "emptyMath", creator: (opts) => Docs.Create.EquationDocument(opts), opts: { title: "Equation", _backgroundGridShow: true, _width: 300, _height: 35 }},
- {key: "emptySlide", creator: (opts) => Docs.Create.TreeDocument([], opts), funcs: {title: 'self.text?.Text'}, opts: {
- _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
- "dragFactory-count": undefined, allowOverlayDrop: true, treeViewType: TreeViewType.outline, _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "white"
- }},
- {key: "emptyComparison", creator: Docs.Create.ComparisonDocument, opts: { title: "Comparer", _width: 300, _height: 300 }},
- {key: "emptyScript", creator: (opts) => Docs.Create.ScriptingDocument(undefined, opts), opts: { title: "script", _width: 200, _height: 250, }},
- {key: "emptyScreenshot", creator: Docs.Create.ScreenshotDocument, opts: { title: "empty screenshot", _width: 400, _height: 200 }},
- {key: "emptyWebCam", creator: (opts) => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, title: "recording", recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
- {key: "emptyAudio", creator: (opts) => Docs.Create.AudioDocument(nullAudio, opts), opts: { title: "audio recording", x: 200, y: 200, _width: 200, _height: 100, }},
- {key: "emptyNote", creator: (opts) => Docs.Create.TextDocument("", opts), opts: { title: "text note", _width: 200, _autoHeight: true }},
- {key: "emptyButton", creator: Docs.Create.ButtonDocument, opts: { title: "Button", _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
- {key: "emptyWebpage", creator: (opts) => Docs.Create.WebDocument("http://www.bing.com/", opts), opts: { title: "webpage", _nativeWidth: 850, _height: 512, _width: 400, useCors: true, }},
- {key: "emptyMap", creator: (opts) => Docs.Create.MapDocument([], opts), opts: { title: "map", _showSidebar: true, _width: 800, _height: 600, }}
- ];
-
- emptyThings.forEach(thing =>
- this.AssignScripts(this.AssignOpts(DocCast(doc[thing.key]), {...standardOps(), ...thing.opts}) ?? (doc[thing.key] = thing.creator({...standardOps(), ...thing.opts})), undefined, thing.funcs))
-
- if (doc.emptyHeader === undefined) {
- const json = {
- doc: {
- type: "doc",
- content: [
- {
- type: "paragraph", attrs: {}, content: [{
- type: "dashField",
- attrs: { fieldKey: "author", docid: "", hideKey: false },
- marks: [{ type: "strong" }]
- }, {
- type: "dashField",
- attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
- marks: [{ type: "strong" }]
- }]
+ const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
+ const json = {
+ doc: {
+ type: "doc",
+ content: [
+ {
+ type: "paragraph", attrs: {}, content: [{
+ type: "dashField",
+ attrs: { fieldKey: "author", docid: "", hideKey: false },
+ marks: [{ type: "strong" }]
+ }, {
+ type: "dashField",
+ attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+ marks: [{ type: "strong" }]
}]
- },
- selection: { type: "text", anchor: 1, head: 1 },
- storedMarks: []
- };
- const headerBtnHgt = 10;
- const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), {
- ...standardOps(), title: "text", _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,
+ }]
+ },
+ selection: { type: "text", anchor: 1, head: 1 },
+ storedMarks: []
+ };
+ const headerBtnHgt = 10;
+ const headerTemplate = (opts:DocumentOptions) => {
+ const header = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { ...opts, title: "text",
layout:
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
@@ -304,28 +272,55 @@ export class CurrentUserUtils {
// " <FormattedTextBox {...props} fieldKey={'header'} dontSelectOnLoad={'true'} ignoreAutoHeight={'true'} pointerEvents='{this._headerPointerEvents||`none`}' fontSize='{this._headerFontSize}px' height='{this._headerHeight}px' background='{this._headerColor||this.target.mySharedDocs.userColor}' />" +
// " <FormattedTextBox {...props} fieldKey={'text'} position='absolute' top='{(this._headerHeight)*scale}px' height='calc({100/scale}% - {this._headerHeight}px)'/>" +
// "</div>";
- Doc.GetProto(headerTemplate).isTemplateDoc = makeTemplate(Doc.GetProto(headerTemplate), true, "headerView");
- doc.emptyHeader = headerTemplate;
+ Doc.GetProto(header).isTemplateDoc = makeTemplate(Doc.GetProto(header), true, "headerView");
+ Doc.GetProto(header).title = "Untitled Header";
+ return header;
}
+ const emptyThings:{key:string, // the field name where the empty thing will be stored
+ opts:DocumentOptions, // the document options that are required for the empty thing
+ funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing
+ creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist
+ }[] = [
+ {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }},
+ {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }},
+ {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
+ {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }},
+ {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
+ {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }},
+ {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }},
+ {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
+ {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
+ {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
+ {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
+ {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
+ {key: "Presentation",creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 500, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
+ {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree,
+ treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
+ allowOverlayDrop: true, treeViewType: TreeViewType.outline,
+ backgroundColor: "white", _xMargin: 0, _yMargin: 0, _singleLine: true
+ }, funcs: {title: 'self.text?.Text'}},
+ ];
+
+ emptyThings.forEach(thing => this.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs));
return [
- { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, clickFactory: doc.emptyPane as Doc, },
- { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyMath as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: {onClick: 'openInOverlay(copyDragFactory(this.dragFactory))',onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a custom note", title: "Custom", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a progressive slide",title: "Slide", icon: "file", dragFactory: doc.emptySlide as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreenshot as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, funcs:{ hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a mobile view", title: "Phone", icon: "mobile", dragFactory: doc.activeMobileMenu as Doc, scripts: {onClick: 'openOnRight(Doc.UserDoc().activeMobileMenu)', onDragStart: 'this.dragFactory'}, funcs: {hidden: 'IsNoviceMode()'} },
+ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, },
+ { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
+ { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, },
+ { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, },
+ { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, },
+ { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
+ { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, },
+ { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} },
+ { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}},
+ { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} },
+ { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}},
+ { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, },
+ { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, scripts: {onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
{ toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: {onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
- // { toolTip: "Tap or drag to create a presentation", title: "Trails", icon: "pres-trail", dragFactory: doc.emptyPresentation as Doc,scripts: {onClick: 'openOnRight(Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory))', onDragStart: `Doc.UserDoc().activePresentation = copyDragFactory(this.dragFactory)`}, funcs: {hidden: 'IsNoviceMode()'} },
- ];
+ ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, }))
}
/// Initalizes the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
@@ -340,9 +335,10 @@ export class CurrentUserUtils {
return this.AssignScripts(this.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs);
});
- const reqdOpts = {
+ const reqdOpts:DocumentOptions = {
title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
_autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
+ childDocumentsActive: true
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return this.AssignScripts(this.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -352,34 +348,26 @@ export class CurrentUserUtils {
static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] {
const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
return [
- { title: "Dashboards", target: CurrentUserUtils.MyDashboards, icon: "desktop", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "Search", target: CurrentUserUtils.MySearcher, icon: "search", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "Files", target: CurrentUserUtils.MyFilesystem, icon: "folder-open", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "Tools", target: CurrentUserUtils.MyTools, icon: "wrench", scripts:{onClick: 'selectMainMenu(self)'}, funcs: {hidden: "IsNoviceMode()"} },
- { title: "Imports", target: CurrentUserUtils.MyImports, icon: "upload", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "Recently Closed", target: CurrentUserUtils.MyRecentlyClosed, icon: "archive", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "Shared with me", target: CurrentUserUtils.MySharedDocs, icon: "users", scripts:{onClick: 'selectMainMenu(self)'}, funcs:{badgeValue:badgeValue}},
- { title: "Trails", target: CurrentUserUtils.MyTrails, icon: "pres-trail", scripts:{onClick: 'selectMainMenu(self)'} },
- { title: "User Doc", target: CurrentUserUtils.MyUserDocView, icon: "address-card",scripts:{onClick: 'selectMainMenu(self)'}, funcs: {hidden: "IsNoviceMode()"} },
- ];
+ { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", },
+ { title: "Search", target: this.setupSearcher(doc, "mySearcher"), icon: "search", },
+ { title: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", },
+ { title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", },
+ { title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", },
+ { title: "Shared Docs", target: this.MySharedDocs, icon: "users", funcs:{badgeValue:badgeValue}},
+ { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", },
+ { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
+ ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}}));
}
- /// setup the left sidebar container and panels that can be displayed within it
+ /// the empty panel that is filled with whichever left menu button's panel has been selected
static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") {
- this.AssignOpts(DocCast(doc[field]), {}) ?? (doc[field] = ((doc:Doc) => {doc.system = true; return doc;})(new Doc()));
- CurrentUserUtils.setupSearcher(doc, "mySearcher");
- CurrentUserUtils.setupToolsBtnPanel(doc, "myTools");
- CurrentUserUtils.setupImportSidebar(doc, "myImports");
- CurrentUserUtils.setupDashboards(doc, "myDashboards");
- CurrentUserUtils.setupTrails(doc, "myTrails");
- CurrentUserUtils.setupFilesystem(doc, "myFilesystem");
- CurrentUserUtils.setupRecentlyClosedDocs(doc, "myRecentlyClosed");
- CurrentUserUtils.setupUserDocView(doc, "myUserDocView");
+ this.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true});
}
/// Initializes the left sidebar menu buttons and the panels they open up
static setupLeftSidebarMenu(doc: Doc, field="myLeftSidebarMenu") {
- this.setupLeftSidebarPanel(doc); // the tools/panels opened up by the menu buttons
+ this.setupLeftSidebarPanel(doc);
const myLeftSidebarMenu = DocCast(doc[field]);
const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => {
const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
@@ -395,14 +383,13 @@ export class CurrentUserUtils {
title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
_chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true
};
- const reqdScripts = { dropConverter: "convertToButtons(dragData)" }
- return this.AssignScripts(this.AssignOpts(myLeftSidebarMenu, reqdStackOpts, menuBtns) ?? (doc[field] = Docs.Create.StackingDocument(menuBtns, reqdStackOpts)), reqdScripts);
+ return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
}
// Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu
static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") {
const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,};
- this.AssignOpts(DocCast(doc[field]), reqdOpts, this.setupMobileButtons()) ?? (doc[field] = Docs.Create.StackingDocument(this.setupMobileButtons(), reqdOpts));
+ this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts);
}
// Sets up mobile buttons for inside mobile menu
@@ -511,30 +498,28 @@ export class CurrentUserUtils {
/// Search option on the left side button panel
static setupSearcher(doc: Doc, field:string) {
- const reqdOpts:DocumentOptions = {
- dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", system: true, childDropAction: "alias",
- _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true,
- };
- this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.SearchDocument(reqdOpts));
+ return this.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), {
+ dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", system: true, childDropAction: "alias",
+ _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true, });
}
/// Initializes the panel of draggable tools that is opened from the left sidebar.
static setupToolsBtnPanel(doc: Doc, field:string) {
const myTools = DocCast(doc[field]);
const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined);
- //const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc);
+ const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined);
const reqdToolOps:DocumentOptions = {
title: "My Tools", system: true, ignoreClick: true, boxShadow: "0 0",
_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
};
- this.AssignOpts(myTools, reqdToolOps, [creatorBtns, /*templateBtns*/]) ?? (doc[field] = Docs.Create.StackingDocument([creatorBtns, /*templateBtns*/], reqdToolOps));
+ return this.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]);
}
/// initializes the left sidebar dashboard pane
static setupDashboards(doc: Doc, field:string) {
var myDashboards = DocCast(doc[field]);
- const newDashboard = `createNewDashboard(Doc.UserDoc())`;
+ const newDashboard = `createNewDashboard()`;
const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true,
title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", system: true };
const reqdBtnScript = {onClick: newDashboard,}
@@ -551,7 +536,7 @@ export class CurrentUserUtils {
childContextMenuIcons: new List<string>(["chalkboard", "tv", "camera", "users", "times"]), // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
};
- myDashboards = this.AssignOpts(myDashboards, reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
+ myDashboards = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`;
const contextMenuScripts = [newDashboard];
const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
@@ -585,18 +570,18 @@ export class CurrentUserUtils {
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
explainer: "All of the trails that you have created will appear here."
};
- myTrails = this.AssignOpts(myTrails, reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([],reqdOpts ));
+ myTrails = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
const contextMenuScripts = [reqdBtnScript.onClick];
if (Cast(myTrails.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
myTrails.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
}
+ return myTrails;
}
/// initializes the left sidebar File system pane
static setupFilesystem(doc: Doc, field:string) {
var myFilesystem = DocCast(doc[field]);
- const reqdOrphansOpts:DocumentOptions = { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true };
- this.AssignOpts(DocCast(doc.myFileOrphans), reqdOrphansOpts) ?? (doc.myFileOrphans = Docs.Create.TreeDocument([], reqdOrphansOpts));
+ const myFileOrphans = this.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true });
const newFolder = `makeTopLevelFolder()`;
const newFolderOpts: DocumentOptions = {
@@ -614,15 +599,16 @@ export class CurrentUserUtils {
childContextMenuIcons: new List<string>(["plus"]),
explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
};
- myFilesystem = this.AssignOpts(myFilesystem, reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([DocCast(doc.myFileOrphans)], reqdOpts));
+ myFilesystem = this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]);
const childContextMenuScripts = [newFolder];
if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) {
myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
}
+ return myFilesystem;
}
/// initializes the panel displaying docs that have been recently closed
- static setupRecentlyClosedDocs(doc: Doc, field:string) {
+ static setupRecentlyClosed(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true,
treeViewTruncateTitleWidth: 150, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
@@ -630,22 +616,20 @@ export class CurrentUserUtils {
contextMenuIcons:new List<string>(["trash"]),
explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
};
- const recentlyClosed = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
+ const recentlyClosed = this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
const clearAll = (target:string) => `getProto(${target}).data = new List([])`;
const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _hideContextMenu: true,
title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", system: true,
toolTip: "Empty recently closed",};
- const clearBtnsScripts = {onClick: clearAll("self.target")}
- const clearDocsButton = this.AssignScripts(
- this.AssignOpts(DocCast(recentlyClosed?.clearDocsBtn), clearBtnsOpts) ?? (recentlyClosed.clearDocsBtn = Docs.Create.FontIconDocument(clearBtnsOpts)),
- clearBtnsScripts);
+ const clearDocsButton = this.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton;
if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) {
recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!])
}
+ return recentlyClosed;
}
/// creates a new, empty filter doc
@@ -668,135 +652,123 @@ export class CurrentUserUtils {
boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true,
treeViewHideTitle: true, treeViewTruncateTitleWidth: 150
};
- if (!doc[field]) this.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" })
- this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([doc], reqdOpts));
+ if (!doc[field]) this.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
+ return this.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]);
}
- static linearButtonList = (title: string, opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, {
- title, ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true,
+ static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, {
+ ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
_lockedPosition: true, system: true, flexDirection: "row"
})
static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
- btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true, _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts,
+ btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true,
+ _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]),
+ _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts,
})
/// initializes the required buttons in the expanding button menu at the bottom of the Dash window
static setupDockedButtons(doc: Doc, field="myDockedBtns") {
const dockedBtns = DocCast(doc[field]);
- const dockBtn = (title:string, onClick: string, opts: DocumentOptions) => {
- const btn = this.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === title), opts) ??
- CurrentUserUtils.createToolButton({title, ...opts});
- this.AssignScripts(btn, {onClick})
- return btn;
- }
+ const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}) =>
+ this.AssignScripts(this.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ??
+ CurrentUserUtils.createToolButton(opts), scripts);
+
const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
- { title: "undo", click: "undo()", opts: { icon: "undo-alt", toolTip: "Click to undo" }},
- { title: "redo", click:"redo()", opts: { icon: "redo-alt", toolTip: "Click to redo" }}
+ { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
+ { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}
];
- const btns = btnDescs.map(desc => dockBtn(desc.title, desc.click, {_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}));
+ const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
const dockBtnsReqdOpts = {
title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true,
childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
};
reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
- return this.AssignOpts(dockedBtns, dockBtnsReqdOpts, btns) ?? (doc[field] = CurrentUserUtils.linearButtonList("dockedBtns", dockBtnsReqdOpts, btns));
+ return this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
}
static textTools():Button[] {
return [
- {
- title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true,
- btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]),
- scripts :{script : 'setFont(value, _readOnly_)'}
- },
- { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'} },
- { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts:{script: '{ return setFontColor(value, _readOnly_); }'}},
- { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
- { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
- { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline",scripts: {onClick:'{ return toggleUnderline(_readOnly_);}'} },
- { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} },
- { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} },
+ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(value, _readOnly_)'},
+ btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
+ { title: "Size", toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
+ { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_); }'}},
+ { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
+ { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick:'{ return toggleUnderline(_readOnly_);}'} },
+ { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} },
+ { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} },
// { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
// { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
// { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
- { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
+ { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
{ title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
- { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
- { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+ { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
+ { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
];
}
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts:{onClick:'{ return setActiveTool("pen", _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts:{onClick:'{ return setActiveTool("write", _readOnly_);}'} },
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts:{onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts: {onClick:'{ return setActiveTool("pen", _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", _readOnly_);}'} },
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", _readOnly_);}' }},
// { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
- { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts:{onClick:'{ return setActiveTool("circle", _readOnly_);}'} },
+ { title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:'{ return setActiveTool("circle", _readOnly_);}'} },
// { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveTool("square")' },
- { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts:{onClick: '{ return setActiveTool("line", _readOnly_);}' }},
- { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", scripts: {script: "{ return setFillColor(value, _readOnly_);}"} },
- { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, numBtnType: NumButtonType.Slider, numBtnMin: 1, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'} },
- { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
+ { title: "Line", toolTip: "Line (Ctrl+Shift+L)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick: '{ return setActiveTool("line", _readOnly_);}' }},
+ { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: "{ return setFillColor(value, _readOnly_);}"} },
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
+ { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
];
}
static schemaTools():Button[] {
- return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{onClick:'toggleSchemaPreview(_readOnly_)'}, }];
+ return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }];
}
static webTools() {
return [
- { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", click: 'webBack(_readOnly_)' },
- { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", click: 'webForward(_readOnly_)' },
+ { title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }},
+ { title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}},
//{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' },
- { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, script: 'webSetURL(value, _readOnly_)' },
+ { title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
];
}
static contextMenuTools():Button[] {
return [
- {
- title: "Perspective", toolTip: "View", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true,
- btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
+ { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
CollectionViewType.Grid]),
- scripts: {script: 'setView(value, _readOnly_)'},
- }, // Always show
- { title: "Back", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton,icon: "chevron-left", scripts:{onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'} },
- { title: "Fwd", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, icon: "chevron-right", scripts:{onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'}},
- { title: "Fill", toolTip: "Background Fill Color", width: 20, btnType: ButtonType.ColorButton, ignoreClick: true, icon: "fill-drip", scripts: { script: "setBackgroundColor(value, _readOnly_)"}, funcs:{ hidden: 'selectedDocumentType()' }}, // Only when a document is selected
- { title: "Header", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, icon: "heading", scripts: {script: "setHeaderColor(value, _readOnly_)"}, funcs : {hidden: 'selectedDocumentType()'} },
- { title: "Overlay", toolTip: "Overlay", btnType: ButtonType.ToggleButton, icon: "layer-group", scripts: {onClick : 'toggleOverlay(_readOnly_)'}, funcs: {hidden: 'selectedDocumentType(undefined, "freeform", true)'} }, // Only when floating document is selected in freeform
- // { title: "Alias", btnType: ButtonType.ClickButton, icon: "copy", hidden: 'selectedDocumentType()' }, // Only when a document is selected
- { title: "Text", icon: "text", subMenu: CurrentUserUtils.textTools(), funcs: { linearViewIsExpanded: `selectedDocumentType("${DocumentType.RTF}")`} }, // Always available
- { title: "Ink", icon: "ink", subMenu: CurrentUserUtils.inkTools(), funcs: { linearViewIsExpanded: `selectedDocumentType("${DocumentType.INK}")`} }, // Always available
- { title: "Web", icon: "web", subMenu: CurrentUserUtils.webTools(), funcs: { linearViewIsExpanded: `selectedDocumentType("${DocumentType.WEB}")`, hidden: `!selectedDocumentType("${DocumentType.WEB}")`} }, // Only when Web is selected
- { title: "Schema", icon: "schema", subMenu: CurrentUserUtils.schemaTools(), funcs: { linearViewIsExpanded: `selectedDocumentType(undefined, "${CollectionViewType.Schema}")`, hidden: `!selectedDocumentType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+ title: "Perspective", toolTip: "View", width: 100,btnType: ButtonType.DropdownList,ignoreClick: true, scripts: { script: 'setView(value, _readOnly_)'}},
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", width: 20, btnType: ButtonType.ClickButton, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, funcs: {hidden: 'IsNoviceMode()'}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",width: 20, btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setBackgroundColor(value, _readOnly_)'},funcs: {hidden: '!selectedDocumentType()'}}, // Only when a document is selected
+ { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, ignoreClick: true, scripts: { script: 'setHeaderColor(value, _readOnly_)'}, funcs: {hidden: '!selectedDocumentType()'}},
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, scripts: { onClick: 'toggleOverlay(_readOnly_)'}, funcs: {hidden: '!selectedDocumentType(undefined, "freeform", true)'}}, // Only when floating document is selected in freeform
+ { title: "Text", icon: "text", subMenu: CurrentUserUtils.textTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.RTF}")`} }, // Always available
+ { title: "Ink", icon: "ink", subMenu: CurrentUserUtils.inkTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.INK}")`} }, // Always available
+ { title: "Web", icon: "web", subMenu: CurrentUserUtils.webTools(), funcs: {linearViewIsExpanded: `selectedDocumentType("${DocumentType.WEB}")`, hidden: `!selectedDocumentType("${DocumentType.WEB}")`} }, // Only when Web is selected
+ { title: "Schema", icon: "schema", subMenu: CurrentUserUtils.schemaTools(), funcs: {linearViewIsExpanded: `selectedDocumentType(undefined, "${CollectionViewType.Schema}")`, hidden: `!selectedDocumentType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
];
}
/// initializes a context menu button for the top bar context menu
static setupContextMenuButton(params:Button, btnDoc?:Doc) {
const reqdOpts:DocumentOptions = {
- title: params.title, btnType: params.btnType, icon: params.icon, toolTip: params.toolTip, ignoreClick: params.ignoreClick,
- numBtnType: params.numBtnType, numBtnMin: params.numBtnMin, numBtnMax: params.numBtnMax,_nativeHeight: 30, btnList: params.btnList,
- backgroundColor: params.scripts?.onClick ? undefined: "transparent", /// a bit hacky. if an onClick is specified, then we assume we assume a toggle use onClick to get the backgroundColor (see below). Otherwise, assume a transparent background
- _nativeWidth: params.width ? params.width : 30,
- _width: params.width ? params.width : 30,
- _height: 30,
+ ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+ backgroundColor: params.scripts?.onClick ? undefined: "transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background
color: Colors.WHITE, system: true, dontUndo: true,
- _stayInCollection: true,
- _hideContextMenu: true,
- _lockedPosition: true,
- _dropAction: "alias",
- _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ _nativeWidth: params.width ?? 30, _width: params.width ?? 30,
+ _height: 30, _nativeHeight: 30,
+ _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
+ _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
@@ -813,36 +785,36 @@ export class CurrentUserUtils {
if (!params.subMenu) {
return this.setupContextMenuButton(params, menuBtnDoc);
} else {
- const reqdSubMenuOpts = { title: params.title, icon: params.icon, childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true,
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, title:"submenu",
+ childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true,
linearViewSubMenu: true, linearViewExpandable: true, };
- const reqdSubMenuFuncs:{[key:string]:any} = { ...params.funcs};
return this.AssignScripts(this.AssignOpts(menuBtnDoc, reqdSubMenuOpts) ??
- CurrentUserUtils.linearButtonList("submenu", reqdSubMenuOpts, params.subMenu.map(sub =>
+ this.linearButtonList(reqdSubMenuOpts, params.subMenu.map(sub =>
this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
- )), undefined, reqdSubMenuFuncs);
+ )), undefined, params.funcs);
}
});
- const reqdCtxtOpts = { title: "menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
- return this.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts) ?? (doc[field] = CurrentUserUtils.linearButtonList("contextMenuButtons", reqdCtxtOpts, ctxtMenuBtns));
+ const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+ return this.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, ctxtMenuBtns);
}
/// collection of documents rendered in the overlay layer above all tabs and other UI
static setupOverlays(doc: Doc, field = "myOverlayDocs") {
- const reqdOpts = { title: "overlay documents", backgroundColor: "#aca3a6", system: true };
- return this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.FreeformDocument([], reqdOpts));
+ return this.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true });
+ }
+
+ static setupPublished(doc:Doc, field = "myPublishedDocs") {
+ return this.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true });
}
/// The database of all links on all documents
- static async setupLinkDocs(doc: Doc, linkDatabaseId: string) {
- if (doc.myLinkDatabase === undefined) {
- let linkDocs = Docs.newAccount ? undefined : await DocServer.GetRefField(linkDatabaseId);
- if (!linkDocs) {
- linkDocs = new Doc(linkDatabaseId, true);
- (linkDocs as Doc).title = "LINK DATABASE: " + Doc.CurrentUserEmail;
- (linkDocs as Doc).author = Doc.CurrentUserEmail;
- (linkDocs as Doc).data = new List<Doc>([]);
- (linkDocs as Doc)["acl-Public"] = SharingPermissions.Augment;
- }
+ static setupLinkDocs(doc: Doc, linkDatabaseId: string) {
+ if (!(Docs.newAccount ? undefined : DocCast(doc.myLinkDatabase))) {
+ const linkDocs = new Doc(linkDatabaseId, true);
+ linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
+ linkDocs.author = Doc.CurrentUserEmail;
+ linkDocs.data = new List<Doc>([]);
+ linkDocs["acl-Public"] = SharingPermissions.Augment;
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
}
@@ -851,11 +823,12 @@ export class CurrentUserUtils {
// 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) {
+ static 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 dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}";
+
+ const sharedScripts = { treeViewChildDoubleClick: dblClkScript, }
const sharedDocOpts:DocumentOptions = {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
@@ -863,9 +836,6 @@ export class CurrentUserUtils {
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
@@ -873,27 +843,24 @@ export class CurrentUserUtils {
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"));
- return this.AssignOpts(DocCast(sharedDocs), sharedRequiredDocOpts) ??
- (doc.mySharedDocs = Docs.Create.TreeDocument([], {...sharedDocOpts, ...sharedRequiredDocOpts}, sharingDocumentId + "outer", sharingDocumentId));
+ this.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
}
/// Import option on the left side button panel
- static setupImportSidebar(doc: Doc, field:string) {
+ static setupImportSidebar(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = {
title: "My Imports", _forceActive: true, buttonMenu: true, 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,
dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go."
};
- const myImports = this.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.StackingDocument([], reqdOpts));
+ const myImports = this.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts);
const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer",
_width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
buttonText: "Import", icon: "upload", system: true };
- const reqdBtnScripts = { onClick: "importDocument()" };
- const newImportBtn = this.AssignOpts(DocCast(myImports.buttonMenuDoc), reqdBtnOpts) ?? (myImports.buttonMenuDoc = Docs.Create.FontIconDocument(reqdBtnOpts));
- this.AssignScripts(newImportBtn, reqdBtnScripts);
+ this.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ return myImports;
}
static setupClickEditorTemplates(doc: Doc) {
@@ -906,9 +873,8 @@ export class CurrentUserUtils {
targetScriptKey: "onChildClick", system: true
});
- const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
- "openOnRight(self.doubleClickView)",
- {}), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick", system: true });
+ const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( "openOnRight(self.doubleClickView)", {}),
+ { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick", system: true });
doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates", system: true });
}
@@ -944,22 +910,23 @@ export class CurrentUserUtils {
return doc.clickFuncs as Doc;
}
- static async updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
- doc.globalGroupDatabase ?? (doc.globalGroupDatabase = Docs.Prototypes.MainGroupDocument());
- await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
+
+ /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be
+ /// written to the server if the code hasn't changed. However, choices need to be made for each Doc/field
+ /// whether to revert to "default" values, or to leave them as the user/system last set them.
+ static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
+ this.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {});
reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]),
async () => {
const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
SnappingManager.SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
}, { fireImmediately: true });
- // Document properties on load
doc.system ?? (doc.system = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
Doc.noviceMode ?? (Doc.noviceMode = true);
doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true);
doc._showLabel ?? (doc._showLabel = true);
- doc._showMenuLabel ?? (doc._showMenuLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
doc.activeInkWidth ?? (doc.activeInkWidth = 1);
@@ -976,21 +943,21 @@ export class CurrentUserUtils {
doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
doc.filterDocCount = 0;
doc.freezeChildren = "remove|add";
- doc.myPublishedDocs ?? (doc.myPublishedDocs = new List<Doc>());
- doc.myHeaderBar ?? (doc.myHeaderBar = Docs.Create.MulticolumnDocument([], { title: "header bar", system: true })); // drop down panel at top of dashboard for stashing documents
- await this.setupLinkDocs(doc, linkDatabaseId);
- await this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
+ this.setupLinkDocs(doc, linkDatabaseId);
+ this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
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
this.setupActiveMobileMenu(doc); // sets up the current mobile menu for Dash Mobile
this.setupOverlays(doc); // sets up the overlay panel where documents and other widgets can be added to float over the rest of the dashboard
+ this.setupPublished(doc); // sets up the list doc of all docs that have been published (meaning that they can be auto-linked by typing their title into another text box)
this.setupContextMenuButtons(doc); // set up the row of buttons at the top of the dashboard that change depending on what is selected
this.setupDockedButtons(doc); // the bottom bar of font icons
this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left
- doc.globalScriptDatabase ?? ( doc.globalScriptDatabase = Docs.Prototypes.MainScriptDocument());
-
+ this.setupDocTemplates(doc); // sets up the template menu of templates
+ this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
+ this.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
+ this.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents
+
setTimeout(() => DocServer.UPDATE_SERVER_CACHE(), 2500);
- doc.fieldInfos = await Docs.setupFieldInfos();
if (doc.activeDashboard instanceof Doc) {
// undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme;
@@ -998,6 +965,24 @@ export class CurrentUserUtils {
return doc;
}
+ static setupFieldInfos(doc:Doc, field="fieldInfos") {
+ const fieldInfoOpts = { title: "Field Infos", system: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object
+ const infos = this.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts);
+ const entries = Object.entries(new DocumentOptions());
+ entries.forEach(pair => {
+ if (!Array.from(Object.keys(fieldInfoOpts)).includes(pair[0])) {
+ const options = pair[1] as FInfo;
+ const opts:DocumentOptions = { system: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")};
+ switch (options.fieldType) {
+ case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break;
+ case "number": opts.fieldValues = new List<number>(options.values as any); break;
+ case "Doc": opts.fieldValues = new List<Doc>(options.values as any); break;
+ default: opts.fieldValues = new List<string>(options.values as any); break;// string, pointerEvents, dimUnit, dropActionType
+ }
+ this.AssignDocField(infos, pair[0], opts => Doc.assign(new Doc(), OmitKeys(opts,["values"]).omit), opts);
+ }
+ });
+ }
public static async loadCurrentUser() {
return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
@@ -1039,18 +1024,15 @@ export class CurrentUserUtils {
public static _urlState: HistoryUtil.DocUrl;
- public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => {
+ public static openDashboard = (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
- if (!DocListCast(CurrentUserUtils.MyDashboards.data).includes(doc)) {
- Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", 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" })]));
- userDoc ? (userDoc.activeDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc);
- }
+ // this has the side-effect of setting the main container since we're assigning the active/guest dashboard
+ Doc.UserDoc() ? (CurrentUserUtils.ActiveDashboard = doc) : (CurrentUserUtils.GuestDashboard = doc);
+
const state = CurrentUserUtils._urlState;
- if (state.sharing === true && !userDoc) {
+ if (state.sharing === true && !Doc.UserDoc()) {
DocServer.Control.makeReadOnly();
} else {
fromHistory || HistoryUtil.pushState({
@@ -1086,30 +1068,24 @@ export class CurrentUserUtils {
input.onchange = async _e => {
const upload = Utils.prepend("/uploadDoc");
const formData = new FormData();
- const file = input.files && input.files[0];
- if (file && file.type === 'application/zip') {
- formData.append('file', file);
- formData.append('remap', "true");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json !== "error") {
- const doc = Docs.newAccount ? undefined : await DocServer.GetRefField(json);
- if (doc instanceof Doc) {
- setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
- docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
- }
- }
+ const file = input.files?.[0];
+ if (file?.type === 'application/zip') {
+ const doc = await Doc.importDocument(file);
+ // NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) {
+ // setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
+ // docs.docs.forEach(d => LinkManager.Instance.addLink(d))), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
+ // }
+ const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null);
+ doc instanceof Doc && list?.splice(0, 0, doc);
} else if (input.files && input.files.length !== 0) {
- const importDocs = CurrentUserUtils.MyImports;
const disposer = OverlayView.ShowSpinner();
- DocListCastAsync(importDocs.data).then(async list => {
- const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {});
- if (results.length !== input.files?.length) {
- alert("Error uploading files - possibly due to unsupported file types");
- }
- list?.splice(0, 0, ...results);
- disposer();
- });
+ const results = await DocUtils.uploadFilesToDocs(Array.from(input.files || []), {});
+ if (results.length !== input.files?.length) {
+ alert("Error uploading files - possibly due to unsupported file types");
+ }
+ const list = Cast(CurrentUserUtils.MyImports.data, listSpec(Doc), null);
+ list?.splice(0, 0, ...results);
+ disposer();
} else {
console.log("No file selected");
}
@@ -1140,11 +1116,11 @@ export class CurrentUserUtils {
}
- public static async snapshotDashboard(userDoc: Doc) {
+ public static async snapshotDashboard() {
if (CurrentUserUtils.ActiveDashboard) {
const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard);
Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", copy);
- CurrentUserUtils.openDashboard(userDoc, copy);
+ CurrentUserUtils.openDashboard(copy);
}
}
@@ -1152,17 +1128,9 @@ export class CurrentUserUtils {
CurrentUserUtils.ActiveDashboard = undefined;
}
- public static async removeDashboard(dashboard: Doc) {
- const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
- if (dashboards && dashboards.length > 0) {
- Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
- }
- }
-
- public static createNewDashboard = async (userDoc: Doc, id?: string, name?: string) => {
- console.log(name)
- const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true);
- const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc;
+ public static createNewDashboard = (id?: string, name?: string) => {
+ const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true);
+ const dashboards = CurrentUserUtils.MyDashboards;
const dashboardCount = DocListCast(dashboards.data).length + 1;
const freeformOptions: DocumentOptions = {
x: 0,
@@ -1183,12 +1151,12 @@ export class CurrentUserUtils {
dashboardDoc.data = new List<Doc>(dashboardTabs);
dashboardDoc["pane-count"] = 1;
- userDoc.activePresentation = presentation;
+ CurrentUserUtils.ActivePresentation = presentation;
Doc.AddDocToList(dashboards, "data", dashboardDoc);
// open this new dashboard
- Doc.UserDoc().activeDashboard = dashboardDoc;
- Doc.UserDoc().activePage = "dashboard";
+ CurrentUserUtils.ActiveDashboard = dashboardDoc;
+ CurrentUserUtils.ActivePage = "dashboard";
}
public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) {
@@ -1222,6 +1190,7 @@ export class CurrentUserUtils {
public static get MyContextMenuBtns() { return DocCast(Doc.UserDoc().myContextMenuBtns); }
public static get MyRecentlyClosed() { return DocCast(Doc.UserDoc().myRecentlyClosed); }
public static get MyOverlayDocs() { return DocCast(Doc.UserDoc().myOverlayDocs); }
+ public static get MyPublishedDocs() { return DocCast(Doc.UserDoc().myPublishedDocs); }
public static get ActiveDashboard() { return DocCast(Doc.UserDoc().activeDashboard); }
public static set ActiveDashboard(val:Doc|undefined) { Doc.UserDoc().activeDashboard = val; }
public static get ActivePresentation() { return DocCast(Doc.UserDoc().activePresentation); }
@@ -1243,8 +1212,8 @@ ScriptingGlobals.add(function openDragFactory(dragFactory: Doc) {
ScriptingGlobals.add(function MySharedDocs() { return CurrentUserUtils.MySharedDocs; }, "document containing all shared Docs");
ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
-ScriptingGlobals.add(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); }, "creates a snapshot copy of a dashboard");
-ScriptingGlobals.add(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(Doc.UserDoc()); }, "creates a new dashboard when called");
+ScriptingGlobals.add(function snapshotDashboard() { CurrentUserUtils.snapshotDashboard(); }, "creates a snapshot copy of a dashboard");
+ScriptingGlobals.add(function createNewDashboard() { return CurrentUserUtils.createNewDashboard(); }, "creates a new dashboard when called");
ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called");
ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)");
@@ -1253,7 +1222,7 @@ ScriptingGlobals.add(function shareDashboard(dashboard: Doc) { SharingManager.In
ScriptingGlobals.add(async function removeDashboard(dashboard: Doc) {
const dashboards = await DocListCastAsync(CurrentUserUtils.MyDashboards.data);
if (dashboards && dashboards.length > 1) {
- if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboards.find(doc => doc !== dashboard)!);
+ if (dashboard === CurrentUserUtils.ActiveDashboard) CurrentUserUtils.openDashboard(dashboards.find(doc => doc !== dashboard)!);
Doc.RemoveDocFromList(CurrentUserUtils.MyDashboards, "data", dashboard);
}
},
@@ -1261,7 +1230,7 @@ ScriptingGlobals.add(async function removeDashboard(dashboard: Doc) {
ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
const dashboardAlias = Doc.MakeAlias(dashboard);
Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", dashboardAlias);
- CurrentUserUtils.openDashboard(Doc.UserDoc(), dashboardAlias);
+ CurrentUserUtils.openDashboard(dashboardAlias);
},
"adds Dashboard to set of Dashboards");
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index dbfba8992..09b463c2f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -6,7 +6,7 @@ import { PrefetchProxy } from "../../fields/Proxy";
import { listSpec } from "../../fields/Schema";
import { SchemaHeaderField } from "../../fields/SchemaHeaderField";
import { ScriptField } from "../../fields/ScriptField";
-import { Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../fields/Types";
import { emptyFunction, Utils } from "../../Utils";
import { Docs, DocUtils } from "../documents/Documents";
import * as globalCssVariables from "../views/global/globalCssVariables.scss";
@@ -72,6 +72,8 @@ export namespace DragManager {
export let StartWindowDrag: Opt<((e: { pageX: number, pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void)>;
export let CompleteWindowDrag: Opt<(aborted: boolean) => void>;
+ export function GetRaiseWhenDragged() { return BoolCast(Doc.UserDoc()._raiseWhenDragged); }
+ export function SetRaiseWhenDragged(val:boolean) { Doc.UserDoc()._raiseWhenDragged = val }
export function Root() {
const root = document.getElementById("root");
if (!root) {
@@ -221,10 +223,13 @@ export namespace DragManager {
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
docDragData.droppedDocuments =
- await Promise.all(dragData.draggedDocuments.map(async d => !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
- docDragData.dropAction === "alias" ? Doc.MakeAlias(d) :
- docDragData.dropAction === "proto" ? Doc.GetProto(d) :
- docDragData.dropAction === "copy" ? (await Doc.MakeClone(d)).clone : d));
+ await Promise.all(dragData.draggedDocuments.map(async d =>
+ !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ?
+ addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
+ docDragData.dropAction === "alias" ? Doc.MakeAlias(d) :
+ docDragData.dropAction === "proto" ? Doc.GetProto(d) :
+ docDragData.dropAction === "copy" ?
+ (await Doc.MakeClone(d)).clone : d));
!["same", "proto"].includes(docDragData.dropAction as any) && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => {
const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []);
const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps));
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 632348306..7dcff9c56 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -197,7 +197,7 @@ export namespace HistoryUtil {
await Promise.all(Object.keys(init).map(id => initDoc(id, init[id])));
}
if (field instanceof Doc) {
- CurrentUserUtils.openDashboard(Doc.UserDoc(), field, true);
+ CurrentUserUtils.openDashboard(field, true);
}
}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 382274462..080237649 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -10,7 +10,9 @@ import { GoogleAuthenticationManager } from "../apis/GoogleAuthenticationManager
import { DocServer } from "../DocServer";
import { Networking } from "../Network";
import { MainViewModal } from "../views/MainViewModal";
+import { FontIconBox } from "../views/nodes/button/FontIconBox";
import { CurrentUserUtils } from "./CurrentUserUtils";
+import { DragManager } from "./DragManager";
import { GroupManager } from "./GroupManager";
import "./SettingsManager.scss";
import { undoBatch } from "./UndoManager";
@@ -65,12 +67,11 @@ export class SettingsManager extends React.Component<{}> {
@undoBatch changeFontSize = action((e: React.ChangeEvent) => Doc.UserDoc().fontSize = (e.currentTarget as any).value);
@undoBatch switchActiveBackgroundColor = action((color: ColorState) => Doc.UserDoc().activeCollectionBackground = String(color.hex));
@undoBatch switchUserColor = action((color: ColorState) => { Doc.SharingDoc().userColor = undefined; Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex); });
- @undoBatch
- playgroundModeToggle = action(() => {
+ @undoBatch playgroundModeToggle = action(() => {
this.playgroundMode = !this.playgroundMode;
if (this.playgroundMode) {
DocServer.Control.makeReadOnly();
- addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "pink !important" });
+ addStyleSheetRule(SettingsManager._settingsStyle, "topbar-inner-container", { background: "red !important" });
}
else DocServer.Control.makeEditable();
});
@@ -155,19 +156,14 @@ export class SettingsManager extends React.Component<{}> {
<div className="preferences-check">Show full toolbar</div>
</div>
<div>
- <input type="checkbox" onChange={e => Doc.UserDoc()._raiseWhenDragged = !Doc.UserDoc()._raiseWhenDragged}
- checked={BoolCast(Doc.UserDoc()._raiseWhenDragged)} />
+ <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())}
+ checked={DragManager.GetRaiseWhenDragged()} />
<div className="preferences-check">Raise on drag</div>
</div>
<div>
- <input type="checkbox" onChange={e => Doc.UserDoc()._showLabel = !Doc.UserDoc()._showLabel}
- checked={BoolCast(Doc.UserDoc()._showLabel)} />
- <div className="preferences-check">Show tool button labels</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => Doc.UserDoc()._showMenuLabel = !Doc.UserDoc()._showMenuLabel}
- checked={BoolCast(Doc.UserDoc()._showMenuLabel)} />
- <div className="preferences-check">Show menu button labels</div>
+ <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())}
+ checked={FontIconBox.GetShowLabels()} />
+ <div className="preferences-check">Show button labels</div>
</div>
</div>;
}
@@ -271,7 +267,7 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
<button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
<div className="default-acl">
- <input className="acl-check" type="checkbox" checked={BoolCast(Doc.UserDoc()?.defaultAclPrivate)}
+ <input className="acl-check" type="checkbox" checked={BoolCast(Doc.defaultAclPrivate)}
onChange={action(() => Doc.defaultAclPrivate = !Doc.defaultAclPrivate)} />
<div className="acl-text">Default access private</div>
</div>
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 069f81d38..057843c68 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -1,5 +1,6 @@
import { observable, action, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
+import { Doc } from "../../fields/Doc";
export namespace SnappingManager {
@@ -30,6 +31,9 @@ export namespace SnappingManager {
export function SetIsDragging(dragging: boolean) { runInAction(() => manager.IsDragging = dragging); }
export function GetIsDragging() { return manager.IsDragging; }
+ export function SetShowSnapLines(show: boolean) { runInAction(() => Doc.UserDoc().showSnapLines = show); }
+ export function GetShowSnapLines() { return Doc.UserDoc().showSnapLines; }
+
/// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts
// need to investigate further what caused the mobx update problems and move to a better location.
const getCachedGroupByNameCache = computedFn(function (name: string) { return manager.cachedGroups.includes(name); }, true);
diff --git a/src/client/views/AudioWaveform.scss b/src/client/views/AudioWaveform.scss
index e20434a25..6cbd1759a 100644
--- a/src/client/views/AudioWaveform.scss
+++ b/src/client/views/AudioWaveform.scss
@@ -1,7 +1,7 @@
.audioWaveform {
position: relative;
width: 100%;
- height: 100%;
+ height: 200%;
overflow: hidden;
z-index: -1000;
bottom: 0;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index e2f98de1e..cffcd0f17 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,10 +1,10 @@
import React = require("react");
-import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
-import { observable, action, computed, runInAction, IReactionDisposer, reaction } from "mobx";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, IReactionDisposer, observable } from "mobx";
import { observer } from "mobx-react";
import "./ContextMenu.scss";
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import Measure from "react-measure";
+import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from "./ContextMenuItem";
+import { Utils } from "../../Utils";
@observer
export class ContextMenu extends React.Component {
@@ -74,7 +74,6 @@ export class ContextMenu extends React.Component {
componentDidMount = () => {
document.addEventListener("pointerdown", this.onPointerDown);
document.addEventListener("pointerup", this.onPointerUp);
-
}
@action
@@ -116,10 +115,6 @@ export class ContextMenu extends React.Component {
this._defaultItem = item;
}
- getItems() {
- return this._items;
- }
-
static readonly buffer = 20;
get pageX() {
const x = this._pageX;
@@ -199,37 +194,25 @@ export class ContextMenu extends React.Component {
return eles;
};
- return flattenItems(this._items, name => [name]);
+ return flattenItems(this._items.slice(), name => [name]);
}
@computed get flatItems(): OriginalMenuProps[] {
return this.filteredItems.filter(item => !Array.isArray(item)) as OriginalMenuProps[];
}
- @computed get filteredViews() {
- const createGroupHeader = (contents: any) => {
- return (
- <div className="contextMenu-group">
- <div className="contextMenu-description">{contents}</div>
- </div>
- );
- };
- const createItem = (item: ContextMenuProps, selected: boolean) => <ContextMenuItem {...item} key={item.description} closeMenu={this.closeMenu} selected={selected} />;
- let itemIndex = 0;
- return this.filteredItems.map(value => {
- if (Array.isArray(value)) {
- return createGroupHeader(value.join(" -> "));
- } else {
- return createItem(value, itemIndex++ === this.selectedIndex);
- }
- });
- }
-
@computed get menuItems() {
if (!this._searchString) {
return this._items.map((item, ind) => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={ind + item.description} closeMenu={this.closeMenu} />);
}
- return this.filteredViews;
+ return this.filteredItems.map((value, index) =>
+ Array.isArray(value) ?
+ <div className="contextMenu-group">
+ <div className="contextMenu-description">{value.join(" -> ")}</div>
+ </div>
+ :
+ <ContextMenuItem {...value} key={index+value.description} closeMenu={this.closeMenu} selected={index === this.selectedIndex} />
+ );
}
@computed get itemsNeedSearch() {
@@ -237,14 +220,8 @@ export class ContextMenu extends React.Component {
}
render() {
- if (!this._display) {
- return null;
- }
- const style = this._yRelativeToTop ? { left: this.pageX, top: this.pageY } :
- { left: this.pageX, bottom: this.pageY };
-
- const contents = (
- <>
+ return !this._display ? (null) :
+ <div className="contextMenu-cont" style={{left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY })}}>
{!this.itemsNeedSearch ? (null) :
<span className={"search-icon"}>
<span className="icon-background">
@@ -253,17 +230,7 @@ export class ContextMenu extends React.Component {
<input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>}
{this.menuItems}
- </>
- );
- return (
- <Measure offset onResize={action((r: any) => { this._width = r.offset.width; this._height = r.offset.height; })}>
- {({ measureRef }) => (
- <div className="contextMenu-cont" style={style} ref={measureRef}>
- {contents}
- </div>
- )}
- </Measure>
- );
+ </div>;
}
@action
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 25d00f701..30073e21f 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,5 +1,5 @@
import React = require("react");
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
import { observer } from "mobx-react";
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -28,12 +28,11 @@ export type ContextMenuProps = OriginalMenuProps | SubmenuProps;
export class ContextMenuItem extends React.Component<ContextMenuProps & { selected?: boolean }> {
@observable private _items: Array<ContextMenuProps> = [];
@observable private overItem = false;
- @observable private subRef = React.createRef<HTMLDivElement>();
- constructor(props: ContextMenuProps | SubmenuProps) {
- super(props);
- if ((this.props as SubmenuProps).subitems) {
- (this.props as SubmenuProps).subitems?.forEach(i => this._items.push(i));
+ componentDidMount() {
+ this._items.length = 0;
+ if ((this.props as SubmenuProps)?.subitems) {
+ (this.props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i)));
}
}
@@ -78,9 +77,6 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
}
render() {
-
-
-
if ("event" in this.props) {
return (
<div className={"contextMenu-item" + (this.props.selected ? " contextMenu-itemSelected" : "")} onPointerDown={this.handleEvent}>
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index ebe73ffea..868d63a90 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -6,7 +6,7 @@ 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 { undoBatch, UndoManager } from "../util/UndoManager";
import "./DashboardView.scss"
import { MainViewModal } from "./MainViewModal";
import { ContextMenu } from "./ContextMenu";
@@ -67,10 +67,9 @@ export class DashboardView extends React.Component {
return sharedDashs.filter((dashboard) => !DocListCast(CurrentUserUtils.MySharedDocs.viewed).includes(dashboard))
}
- createNewDashboard = async (name: string) => {
- const batch = UndoManager.StartBatch("new dash");
- await CurrentUserUtils.createNewDashboard(Doc.UserDoc(), undefined, name);
- batch.end();
+ @undoBatch
+ createNewDashboard = async (name: string) => {
+ CurrentUserUtils.createNewDashboard(undefined, name);
this.abortCreateNewDashboard();
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 18cf785b9..669718e81 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -87,10 +87,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
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);
+ Doc.RemoveDocFromList(CurrentUserUtils.MyPublishedDocs, undefined, d.rootDoc);
}
if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) {
- Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc);
+ Doc.AddDocToList(CurrentUserUtils.MyPublishedDocs, undefined, d.rootDoc);
}
}
//@ts-ignore
@@ -311,7 +311,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
move[1] = thisPt.y - this._snapY;
this._snapX = thisPt.x;
this._snapY = thisPt.y;
- let dragBottom = false, dragRight = false, dragBotRight = false;
+ let dragBottom = false, dragRight = false, dragBotRight = false, dragTop = false;
let dX = 0, dY = 0, dW = 0, dH = 0;
switch (this._resizeHdlId.split(" ")[0]) {
case "": break;
@@ -329,7 +329,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
case "documentDecorations-topResizer":
dY = -1;
dH = -move[1];
- dragBottom = true;
+ dragTop = true;
break;
case "documentDecorations-bottomLeftResizer":
dX = -1;
@@ -361,27 +361,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const doc = Document(docView.rootDoc);
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
- const docheight = doc._height || 0;
- const docwidth = doc._width || 0;
+ let docheight = doc._height || 0;
+ let 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;
+ const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen);
if (nwidth && nheight) {
- if (nwidth / nheight !== width / height && !dragBottom) {
+ if (nwidth / nheight !== width / height && !dragBottom && !dragTop) {
height = nheight / nwidth * width;
}
- if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
+ if (modifyNativeDim && !dragBottom && !dragTop) { // 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);
+ const fixedAspect = (nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen));
+ console.log(fixedAspect);
if (fixedAspect) {
- if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
+ if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop)|| !modifyNativeDim)) || dragRight) {
if (dragRight && modifyNativeDim) {
doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
} else {
@@ -394,7 +395,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
doc._width = actualdW;
}
else {
- if (dragBottom && (modifyNativeDim ||
+ if ((dragBottom|| dragTop) && (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;
@@ -417,7 +418,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
dH && (doc._autoHeight = false);
}
doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
- doc.y = (doc.y || 0) + dY * (actualdH - docheight);
+ doc.y = (doc.y || 0) + (dragBottom ? 0: dY * (actualdH - docheight));
doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 895687f2c..4940c5f9d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -8,9 +8,8 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { PrefetchProxy } from '../../fields/Proxy';
import { ScriptField } from '../../fields/ScriptField';
-import { DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { PromiseValue, StrCast } from '../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
@@ -32,8 +31,7 @@ import { CollectionDockingView } from './collections/CollectionDockingView';
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
import { CollectionLinearView } from './collections/collectionLinear';
import { CollectionMenu } from './collections/CollectionMenu';
-import { TreeViewType } from './collections/CollectionTreeView';
-import { CollectionView, CollectionViewType } from './collections/CollectionView';
+import { CollectionViewType } from './collections/CollectionView';
import "./collections/TreeView.scss";
import { ComponentDecorations } from './ComponentDecorations';
import { ContextMenu } from './ContextMenu';
@@ -49,7 +47,6 @@ import { LightboxView } from './LightboxView';
import { LinkMenu } from './linking/LinkMenu';
import "./MainView.scss";
import { AudioBox } from './nodes/AudioBox';
-import { ButtonType } from './nodes/button/FontIconBox';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView } from './nodes/DocumentView';
import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
@@ -191,7 +188,7 @@ export class MainView extends React.Component {
fa.faArrowAltCircleDown, fa.faArrowAltCircleUp, fa.faArrowAltCircleLeft, fa.faArrowAltCircleRight, fa.faStopCircle, fa.faCheckCircle, fa.faGripVertical,
fa.faSortUp, fa.faSortDown, fa.faTable, fa.faTh, fa.faThList, fa.faProjectDiagram, fa.faSignature, fa.faColumns, fa.faChevronCircleUp, fa.faUpload, fa.faBorderAll,
fa.faBraille, fa.faChalkboard, fa.faPencilAlt, fa.faEyeSlash, fa.faSmile, fa.faIndent, fa.faOutdent, fa.faChartBar, fa.faBan, fa.faPhoneSlash, fa.faGripLines,
- fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt, fa.faSearchPlus, fa.faVolumeUp, fa.faVolumeDown, fa.faSquareRootAlt]);
+ fa.faSave, fa.faBookmark, fa.faList, fa.faListOl, fa.faFolderPlus, fa.faLightbulb, fa.faBookOpen, fa.faMapMarkerAlt, fa.faSearchPlus, fa.faVolumeUp, fa.faVolumeDown, fa.faSquareRootAlt, fa.faVolumeMute]);
this.initAuthenticationRouters();
}
@@ -224,21 +221,22 @@ export class MainView extends React.Component {
initAuthenticationRouters = async () => {
const received = CurrentUserUtils.MainDocId;
if (received && !this.userDoc) {
- reaction(() => CurrentUserUtils.GuestTarget, target => target);
+ reaction(() => CurrentUserUtils.GuestTarget, target => target && CurrentUserUtils.createNewDashboard(), { fireImmediately: true });
}
+ // else {
+ // PromiseValue(this.userDoc.activeDashboard).then(dash => {
+ // if (dash instanceof Doc) CurrentUserUtils.openDashboard(dash);
+ // else CurrentUserUtils.createNewDashboard();
+ // });
+ // }
}
@action
createNewPresentation = async () => {
- if (!await this.userDoc.myTrails) {
- this.userDoc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], {
- title: "TRAILS", childDontRegisterViews: true, _height: 100, _forceActive: true, boxShadow: "0 0", _lockedPosition: true, treeViewOpen: true, system: true
- }));
- }
const pres = Docs.Create.PresDocument({ title: "Untitled Trail", _viewType: CollectionViewType.Stacking, _fitWidth: true, _width: 400, _height: 500, targetDropAction: "alias", _chromeHidden: true, boxShadow: "0 0" });
CollectionDockingView.AddSplit(pres, "left");
- this.userDoc.activePresentation = pres;
- Doc.AddDocToList(this.userDoc.myTrails as Doc, "data", pres);
+ CurrentUserUtils.ActivePresentation = pres;
+ Doc.AddDocToList(CurrentUserUtils.MyTrails, "data", pres);
}
@action
@@ -347,9 +345,9 @@ export class MainView extends React.Component {
addDocTabFunc = (doc: Doc, location: string): boolean => {
const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":");
const locationParams = locationFields.length > 1 ? locationFields[1] : "";
- if (doc.dockingConfig) return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc);
+ if (doc.dockingConfig) return CurrentUserUtils.openDashboard(doc);
switch (locationFields[0]) {
- case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc);
+ case "dashboard": return CurrentUserUtils.openDashboard(doc);
case "close": return CollectionDockingView.CloseSplit(doc, locationParams);
case "fullScreen": return CollectionDockingView.OpenFullScreen(doc);
case "lightbox": return LightboxView.AddDocTab(doc, location);
@@ -520,7 +518,7 @@ export class MainView extends React.Component {
}
@computed get docButtons() {
- return !(this.userDoc.dockedBtns instanceof Doc) ? (null) :
+ return !CurrentUserUtils.MyDockedBtns ? (null) :
<div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !CurrentUserUtils.MyDockedBtns.linearViewIsExpanded ? "42px" : undefined }} >
<CollectionLinearView
Document={CurrentUserUtils.MyDockedBtns}
@@ -557,7 +555,7 @@ export class MainView extends React.Component {
</div>;
}
@computed get snapLines() {
- return !this.userDoc.showSnapLines ? (null) : <div className="mainView-snapLines">
+ return !SnappingManager.GetShowSnapLines() ? (null) : <div className="mainView-snapLines">
<svg style={{ width: "100%", height: "100%" }}>
{SnappingManager.horizSnapLines().map(l => <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
{SnappingManager.vertSnapLines().map(l => <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={"1 1"} />)}
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 302e7a5e3..033cdf1f7 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -15,6 +15,7 @@
flex-direction: column;
top: 0;
left: 0;
+ pointer-events: all;
}
.overlayWindow-outerDiv,
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 90c86fa18..faab2ed26 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -304,7 +304,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
rootSelected={returnFalse}
styleProvider={DefaultStyleProvider}
docViewPath={returnEmptyDoclist}
- freezeDimensions={true}
dontCenter={"y"}
isDocumentActive={returnFalse}
isContentActive={emptyFunction}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 5f36a7a51..07fcd6a7d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -121,7 +121,7 @@ export class CollectionDockingView extends CollectionSubView() {
SelectionManager.DeselectAll();
const instance = CollectionDockingView.Instance;
if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === "layout") {
- return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc);
+ return CurrentUserUtils.openDashboard(doc);
}
const newItemStackConfig = {
type: 'stack',
@@ -172,7 +172,7 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
@action
public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) {
- if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(Doc.UserDoc(), document);
+ if (document._viewType === CollectionViewType.Docking) return CurrentUserUtils.openDashboard(document);
const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
if (tab) {
@@ -416,8 +416,10 @@ export class CollectionDockingView extends CollectionSubView() {
}
tabDestroyed = (tab: any) => {
- Doc.AddDocToList(CurrentUserUtils.MyHeaderBar, "data", tab.DashDoc);
- Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true);
+ if(tab.DashDoc?.type !== DocumentType.KVP) {
+ Doc.AddDocToList(CurrentUserUtils.MyHeaderBar, "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);
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 9b1bb5b97..668d82387 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -164,7 +164,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps>{
// </button>
// </Tooltip>;
- // OLD BUTTONS
+ // //OLD BUTTONS
// return this.getElement(!this.SelectedCollection ? [/*button*/] :
// [<CollectionViewBaseChrome key="chrome"
// docView={this.SelectedCollection}
@@ -306,7 +306,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@undoBatch
viewChanged = (e: React.ChangeEvent) => {
- const target = this.document !== Doc.UserDoc().sidebar ? this.document : this.document.proto as Doc;
+ const target = this.document !== CurrentUserUtils.MyLeftSidebarPanel ? this.document : this.document.proto as Doc;
//@ts-ignore
target._viewType = e.target.selectedOptions[0].value;
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index e8b6817b4..bb98e1c99 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -6,8 +6,18 @@
overflow-y: hidden;
border: none;
background-color: $white;
- border: 2px solid $dark-gray;
border-width: 0 2px 0 2px;
+
+ &:hover {
+ .collectionStackedTimeline-hover {
+ display: block;
+ }
+ }
+}
+
+.timeline-container:hover + .timeline-hoverUI {
+ display: flex;
+ justify-content: center;
}
::-webkit-scrollbar {
@@ -19,6 +29,7 @@
background: $off-white;
z-index: 1000;
height: 100%;
+ overflow: hidden;
.collectionStackedTimeline-trim-shade {
position: absolute;
@@ -61,15 +72,23 @@
border-width: 1px;
}
- .collectionStackedTimeline-current {
+ .collectionStackedTimeline-current, .collectionStackedTimeline-hover {
width: 1px;
height: 100%;
- background-color: $pink;
position: absolute;
top: 0px;
pointer-events: none;
}
+ .collectionStackedTimeline-current {
+ background-color: $pink;
+ }
+
+ .collectionStackedTimeline-hover {
+ display: none;
+ background-color: $medium-blue;
+ }
+
.collectionStackedTimeline-marker-timeline {
position: absolute;
top: 2.5%;
@@ -108,3 +127,19 @@
pointer-events: none;
}
}
+
+.timeline-hoverUI {
+ position: absolute;
+ z-index: 10000;
+ transform: translate(-50%, 100%);
+ height: 100%;
+ display: none;
+
+ .hoverTime {
+ position: absolute;
+ color: $dark-gray;
+ transform: translate(0, -100%);
+
+ font-weight: bold;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index ebdea9aaf..3e85edac8 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -43,6 +43,8 @@ import {
} from "../nodes/DocumentView";
import { LabelBox } from "../nodes/LabelBox";
import "./CollectionStackedTimeline.scss";
+import { VideoBox } from "../nodes/VideoBox";
+import { ImageField } from "../../../fields/URLField";
export type CollectionStackedTimelineProps = {
Play: () => void;
@@ -86,9 +88,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@observable _trimEnd: number = 0; // trim controls end pos
@observable _zoomFactor: number = 1;
-
@observable _scroll: number = 0;
+ @observable _hoverTime: number = 0;
+
+ @observable _thumbnail: string | undefined;
+
// ensures that clip doesn't get trimmed so small that controls cannot be adjusted anymore
get minTrimLength() { return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5); }
@@ -178,7 +183,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@action
keyEvents = (e: KeyboardEvent) => {
if (
- !(e.target instanceof HTMLInputElement) &&
+ // need to include range inputs because after dragging video time slider it becomes target element
+ !(e.target instanceof HTMLInputElement && !(e.target.type === "range")) &&
this.props.isSelected(true)
) {
// if shift pressed scrub 1 second otherwise 1/10th
@@ -315,11 +321,28 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
}
+ @action
+ onHover = (e: React.MouseEvent): void => {
+ e.stopPropagation();
+ const rect = this._timeline?.getBoundingClientRect();
+ const clientX = e.clientX;
+ if (rect) {
+ this._hoverTime = this.toTimeline(clientX - rect.x, rect.width);
+ if (this.dataDoc.thumbnails) {
+ const nearest = Math.floor(this._hoverTime / this.props.rawDuration * VideoBox.numThumbnails);
+ const thumbnails = Cast(this.dataDoc.thumbnails, listSpec("string"), []);
+ const imgField = thumbnails && thumbnails.length > 0 ? new ImageField(thumbnails[nearest]) : new ImageField("");
+ const src = imgField && imgField.url.href ? imgField.url.href.replace(".png", "_s.png") : "";
+ this._thumbnail = src ? src : undefined;
+ }
+ }
+ }
+
+
// for dragging trim start handle
@action
trimLeft = (e: React.PointerEvent): void => {
const rect = this._timeline?.getBoundingClientRect();
- const clientX = e.movementX;
setupMoveUpEvents(
this,
e,
@@ -346,7 +369,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@action
trimRight = (e: React.PointerEvent): void => {
const rect = this._timeline?.getBoundingClientRect();
- const clientX = e.movementX;
setupMoveUpEvents(
this,
e,
@@ -436,7 +458,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
if (anchorStartTime === undefined) return rootDoc;
const anchor = docAnchor ?? Docs.Create.LabelDocument({
title: ComputedField.MakeFunction(
- `"#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"])`
+ `self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`
) as any,
_minFontSize: 12,
_maxFontSize: 24,
@@ -556,7 +578,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
dictationHeight = () => (this.props.PanelHeight() * (100 - this.dictationHeightPercent)) / 100;
@computed get timelineContentHeight() { return this.props.PanelHeight() * this.dictationHeightPercent / 100; }
- @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor - 4; } // subtract size of container border
+ @computed get timelineContentWidth() { return this.props.PanelWidth() * this.zoomFactor; } // subtract size of container border
dictationScreenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.timelineContentHeight);
@@ -632,6 +654,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
style={{ width: this.props.PanelWidth() }}
onWheel={e => e.stopPropagation()}
onScroll={this.setScroll}
+ onMouseMove={(e) => this.isContentActive() && this.onHover(e)}
ref={wrapper => this._timelineWrapper = wrapper}>
<div
className="collectionStackedTimeline"
@@ -702,6 +725,13 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
/>
{/* {this.renderDictation} */}
+ { /* check time to prevent weird div overflow */ this._hoverTime < this.clipDuration && <div
+ className="collectionStackedTimeline-hover"
+ style={{
+ left: `${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%`,
+ }}
+ />}
+
<div
className="collectionStackedTimeline-current"
style={{
@@ -744,6 +774,10 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
)}
</div>
</div>
+ <div className="timeline-hoverUI" style={{ left: `calc(${((this._hoverTime - this.clipStart) / this.clipDuration) * 100}%` }}>
+ <div className="hoverTime">{formatTime(this._hoverTime)}</div>
+ {this._thumbnail && <img className="videoBox-thumbnail" src={this._thumbnail} />}
+ </div>
</div >);
}
}
@@ -799,7 +833,6 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
return `#${formatTime(start)}-${formatTime(end)}`;
}
-
componentDidMount() {
this._disposer = reaction(
() => this.props.currentTimecode(),
@@ -890,7 +923,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
// context menu
contextMenuItems = () => {
- const resetTitle = { script: ScriptField.MakeFunction(`self.title = "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"])`)!, icon: "folder-plus", label: "Reset Title" };
+ const resetTitle = { script: ScriptField.MakeFunction(`self.title = self["${this.props.endTag}"] ? "#" + formatToTime(self["${this.props.startTag}"]) + "-" + formatToTime(self["${this.props.endTag}"]) : "#" + formatToTime(self["${this.props.startTag}"])`)!, icon: "folder-plus", label: "Reset Title" };
return [resetTitle];
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 277fcd59c..4e8c14039 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -229,6 +229,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
}
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive));
getDisplayDoc(doc: Doc, width: () => number) {
const dataDoc = (!doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS) ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
@@ -245,12 +246,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
styleProvider={this.styleProvider}
docViewPath={this.props.docViewPath}
fitWidth={this.props.childFitWidth}
- isContentActive={emptyFunction}
+ isContentActive={this.isChildContentActive}
onKey={this.onKeyDown}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
- freezeDimensions={this.props.childFreezeDimensions}
NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeWidth(doc) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || doc._fitWidth && !Doc.NativeHeight(doc) ? height : undefined}
dontCenter={this.props.childIgnoreNativeSize ? "xy" : undefined}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 17fdba764..03450b798 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -274,7 +274,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (docid) { // prosemirror text containing link to dash document
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ if (options.x || options.y) { f.x = options.x as number; f.y = options.y as number; } // should be in CollectionFreeFormView
(f instanceof Doc) && addDocument(f);
}
});
@@ -311,7 +311,7 @@ export function CollectionSubView<X>(moreProps?: X) {
const docid = text.replace(Doc.globalServerPath(), "").split("?")[0];
DocServer.GetRefField(docid).then(f => {
if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ if (options.x || options.y) { f.x = options.x as number; f.y = options.y as number; } // should be in CollectionFreeFormView
(f instanceof Doc) && addDocument(f);
}
});
@@ -445,7 +445,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (completed) completed(set);
else {
if (isFreeformView && generatedDocuments.length > 1) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!,);
+ addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!,);
} else {
generatedDocuments.forEach(addDocument);
}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 7573b938a..3dd9d2d84 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -59,7 +59,7 @@ export class CollectionTimeView extends CollectionSubView() {
//const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
runInAction(() => {
- this._childClickedScript = ScriptField.MakeScript("openInLightbox(self, shiftKey)", { this: Doc.name, shiftKey: "boolean" });//, { detailView: detailView! });
+ this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name });
this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
});
}
@@ -138,8 +138,7 @@ export class CollectionTimeView extends CollectionSubView() {
fitContentsToBox={returnTrue}
childClickScript={this._childClickedScript}
viewDefDivClick={this._viewDefDivClick}
- childFreezeDimensions={true}
- dontScaleFilter={this.dontScaleFilter}
+ //dontScaleFilter={this.dontScaleFilter}
layoutEngine={this.layoutEngine} />
</div>;
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4b5c5e3fb..b432104a1 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -75,7 +75,6 @@ export interface CollectionViewProps extends FieldViewProps {
childHideResizeHandles?: () => boolean;
childLayoutTemplate?: () => (Doc | undefined);// specify a layout Doc template to use for children of the collection
childLayoutString?: string;
- childFreezeDimensions?: boolean; // used by TimeView to coerce documents to treat their width height as their native width/height
childIgnoreNativeSize?: boolean;
childClickScript?: ScriptField;
childDoubleClickScript?: ScriptField;
@@ -205,7 +204,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
});
}
if (this.Document._viewType === CollectionViewType.Docking) {
- optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(Doc.UserDoc()), icon: "project-diagram" });
+ optionItems.push({ description: "Create Dashboard", event: () => CurrentUserUtils.createNewDashboard(), icon: "project-diagram" });
}
!options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "hand-point-right" });
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 70db121d1..62d07b0e4 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -329,7 +329,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":");
const locationParams = locationFields.length > 1 ? locationFields[1] : "";
switch (locationFields[0]) {
- case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc);
+ case "dashboard": return CurrentUserUtils.openDashboard(doc);
case "close": return CollectionDockingView.CloseSplit(doc, locationParams);
case "fullScreen": return CollectionDockingView.OpenFullScreen(doc);
case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ffe146ae4..3c2047db7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -253,7 +253,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];
layoutDoc._width = NumCast(layoutDoc._width, 300);
layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300);
- (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront
+ (d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged(): d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront
}
(docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments);
@@ -1265,7 +1265,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
styleProvider={this.getClusterColor}
dataProvider={this.childDataProvider}
sizeProvider={this.childSizeProvider}
- freezeDimensions={BoolCast(this.props.Document.childFreezeDimensions, this.props.childFreezeDimensions)}
dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
bringToFront={this.bringToFront}
showTitle={this.props.childShowTitle}
@@ -1645,7 +1644,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const viewctrls = ContextMenu.Instance.findByDescription("UI Controls...");
const viewCtrlItems = viewctrls && "subitems" in viewctrls ? viewctrls.subitems : [];
- !Doc.noviceMode ? viewCtrlItems.push({ description: (Doc.UserDoc().showSnapLines ? "Hide" : "Show") + " Snap Lines", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" }) : null;
+ !Doc.noviceMode ? viewCtrlItems.push({ description: (SnappingManager.GetShowSnapLines() ? "Hide" : "Show") + " Snap Lines", event: () => SnappingManager.SetShowSnapLines(!SnappingManager.GetShowSnapLines()), icon: "compress-arrows-alt" }) : null;
!Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? "Hide" : "Show") + " Clusters", event: () => this.updateClusters(!this.Document._useClusters), icon: "braille" }) : null;
!viewctrls && ContextMenu.Instance.addItem({ description: "UI Controls...", subitems: viewCtrlItems, icon: "eye" });
@@ -1661,8 +1660,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const mores = ContextMenu.Instance.findByDescription("More...");
const moreItems = mores && "subitems" in mores ? mores.subitems : [];
if (!Doc.noviceMode) {
+ e.persist();
moreItems.push({ description: "Export collection", icon: "download", event: async () => Doc.Zip(this.props.Document) });
- moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(x, y) });
+ moreItems.push({ description: "Import exported collection", icon: "upload", event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
}
!mores && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "eye" });
}
@@ -1671,28 +1671,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const input = document.createElement("input");
input.type = "file";
input.accept = ".zip";
- input.onchange = async _e => {
- const upload = Utils.prepend("/uploadDoc");
- const formData = new FormData();
- const file = input.files && input.files[0];
- if (file) {
- formData.append('file', file);
- formData.append('remap', "true");
- const response = await fetch(upload, { method: "POST", body: formData });
- const json = await response.json();
- if (json !== "error") {
- const doc = await DocServer.GetRefField(json);
- if (doc instanceof Doc) {
- const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
- doc.x = xx, doc.y = yy;
- this.props.addDocument?.(doc);
- setTimeout(() =>
- SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs => {
- docs.docs.forEach(d => LinkManager.Instance.addLink(d));
- }), 2000); // need to give solr some time to update so that this query will find any link docs we've added.
- }
- }
- }
+ input.onchange = _e => {
+ input.files && Doc.importDocument(input.files[0]).then(doc => {
+ if (doc instanceof Doc) {
+ const [xx, yy] = this.getTransform().transformPoint(x, y);
+ doc.x = xx, doc.y = yy;
+ this.props.addDocument?.(doc);}
+ });
};
input.click();
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b62020a04..081a1a924 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { RichTextField } from "../../../../fields/RichTextField";
import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../fields/Types";
+import { Cast, DocCast, FieldValue, NumCast, StrCast } from "../../../../fields/Types";
import { ImageField } from "../../../../fields/URLField";
import { GetEffectiveAcl } from "../../../../fields/util";
import { intersectRect, returnFalse, Utils } from "../../../../Utils";
@@ -156,7 +156,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}));
} else if (e.key === "s" && e.ctrlKey) {
e.preventDefault();
- const slide = Doc.copyDragFactory(Doc.UserDoc().emptySlide as Doc)!;
+ const slide = Doc.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
slide.x = x;
slide.y = y;
FormattedTextBox.SelectOnLoad = slide[Id];
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index da102fe18..4e4c33446 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -162,7 +162,6 @@ export class CollectionGridView extends CollectionSubView() {
DataDoc={layout.resolvedDataDoc as Doc}
PanelWidth={width}
PanelHeight={height}
- freezeDimensions={true}
ScreenToLocalTransform={dxf}
onClick={this.onChildClickHandler}
renderDepth={this.props.renderDepth + 1}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index b7ba94940..777ef464f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,6 +6,7 @@ import { List } from '../../../../fields/List';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { DocumentView } from '../../nodes/DocumentView';
@@ -242,7 +243,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
return this.props.addDocTab(doc, where);
}
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- isChildContentActive = () => this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
+ isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
return <DocumentView
Document={layout}
@@ -251,13 +252,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
docViewPath={this.props.docViewPath}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
- freezeDimensions={this.props.childFreezeDimensions}
renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isChildContentActive}
- isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
- hideResizeHandles={this.props.childHideResizeHandles?.()}
- hideDecorationTitle={this.props.childHideDecorationTitle?.()}
- fitContentsToBox={this.props.fitContentsToBox}
PanelWidth={width}
PanelHeight={height}
rootSelected={this.rootSelected}
@@ -266,6 +261,11 @@ export class CollectionMulticolumnView extends CollectionSubView() {
onDoubleClick={this.onChildDoubleClickHandler}
suppressSetHeight={true}
ScreenToLocalTransform={dxf}
+ isContentActive={this.isChildContentActive}
+ isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+ hideResizeHandles={this.props.childHideResizeHandles?.()}
+ hideDecorationTitle={this.props.childHideDecorationTitle?.()}
+ fitContentsToBox={this.props.fitContentsToBox}
focus={this.props.focus}
docFilters={this.childDocFilters}
docRangeFilters={this.childDocRangeFilters}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 338639a83..08385bcb5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,6 +6,7 @@ import { List } from '../../../../fields/List';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { DocumentView } from '../../nodes/DocumentView';
@@ -242,7 +243,7 @@ export class CollectionMultirowView extends CollectionSubView() {
return this.props.addDocTab(doc, where);
}
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- isChildContentActive = () => this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
+ isChildContentActive = () => ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
return <DocumentView
Document={layout}
@@ -251,7 +252,6 @@ export class CollectionMultirowView extends CollectionSubView() {
docViewPath={this.props.docViewPath}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
- freezeDimensions={this.props.childFreezeDimensions}
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
@@ -261,7 +261,7 @@ export class CollectionMultirowView extends CollectionSubView() {
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
isContentActive={this.isChildContentActive}
- isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
+ isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
fitContentsToBox={this.props.fitContentsToBox}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index f45068b6a..9eba788a9 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -403,7 +403,6 @@ export class CollectionSchemaView extends CollectionSubView() {
Document={this.previewDocument}
DataDoc={undefined}
fitContentsToBox={returnTrue}
- freezeDimensions={true}
dontCenter={"y"}
focus={DocUtils.DefaultFocus}
renderDepth={this.props.renderDepth}
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bea5b3be6..43266a571 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -574,7 +574,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
DataDoc={this._showDataDoc}
styleProvider={DefaultStyleProvider}
docViewPath={returnEmptyDoclist}
- freezeDimensions={true}
focus={DocUtils.DefaultFocus}
renderDepth={this.props.renderDepth}
rootSelected={returnFalse}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8e24fce11..c42c2306a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -63,7 +63,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_recorder: any; // MediaRecorder
_recordStart = 0;
_pauseStart = 0; // time when recording is paused (used to keep track of recording timecodes)
- _pauseEnd = 0;
_pausedTime = 0;
_stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
_play: any = null; // timeout for playback
@@ -81,7 +80,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get miniPlayer() { return this.props.PanelHeight() < 50; } // used to collapse timeline when node is shrunk
@computed get links() { return DocListCast(this.dataDoc.links); }
- @computed get pauseTime() { return this._pauseEnd - this._pauseStart; } // total time paused to update the correct recording time
@computed get mediaState() { return this.dataDoc.mediaState as media_state; }
@computed get path() { // returns the path of the audio file
const path = Cast(this.props.Document[this.fieldKey], AudioField, null)?.url.href || "";
@@ -97,9 +95,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._dropDisposer?.();
Object.values(this._disposers).forEach((disposer) => disposer?.());
- // removes doc from active recordings if recording when closed
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && DocUtils.ActiveRecordings.splice(ind, 1);
+ this.mediaState === media_state.Recording && this.stopRecording();
}
@action
@@ -220,10 +216,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
updateRecordTime = () => {
if (this.mediaState === media_state.Recording) {
setTimeout(this.updateRecordTime, 30);
- if (this._paused) {
- this._pausedTime += (new Date().getTime() - this._recordStart) / 1000;
- } else {
- this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ if (!this._paused) {
+ this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
}
}
}
@@ -253,7 +247,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (this._recorder) {
this._recorder.stop();
this._recorder = undefined;
- this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this._recordStart - this.pauseTime) / 1000;
+ const now = new Date().getTime();
+ this._paused && (this._pausedTime += now - this._pauseStart);
+ this.dataDoc[this.fieldKey + "-duration"] = (now - this._recordStart - this._pausedTime) / 1000;
this.mediaState = media_state.Paused;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
@@ -379,8 +375,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// continue the recording
recordPlay = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, returnFalse, action(() => {
- this._pauseEnd = new Date().getTime();
this._paused = false;
+ this._pausedTime += new Date().getTime() - this._pauseStart;
this._recorder.resume();
}), false);
}
@@ -578,7 +574,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<div className="timecode-current">
{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}
</div>
- {!this.miniPlayer &&
+ {this.miniPlayer ?
+ <div>/</div>
+ :
<div className="bottom-controls-middle">
<FontAwesomeIcon icon="search-plus" />
<input type="range" step="0.1" min="1" max="5" value={this.timeline?._zoomFactor}
diff --git a/src/client/views/nodes/DataViz.scss b/src/client/views/nodes/DataViz.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/DataViz.scss
diff --git a/src/client/views/nodes/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
new file mode 100644
index 000000000..d9541dba0
--- /dev/null
+++ b/src/client/views/nodes/DataViz.tsx
@@ -0,0 +1,21 @@
+import { observer } from "mobx-react";
+import * as React from "react";
+import { ViewBoxBaseComponent } from '../DocComponent';
+import "./DataViz.scss";
+import { FieldView, FieldViewProps } from "./FieldView";
+
+@observer
+export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+
+ render() {
+ return (
+ <div >
+ <div>
+ Hi
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 70732e74c..371d85a32 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -16,14 +16,15 @@ import { SearchBox } from "../search/SearchBox";
import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
import { AudioBox } from "./AudioBox";
+import { FontIconBox } from "./button/FontIconBox";
import { ColorBox } from "./ColorBox";
import { ComparisonBox } from "./ComparisonBox";
+import { DataVizBox } from "./DataViz";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { EquationBox } from "./EquationBox";
import { FieldView, FieldViewProps } from "./FieldView";
import { FilterBox } from "./FilterBox";
-import { FontIconBox } from "./button/FontIconBox";
import { FormattedTextBox, FormattedTextBoxProps } from "./formattedText/FormattedTextBox";
import { FunctionPlotBox } from "./FunctionPlotBox";
import { ImageBox } from "./ImageBox";
@@ -31,17 +32,17 @@ import { KeyValueBox } from "./KeyValueBox";
import { LabelBox } from "./LabelBox";
import { LinkAnchorBox } from "./LinkAnchorBox";
import { LinkBox } from "./LinkBox";
+import { MapBox } from "./MapBox/MapBox";
import { PDFBox } from "./PDFBox";
-import { PresBox } from "./trails/PresBox";
+import { RecordingBox } from "./RecordingBox";
import { ScreenshotBox } from "./ScreenshotBox";
import { ScriptingBox } from "./ScriptingBox";
import { SliderBox } from "./SliderBox";
+import { PresBox } from "./trails/PresBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import React = require("react");
import XRegExp = require("xregexp");
-import { MapBox } from "./MapBox/MapBox";
-import { RecordingBox } from "./RecordingBox";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -143,7 +144,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
const docOnlyProps = [ // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews
- "freezeDimensions",
"hideResizeHandles",
"hideTitle",
"treeViewDoc",
@@ -228,8 +228,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
PDFBox, VideoBox, AudioBox, RecordingBox, PresBox, YoutubeBox, PresElementBox, SearchBox, FilterBox, FunctionPlotBox,
ColorBox, DashWebRTCVideo, LinkAnchorBox, InkingStroke, LinkBox, ScriptingBox, MapBox,
- ScreenshotBox,
- HTMLtag, ComparisonBox
+ ScreenshotBox, DataVizBox, HTMLtag, ComparisonBox
}}
bindings={bindings}
jsx={layoutFrame}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4d84a8ad2..2ea976813 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -160,7 +160,6 @@ export interface DocumentViewSharedProps {
// these props are specific to DocuentViews
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
- freezeDimensions?: boolean;
hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
@@ -484,7 +483,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
let stopPropagate = true;
let preventDefault = true;
const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
- (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
+ (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
if (this._timeout) {
clearTimeout(this._timeout);
@@ -1220,11 +1219,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); }
@computed get nativeWidth() {
return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
- returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
+ returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
}
@computed get nativeHeight() {
return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
- returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
+ returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
}
@computed get shouldNotScale() {
return (this.fitWidth && !this.nativeWidth) ||
@@ -1244,15 +1243,16 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); }
@computed get panelHeight() {
- if (this.effectiveNativeHeight) {
- return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling);
+ if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
+ const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
+ return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
}
return this.props.PanelHeight();
}
@computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; }
- @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; }
+ @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && !this.layoutDoc.nativeHeightUnfrozen ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; }
@computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; }
- @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
+ @computed get centeringY() { return this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options);
@@ -1311,11 +1311,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
PanelHeight = () => this.panelHeight;
ContentScale = () => this.nativeScaling;
selfView = () => this;
- screenToLocalTransform = () => {
- const oshift = this.fitWidth && this.ComponentView instanceof FormattedTextBox;
- const shift = oshift ? -(this.props.PanelHeight() - this.rootDoc[HeightSym]()) / 2 : 0;
- return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).translate(0, shift).scale(1 / this.nativeScaling);
- }
+ screenToLocalTransform = () =>this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
componentDidMount() {
this._disposers.reactionScript = reaction(
() => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
@@ -1348,7 +1344,6 @@ export class DocumentView extends React.Component<DocumentViewProps> {
transition: this.props.dataTransition,
position: this.props.Document.isInkMask ? "absolute" : undefined,
transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- margin: this.fitWidth ? "auto" : undefined,
width: isButton || isPresTreeElement ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
`${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index d4cddd65e..aa51714da 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -80,55 +80,49 @@
// pointer-events: all;
// }
+.videoBox-ui-wrapper {
+ width: 0;
+ height: 0;
+}
+
.videoBox-ui {
position: absolute;
flex-direction: row;
align-items: center;
justify-content: center;
display: flex;
- width: 100%;
- visibility: none;
- opacity: 0;
background-color: $dark-gray;
color: white;
border-radius: 100px;
- transform-origin: bottom left;
- left: 0;
- bottom: 0;
-
- transition: top 0.5s, width 0.5s, opacity 0.2s, visibility 0s;
- height: 24px;
- padding: 0 20px;
+ height: 40px;
+ padding: 0 10px 0 7px;
+ transition: opacity 0.3s;
+ z-index: 100001;
.timecode-controls {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
- margin: 0 5px;
+ margin: 0 2px;
flex-grow: 2;
- font-size: 12px;
-
- .timecode {
- margin: 0 5px;
- }
+ font-size: 14px;
.timeline-slider {
- margin: 0 10px 0 10px;
+ margin: 5px;
flex-grow: 2;
}
}
- .toolbar-slider.volume,
- .toolbar-slider.zoom {
- width: 100px;
+ .toolbar-slider.volume, .toolbar-slider.zoom {
+ width: 50px;
}
.videobox-button {
- margin: 5px;
+ margin: 2px;
cursor: pointer;
- width: 24px;
- height: 24px;
+ width: 25px;
+ height: 25px;
border-radius: 50%;
background: $dark-gray;
display: flex;
@@ -140,8 +134,8 @@
}
svg {
- width: 18px;
- height: 18px;
+ width: 15px;
+ height: 15px;
}
}
}
@@ -163,28 +157,17 @@
}
}
-.videoBox:hover {
- .videoBox-ui {
- visibility: visible;
- opacity: 1;
- z-index: 10000;
- }
-}
-
-.videoBox-content-fullScreen,
-.videoBox-content-fullScreen-interactive {
+.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive {
display: flex;
justify-content: center;
- align-items: center;
-
- &:hover {
- .videoBox-ui {
- opacity: 0;
- }
- }
+ align-items: flex-end;
- .videoBox-ui:hover {
- opacity: 1;
+ .videoBox-ui {
+ left: 50%;
+ top: 90%;
+ transform: translate(-50%, -50%);
+ width: 80%;
+ transition: top 0s, width 0s, opacity 0.3s, visibility 0.3s;
}
}
@@ -195,7 +178,6 @@ video::-webkit-media-controls {
input[type="range"] {
-webkit-appearance: none;
background: none;
- margin: 10px;
}
input[type="range"]:focus {
@@ -204,19 +186,19 @@ input[type="range"]:focus {
input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
- height: 18px;
+ height: 10px;
cursor: pointer;
box-shadow: 0;
background: $light-gray;
- border-radius: 18px;
+ border-radius: 10px;
}
input[type="range"]::-webkit-slider-thumb {
box-shadow: 0;
border: 0;
- height: 20px;
- width: 20px;
- border-radius: 20px;
+ height: 12px;
+ width: 12px;
+ border-radius: 10px;
background: $medium-blue;
cursor: pointer;
-webkit-appearance: none;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index ddfbf50df..1b891034f 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -6,14 +6,16 @@ import { basename } from "path";
import * as rp from 'request-promise';
import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
import { InkTool } from "../../../fields/InkField";
+import { List } from "../../../fields/List";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, ImageField, RecordingField, VideoField } from "../../../fields/URLField";
+import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
import { Networking } from "../../Network";
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { DocumentManager } from "../../util/DocumentManager";
+import { RecordingApi } from "../../util/RecordingApi";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
import { undoBatch } from "../../util/UndoManager";
@@ -27,13 +29,10 @@ import { MarqueeAnnotator } from "../MarqueeAnnotator";
import { AnchorMenu } from "../pdf/AnchorMenu";
import { StyleProp } from "../StyleProvider";
import { FieldView, FieldViewProps } from './FieldView';
+import { RecordingBox } from "./RecordingBox/RecordingBox";
import "./VideoBox.scss";
-import { RecordingApi } from "../../util/RecordingApi";
-import { List } from "../../../fields/List";
-import { RecordingBox } from "./RecordingBox";
const path = require('path');
-
/**
* VideoBox
* Main component: VideoBox.tsx
@@ -48,874 +47,1005 @@ 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);
+ 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
+
+ @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;
+ @observable _scrubbing: boolean = false;
+
+ @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 ?? "";
+ }
+
+ // returns the presentation data if it exists, null otherwise
+ @computed get presentation() {
+ const data = this.dataDoc[this.fieldKey + '-presentation'];
+ return data ? JSON.parse(data) : null;
+ }
+
+ @computed private get timeline() { return this._stackedTimeline; }
+ private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
+ public get player(): HTMLVideoElement | null { return this._videoRef; }
+
+
+ componentDidMount() {
+ this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
+ if (this.youtubeVideoId) {
+ const youtubeaspect = 400 / 315;
+ const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+ const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+ if (!nativeWidth || !nativeHeight) {
+ if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
+ Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
+ this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
}
- }
-
- 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;
-
- @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 ?? "";
- }
-
- // returns the presentation data if it exists, null otherwise
- @computed get presentation() {
- const data = this.dataDoc[this.fieldKey + '-presentation'];
- return data ? JSON.parse(data) : null;
- }
-
-
- @computed private get timeline() { return this._stackedTimeline; }
- private get transition() { return this._clicking ? "left 0.5s, width 0.5s, height 0.5s" : ""; } // css transition for hiding/showing timeline
- public get player(): HTMLVideoElement | null { return this._videoRef; }
-
-
- componentDidMount() {
- this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
- if (this.youtubeVideoId) {
- const youtubeaspect = 400 / 315;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 600);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 600) / youtubeaspect);
- this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
- }
+ }
+ this.player && this.setPlayheadTime(0);
+ document.addEventListener("keydown", this.keyEvents, true);
+ }
+
+ componentWillUnmount() {
+ this.removeCurrentlyPlaying();
+ this.Pause();
+ Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
+ document.removeEventListener("keydown", this.keyEvents, true);
+ }
+
+ // handles key events, when timeline scrubs fade controls
+ @action
+ keyEvents = (e: KeyboardEvent) => {
+ if (
+ // need to include range inputs because after dragging time slider it becomes target element
+ !(e.target instanceof HTMLInputElement && !(e.target.type === "range")) &&
+ this.props.isSelected(true)
+ ) {
+ switch (e.key) {
+ case "ArrowLeft":
+ case "ArrowRight":
+ clearTimeout(this._controlsFadeTimer);
+ this._scrubbing = true;
+ this._controlsFadeTimer = setTimeout(action(() => this._scrubbing = false), 500);
+ e.stopPropagation();
+ break;
}
- 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;
-
- // plays video
- @action public Play = (update: boolean = true) => {
- // if (Doc.UserDoc().presentationMode === 'watching' && !this._playing) {
+ // 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);
- }
+ // 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;
}
- this.updateTimecode();
- }
- // goes to time
- @action public Seek(time: number) {
try {
- this._youtubePlayer?.seekTo(Math.round(time), true);
+ 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 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);
+ console.log("Video Play Exception:", e);
}
-
- 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);
+ }
+ 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
+ }
+ 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();
+ if (!this._scrubbing) {
+ clearTimeout(this._controlsFadeTimer);
+ this._controlsVisible = true;
+ this._controlsFadeTimer = setTimeout(action(() => this._controlsVisible = false), 3000);
+ }
+ }
+
+
+ // drag controls around window in fulls screen
+ @action controlsDrag = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const eleStyle = getComputedStyle(e.target as Element);
+ this._controlsTransform = { X: parseInt(eleStyle.left), Y: parseInt(eleStyle.top) };
+
+ setupMoveUpEvents(e.target,
+ e,
+ action((e, down, delta) => {
+ if (this._controlsTransform) {
+ this._controlsTransform.X = Math.max(0, Math.min(delta[0] + this._controlsTransform.X, window.innerWidth));
+ this._controlsTransform.Y = Math.max(0, Math.min(delta[1] + this._controlsTransform.Y, window.innerHeight));
+ }
+ return false;
+ }),
+ emptyFunction,
+ emptyFunction)
+ }
+
+
+ // creates and links snapshot photo of current video frame
+ @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+ const width = NumCast(this.layoutDoc._width);
+ const canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
+ const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ if (ctx) {
+ 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.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);
+ 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;
}
-
- 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));
+ else {
+ Promise.all(thumbnailPromises).then(thumbnails => { this.dataDoc.thumbnails = new List<string>(thumbnails); });
}
- }
-
- 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-"
+ }
+
+ 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);
+ this._controlsVisible = true;
+ this._scrubbing = false;
+ clearTimeout(this._controlsFadeTimer);
+ if (this._fullScreen) {
+ document.addEventListener('pointermove', this.controlsFade);
+ }
+ else {
+ document.removeEventListener('pointermove', this.controlsFade);
+ }
});
- 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);
- }
- }
-
-
- // 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);
- console.log("vref time = " + vref.currentTime)
- }, { fireImmediately: true });
- }
- }
-
- // set ref for div that wraps video and controls for fullscreen
- @action
- setContentRef = (cref: HTMLDivElement | null) => {
- this._contentRef = cref;
- if (cref) {
- cref.onfullscreenchange = action((e) => this._fullScreen = (document.fullscreenElement === cref));
- }
- }
-
-
- // 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" });
- // if the videobox was turned from a recording box
- if (this.dataDoc[this.fieldKey + "-recorded"] === true) {
- subitems.push({
- description: "Recreate recording", event: () => {
- this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
- // delete assoicated video data
- this.dataDoc[this.props.fieldKey] = "";
- this.dataDoc[this.fieldKey + "-duration"] = "";
- // delete assoicated presentation data
- this.dataDoc[this.fieldKey + "-presentation"] = "";
- }, icon: "expand-arrows-alt"
- });
-
- }
- ContextMenu.Instance.addItem({ description: "Options...", subitems: subitems, icon: "video" });
- }
- }
-
-
- // 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.ActiveTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
- return !field ? <div key="loading">Loading</div> :
- <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
- <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
- {this.uIButtons}
- <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
- onCanPlay={this.videoLoad}
- controls={VideoBox._nativeControls}
- onPlay={() => {
- // console.log("PLAY from CONTENT")
- //this.Play()
- }}
- onSeeked={this.updateTimecode}
- // onPause={() => this.Pause() }
- onClick={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);
+ }
+ }
+
+
+ // 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"
});
- 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.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
- (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
- };
- if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
- 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 = [];
+ 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" });
+ // if the videobox was turned from a recording box
+ if (this.dataDoc[this.fieldKey + "-recorded"] === true) {
+ subitems.push({
+ description: "Recreate recording", event: () => {
+ this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
+ // delete assoicated video data
+ this.dataDoc[this.props.fieldKey] = "";
+ this.dataDoc[this.fieldKey + "-duration"] = "";
+ // delete assoicated presentation data
+ this.dataDoc[this.fieldKey + "-presentation"] = "";
+ }, icon: "expand-arrows-alt"
+ });
}
- 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();
- }
+ 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.ActiveTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
+ const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+ const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0);
+ return !field ? <div key="loading">Loading</div> :
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply", cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'pointer' }}>
+ <div className={classname} ref={this.setContentRef} onPointerDown={(e) => this._fullScreen && e.stopPropagation()}>
+ {this._fullScreen && <div className="videoBox-ui" onPointerDown={this.controlsDrag}
+ style={{ left: this._controlsTransform && this._controlsTransform.X, top: this._controlsTransform && this._controlsTransform.Y, visibility: this._controlsVisible || this._scrubbing ? 'visible' : 'hidden', opacity: opacity }}>
+ {this.UIButtons}
+ </div>}
+ <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} style={this._fullScreen ? this.fullScreenSize() : {}}
+ onCanPlay={this.videoLoad}
+ controls={VideoBox._nativeControls}
+ onPlay={() => this.Play()}
+ onSeeked={this.updateTimecode}
+ onPause={() => this.Pause()}
+ onClick={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;
}
- }
-
-
- // 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) {
+ 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.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+ (interactive) => iframe.style.pointerEvents = interactive ? "all" : "none", { fireImmediately: true });
+ };
+ if (typeof (YT) === undefined) setTimeout(() => this.loadYouTube(iframe), 100);
+ 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 = () => 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();
- 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();
- }
+ }, playRegionDuration * 1000);
+ } else {
+ this.Pause();
}
- }
-
- // toggles video mute
- @action
- toggleMute = () => {
- if (this.player) {
- this._muted = !this._muted;
- this.player.muted = this._muted;
+ }
+ }
+
+
+ // 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);
}
- }
-
-
- // 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%" };
+ }));
+ }
+
+
+ // for volume slider sets volume
+ @action
+ setVolume = (volume: number) => {
+ if (this.player) {
+ this._volume = volume;
+ this.player.volume = volume;
+ if (this._muted) {
+ this.toggleMute();
}
- else {
- return { width: "100%" };
+ }
+ }
+
+ // 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.ActiveTool)) {
+ setupMoveUpEvents(this, e, action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ }
+ }
+
+ // ends marquee selection
+ @action
+ finishMarquee = () => {
+ this._marqueeing = undefined;
+ this.props.select(true);
+ }
+
+ timelineWhenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
+
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().scale(this.scaling()).translate(0, -this.heightPercent / 100 * this.props.PanelHeight());
+
+ setPlayheadTime = (time: number) => this.player!.currentTime = this.layoutDoc._currentTimecode = time;
+
+ timelineHeight = () => this.props.PanelHeight() * (100 - this.heightPercent) / 100;
+
+ playing = () => this._playing;
+
+ contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
+
+ scaling = () => this.props.scaling?.() || 1;
+
+ panelWidth = () => this.props.PanelWidth() * this.heightPercent / 100;
+ panelHeight = () => this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : this.props.PanelHeight() * this.heightPercent / 100;
+
+ screenToLocalTransform = () => {
+ const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
+ return this.props.ScreenToLocalTransform().translate(-offset, 0).scale(100 / this.heightPercent);
+ }
+
+ marqueeFitScaling = () => (this.props.scaling?.() || 1) * this.heightPercent / 100;
+ marqueeOffset = () => [this.panelWidth() / 2 * (1 - this.heightPercent / 100) / (this.heightPercent / 100), 0];
+
+ timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+
+
+ // renders video controls
+ componentUI = (boundsLeft: number, boundsTop: number) => {
+ const bounds = this.props.docViewPath().lastElement().getBounds();
+ const left = bounds?.left || 0;
+ const right = bounds?.right || 0;
+ const top = bounds?.top || 0;
+ const height = (bounds?.bottom || 0) - top;
+ const width = Math.max(right - left, 100);
+ const uiHeight = Math.max(25, Math.min(50, height / 10));
+ const uiMargin = Math.min(10, height / 20);
+ const vidHeight = height * this.heightPercent / 100;
+ const yPos = top + vidHeight - uiHeight - uiMargin;
+ const xPos = uiHeight / vidHeight > 0.4 ? right + 10 : left + 10;
+ const opacity = this._scrubbing ? 0.3 : (this._controlsVisible ? 1 : 0);
+ return this._fullScreen || (right - left) < 50 ? null : <div className="videoBox-ui-wrapper" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <div className="videoBox-ui" style={{ left: xPos, top: yPos, height: uiHeight, width: width - 20, transition: this._clicking ? "top 0.5s" : "", opacity: opacity}}>
+ {this.UIButtons}
+ </div>
+ </div>
+ }
+
+ @computed get UIButtons() {
+ const bounds = this.props.docViewPath().lastElement().getBounds();
+ const width = (bounds?.right || 0) - (bounds?.left || 0);
+ const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ return <>
+ <div className="videobox-button"
+ title={this._playing ? "play" : "pause"}
+ onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? "pause" : "play"} />
+ </div>
+
+ {this.timeline && width > 150 && <div className="timecode-controls">
+ <div className="timecode-current">
+ {formatTime(curTime)}
+ </div>
+
+ {this._fullScreen || (this.heightPercent === 100 && width > 200) ?
+ <div className="timeline-slider">
+ <input type="range" step="0.1" min={this.timeline.clipStart} max={this.timeline.clipEnd} value={curTime}
+ className="toolbar-slider time-progress"
+ onPointerDown={action((e: React.PointerEvent) => { e.stopPropagation(); this._scrubbing = true;})}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ onPointerUp={action((e: React.PointerEvent) => {e.stopPropagation(); this._scrubbing = false;})}
+ />
+ </div>
+ :
+ <div>/</div>}
+
+ <div className="timecode-end">
+ {formatTime(this.timeline.clipDuration)}
+ </div>
+ </div>
}
- }
-
-
- // 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);
+ <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>
+ }
- // starts marquee selection
- marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && this.layoutDoc._viewScale === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.ActiveTool)) {
- setupMoveUpEvents(this, e, action(e => {
- MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX, e.clientY];
- return true;
- }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), false);
+ <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))}
+ />
}
- }
-
- // 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))}
- />
-
- {!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}
+ {
+ !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}
- startTag={"_timecodeToShow" /* videoStart */}
- endTag={"_timecodeToHide" /* videoEnd */}
- bringToFront={emptyFunction}
+ fieldKey={this.annotationKey}
CollectionView={undefined}
- playFrom={this.playFrom}
- setTime={this.setPlayheadTime}
- playing={this.playing}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.timelineWhenChildContentsActiveChanged}
- moveDocument={this.moveDocument}
- addDocument={this.addDocument}
+ 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}
- 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 >);
- }
+ 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/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 3af6a3d51..85efc67a5 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -79,6 +79,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
}
+ static GetShowLabels() { return BoolCast(Doc.UserDoc()._showLabel); }
+ static SetShowLabels(show:boolean) { Doc.UserDoc()._showLabel = show; }
+
// Determining UI Specs
@observable private label = StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
@observable private icon = StrCast(this.dataDoc.icon, "user") as any;
@@ -111,7 +114,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// Script for checking the outcome of the toggle
const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
- const label = !Doc.UserDoc()._showLabel ? (null) :
+ const label = !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label">
{this.label}
</div>;
@@ -212,7 +215,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(() => this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen)}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
- {!this.label || !Doc.UserDoc()._showLabel ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
+ {!this.label || !FontIconBox.GetShowLabels() ? (null) : <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
<div
className="menuButton-dropdown"
style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
@@ -283,7 +286,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>;
});
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ bottom: 0, position: "absolute", color: color, backgroundColor: backgroundColor }}>
{this.label}
</div>;
@@ -337,7 +340,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const curColor = this.colorScript?.script.run({ value: undefined, _readOnly_: true }).result ?? "transparent";
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color, backgroundColor }}>
{this.label}
</div>;
@@ -349,7 +352,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>;
setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
return (
- <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")} ${this.colorPickerClosed}`}
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? "Label" : "")} ${this.colorPickerClosed}`}
style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(() => this.colorPickerClosed = !this.colorPickerClosed)}
onPointerDown={e => e.stopPropagation()}>
@@ -381,7 +384,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
// Button label
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color, backgroundColor }}>
{this.label}
</div>;
@@ -400,7 +403,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
);
} else {
return (
- <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`}
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? "Label" : "")}`}
style={{ opacity: 1, backgroundColor, color }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
@@ -423,7 +426,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
style={{ backgroundColor: "transparent", borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
<div className="menuButton-wrap">
<FontAwesomeIcon className={`menuButton-icon-${this.type}`} icon={this.icon} color={"black"} size={"sm"} />
- {!this.label || !Doc.UserDoc()._showLabel ? (null) :
+ {!this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {this.label} </div>}
</div>
</div>
@@ -450,12 +453,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
render() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const label = !this.label || !Doc.UserDoc()._showLabel ? (null) :
+ const label = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color, backgroundColor }}>
{this.label}
</div>;
- const menuLabel = !this.label || !Doc.UserDoc()._showMenuLabel ? (null) :
+ const menuLabel = !this.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color, backgroundColor: "transparent" }}>
{this.label}
</div>;
@@ -497,7 +500,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
case ButtonType.ToolButton:
button = (
- <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`} style={{ opacity: 1, backgroundColor, color }}>
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? "Label" : "")}`} style={{ opacity: 1, backgroundColor, color }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
@@ -509,7 +512,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
case ButtonType.ClickButton:
button = (
- <div className={`menuButton ${this.type + (Doc.UserDoc()._showLabel ? "Label" : "")}`} style={{ color, backgroundColor, opacity: 1 }}>
+ <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? "Label" : "")}`} style={{ color, backgroundColor, opacity: 1 }}>
<FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={this.icon} color={color} />
{label}
</div>
@@ -675,10 +678,9 @@ ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: b
ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return (editorView ? RichTextMenu.Instance.noAutoLink : Doc.UserDoc().noAutoLink) ? Colors.MEDIUM_BLUE : "transparent";
+ return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : "transparent";
}
if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
- else Doc.UserDoc().noAutoLink = Doc.UserDoc().noAutoLink ? true : false;
});
ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
@@ -901,9 +903,9 @@ ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
}
else if (selected) {
if (NumCast(selected.schemaPreviewWidth) > 0) {
- selected.schemaPreviewWidth = 200;
- } else {
selected.schemaPreviewWidth = 0;
+ } else {
+ selected.schemaPreviewWidth = 200;
}
}
});
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
index 235495250..7f414ddbb 100644
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
@@ -5,6 +5,7 @@ import { IButtonProps } from '../ButtonInterface';
import { ColorState, SketchPicker } from 'react-color';
import { ScriptField } from '../../../../../fields/ScriptField';
import { Doc } from '../../../../../fields/Doc';
+import { FontIconBox } from '../FontIconBox';
export class ColorDropdown extends Component<IButtonProps> {
render() {
@@ -31,7 +32,7 @@ export class ColorDropdown extends Component<IButtonProps> {
disableAlpha={!stroke}
onChange={func} color={boolResult ? boolResult : "#FFFFFF"}
presetColors={colorOptions} />;
- const label = !this.props.label || !Doc.UserDoc()._showLabel ? (null) :
+ const label = !this.props.label || !FontIconBox.GetShowLabels() ? (null) :
<div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: "absolute" }}>
{this.props.label}
</div>;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 90199618b..9ae604e9b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -355,7 +355,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
var tr = this._editorView.state.tr as any;
const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
- DocListCast(Doc.UserDoc().myPublishedDocs).forEach(term => tr = this.hyperlinkTerm(tr, term, newAutoLinks));
+ DocListCast(CurrentUserUtils.MyPublishedDocs.data).forEach(term => tr = this.hyperlinkTerm(tr, term, newAutoLinks));
tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
@@ -376,7 +376,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (!(cfield instanceof ComputedField)) {
this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : "");
if (str.startsWith("@") && str.length > 1) {
- Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", this.rootDoc);
+ Doc.AddDocToList(CurrentUserUtils.MyPublishedDocs, undefined, this.rootDoc);
}
}
}
@@ -854,6 +854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed
}
+ getScrollHeight = () => this.scrollHeight;
// if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
// Since we also monitor all component height changes, this will update the document's height.
resetNativeHeight = (scrollHeight: number) => {
@@ -862,6 +863,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (nh) this.layoutDoc._nativeHeight = scrollHeight;
}
+ @computed get contentScaling() { return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.scaling?.() || 1 : 1;}
componentDidMount() {
!this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
this._cachedLinks = DocListCast(this.Document.links);
@@ -875,7 +877,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on
() => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
- autoHeight && this.props.setHeight?.((this.props.scaling?.() || 1) * (marginsHeight + Math.max(sidebarHeight, textHeight)));
+ autoHeight && this.props.setHeight?.(this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)));
}, { fireImmediately: true });
this._disposers.links = reaction(() => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0d2cffc2c..9f858539f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -10,7 +10,7 @@ import { InkTool } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { PrefetchProxy } from "../../../../fields/Proxy";
import { listSpec } from "../../../../fields/Schema";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types";
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from "../../../documents/Documents";
import { DocumentType } from "../../../documents/DocumentTypes";
@@ -131,17 +131,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
else return false;
}
- @computed get presElement() { return Cast(Doc.UserDoc().presElement, Doc, null); }
constructor(props: any) {
super(props);
if (CurrentUserUtils.ActivePresentation = this.rootDoc) runInAction(() => PresBox.Instance = this);
- if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations.
- Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
- title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"
- }));
- }
this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
-
}
@computed get selectedDocumentView() {
if (SelectionManager.Views().length) return SelectionManager.Views()[0];
@@ -728,7 +721,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
return true;
}
- childLayoutTemplate = () => !this.isTreeOrStack ? undefined : this.presElement;
+ childLayoutTemplate = () => !this.isTreeOrStack ? undefined : DocCast(Doc.UserDoc().presElement);
removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 13c8e907a..bc9ed9293 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -60,7 +60,7 @@ export class TopBar extends React.Component {
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());
+ await CurrentUserUtils.snapshotDashboard();
batch.end();
}, icon: "edit" });
dashView?.showContextMenu(e.clientX+20, e.clientY+30);
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 981514b25..4fe6eb1e7 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -658,7 +658,7 @@ export namespace Doc {
const zip = new JSZip();
- zip.file(doc.title + ".json", docString);
+ zip.file("doc.json", docString);
// // Generate a directory within the Zip file structure
// var img = zip.folder("images");
@@ -1289,6 +1289,21 @@ export namespace Doc {
}
}
+ export async function importDocument(file:File) {
+ const upload = Utils.prepend("/uploadDoc");
+ const formData = new FormData();
+ if (file) {
+ formData.append('file', file);
+ formData.append('remap', "true");
+ const response = await fetch(upload, { method: "POST", body: formData });
+ const json = await response.json();
+ if (json !== "error") {
+ const doc = await DocServer.GetRefField(json);
+ return doc;
+ }
+ }
+ return undefined;
+ }
export namespace Get {
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index fe8100997..bf06faeb9 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -391,7 +391,7 @@ export class MobileInterface extends React.Component {
* Handles the 'Create New Dashboard' button in the menu (taken from MainView.tsx)
*/
@action
- createNewDashboard = async (id?: string) => {
+ createNewDashboard = (id?: string) => {
const scens = CurrentUserUtils.MyDashboards;
const dashboardCount = DocListCast(scens.data).length + 1;
const freeformOptions: DocumentOptions = {