aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormehekj <mehek.jethani@gmail.com>2022-10-12 13:21:07 -0400
committermehekj <mehek.jethani@gmail.com>2022-10-12 13:21:07 -0400
commit0b3a83acd4f75b7f6ff4b9bb7daf4377dede51a1 (patch)
tree438789f7e7f50e5eb9829e1f301b4d043d8d4906 /src
parent69ca9baca6ff1da272a5191187542351bd242ccc (diff)
parenteb5f75785fd28acb50f1b30434e89223fff00185 (diff)
Merge branch 'master' into schema-mehek
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/Network.ts117
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts173
-rw-r--r--src/client/goldenLayout.js48
-rw-r--r--src/client/util/CurrentUserUtils.ts99
-rw-r--r--src/client/util/DocumentManager.ts24
-rw-r--r--src/client/util/DragManager.ts44
-rw-r--r--src/client/util/InteractionUtils.tsx53
-rw-r--r--src/client/util/ReportManager.tsx33
-rw-r--r--src/client/util/Scripting.ts8
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SettingsManager.tsx2
-rw-r--r--src/client/util/SharingManager.scss3
-rw-r--r--src/client/util/SharingManager.tsx2
-rw-r--r--src/client/util/request-image-size.js26
-rw-r--r--src/client/views/ContextMenu.scss2
-rw-r--r--src/client/views/ContextMenu.tsx30
-rw-r--r--src/client/views/DashboardView.scss85
-rw-r--r--src/client/views/DashboardView.tsx252
-rw-r--r--src/client/views/DictationOverlay.tsx60
-rw-r--r--src/client/views/DocComponent.tsx4
-rw-r--r--src/client/views/DocumentButtonBar.tsx7
-rw-r--r--src/client/views/DocumentDecorations.scss189
-rw-r--r--src/client/views/DocumentDecorations.tsx25
-rw-r--r--src/client/views/GestureOverlay.scss5
-rw-r--r--src/client/views/GestureOverlay.tsx43
-rw-r--r--src/client/views/GlobalKeyHandler.ts9
-rw-r--r--src/client/views/InkingStroke.tsx15
-rw-r--r--src/client/views/LightboxView.scss96
-rw-r--r--src/client/views/LightboxView.tsx126
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/MainView.scss5
-rw-r--r--src/client/views/MainView.tsx94
-rw-r--r--src/client/views/MarqueeAnnotator.scss9
-rw-r--r--src/client/views/MarqueeAnnotator.tsx3
-rw-r--r--src/client/views/OverlayView.tsx137
-rw-r--r--src/client/views/PreviewCursor.scss5
-rw-r--r--src/client/views/PreviewCursor.tsx75
-rw-r--r--src/client/views/PropertiesView.tsx23
-rw-r--r--src/client/views/SidebarAnnos.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx4
-rw-r--r--src/client/views/_nodeModuleOverrides.scss4
-rw-r--r--src/client/views/collections/CollectionDockingView.scss8
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx46
-rw-r--r--src/client/views/collections/CollectionMenu.tsx21
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.scss41
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx145
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx88
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewDivider.tsx1
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss5
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx35
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx16
-rw-r--r--src/client/views/collections/CollectionSubView.tsx27
-rw-r--r--src/client/views/collections/CollectionTreeView.scss5
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx73
-rw-r--r--src/client/views/collections/CollectionView.tsx10
-rw-r--r--src/client/views/collections/TabDocView.tsx172
-rw-r--r--src/client/views/collections/TreeView.scss19
-rw-r--r--src/client/views/collections/TreeView.tsx15
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx144
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss20
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx16
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx20
-rw-r--r--src/client/views/global/globalCssVariables.scss8
-rw-r--r--src/client/views/nodes/AudioBox.tsx4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx46
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.scss4
-rw-r--r--src/client/views/nodes/DocumentView.tsx132
-rw-r--r--src/client/views/nodes/FilterBox.tsx10
-rw-r--r--src/client/views/nodes/ImageBox.tsx7
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx4
-rw-r--r--src/client/views/nodes/LoadingBox.scss35
-rw-r--r--src/client/views/nodes/LoadingBox.tsx68
-rw-r--r--src/client/views/nodes/PDFBox.tsx6
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss18
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx9
-rw-r--r--src/client/views/nodes/VideoBox.scss22
-rw-r--r--src/client/views/nodes/VideoBox.tsx326
-rw-r--r--src/client/views/nodes/WebBox.scss8
-rw-r--r--src/client/views/nodes/WebBox.tsx64
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx38
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx22
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts9
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx104
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts17
-rw-r--r--src/client/views/nodes/trails/PresBox.scss27
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx1445
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss337
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx167
-rw-r--r--src/client/views/nodes/trails/PresEnums.ts42
-rw-r--r--src/client/views/pdf/Annotation.tsx3
-rw-r--r--src/client/views/pdf/PDFViewer.tsx19
-rw-r--r--src/client/views/topbar/TopBar.scss33
-rw-r--r--src/client/views/topbar/TopBar.tsx243
-rw-r--r--src/debug/Viewer.tsx72
-rw-r--r--src/fields/Doc.ts49
-rw-r--r--src/fields/SchemaHeaderField.ts2
-rw-r--r--src/fields/ScriptField.ts24
-rw-r--r--src/fields/util.ts11
-rw-r--r--src/mobile/MobileInterface.tsx2
-rw-r--r--src/server/ApiManagers/UploadManager.ts29
-rw-r--r--src/server/DashUploadUtils.ts349
-rw-r--r--src/server/SharedMediaTypes.ts38
107 files changed, 3801 insertions, 2935 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 4751acf44..4ed785983 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/Utils.ts b/src/Utils.ts
index 9e002ebd4..6fc00040f 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -221,8 +221,8 @@ export namespace Utils {
if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
}
- if (scrollTop >= Math.max(0, targetY - minSpacing - targetHgt)) {
- return Math.max(0, Math.floor(targetY - minSpacing - targetHgt));
+ if (scrollTop >= Math.max(0, targetY - minSpacing)) {
+ return Math.max(0, Math.floor(targetY - minSpacing));
}
}
diff --git a/src/client/Network.ts b/src/client/Network.ts
index c781d4b6b..19eff3b3b 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,57 +1,74 @@
-import { Utils } from "../Utils";
+import { Utils } from '../Utils';
import requestPromise = require('request-promise');
-import { Upload } from "../server/SharedMediaTypes";
+import { Upload } from '../server/SharedMediaTypes';
export namespace Networking {
+ export async function FetchFromServer(relativeRoute: string) {
+ return (await fetch(relativeRoute)).text();
+ }
- export async function FetchFromServer(relativeRoute: string) {
- return (await fetch(relativeRoute)).text();
- }
+ export async function PostToServer(relativeRoute: string, body?: any) {
+ const options = {
+ uri: Utils.prepend(relativeRoute),
+ method: 'POST',
+ body,
+ json: true,
+ };
+ return requestPromise.post(options);
+ }
- export async function PostToServer(relativeRoute: string, body?: any) {
- const options = {
- uri: Utils.prepend(relativeRoute),
- method: "POST",
- body,
- json: true
- };
- return requestPromise.post(options);
- }
-
- /**
- * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint
- * with the mapping of GUID to filem as parameters.
- *
- * @param files the files to be uploaded to the server
- * @returns the response as a json from the server
- */
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
- const formData = new FormData();
- if (Array.isArray(files)) {
- if (!files.length) {
- return [];
- }
- files.forEach(file => formData.append(Utils.GenerateGuid(), file));
- } else {
- formData.append(Utils.GenerateGuid(), files);
+ /**
+ * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint
+ * with the mapping of GUID to filem as parameters.
+ *
+ * @param files the files to be uploaded to the server
+ * @returns the response as a json from the server
+ */
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+ const formData = new FormData();
+ if (Array.isArray(files)) {
+ if (!files.length) {
+ return [];
+ }
+ const maxFileSize = 50000000;
+ if (files.some(f => f.size > maxFileSize)) {
+ return new Promise<any>(res =>
+ res([
+ {
+ source: { name: '', type: '', size: 0, toJson: () => ({ name: '', type: '' }) },
+ result: { name: '', message: `max file size (${maxFileSize / 1000000}MB) exceeded` },
+ },
+ ])
+ );
}
- const parameters = {
- method: 'POST',
- body: formData
- };
- const response = await fetch("/uploadFormData", parameters);
- return response.json();
- }
-
- export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
- const parameters = {
- method: 'POST',
- body: JSON.stringify({ videoId }),
- json: true
- };
- const response = await fetch("/uploadYoutubeVideo", parameters);
- return response.json();
- }
-
+ files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+ } else {
+ formData.append(Utils.GenerateGuid(), files);
+ }
+ const parameters = {
+ method: 'POST',
+ body: formData,
+ };
+ const response = await fetch('/uploadFormData', parameters);
+ return response.json();
+ }
-} \ No newline at end of file
+ export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+ const parameters = {
+ method: 'POST',
+ body: JSON.stringify({ videoId }),
+ json: true,
+ };
+ const response = await fetch('/uploadYoutubeVideo', parameters);
+ return response.json();
+ }
+ export async function QueryYoutubeProgress(videoId: string): Promise<{ progress: string }> {
+ const parameters = {
+ method: 'POST',
+ body: JSON.stringify({ videoId }),
+ json: true,
+ };
+ const response = await fetch('/queryYoutubeProgress', parameters);
+ return response.json();
+ }
+}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index c9b42f2b9..b910b26b0 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -26,6 +26,7 @@ export enum DocumentType {
FUNCPLOT = 'funcplot', // function plotter
MAP = 'map',
DATAVIZ = 'dataviz',
+ LOADING = 'loading',
// 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 c9424a92e..2a6a4f60d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -49,6 +49,7 @@ 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 { LoadingBox } from '../views/nodes/LoadingBox';
import { MapBox } from '../views/nodes/MapBox/MapBox';
import { PDFBox } from '../views/nodes/PDFBox';
import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox';
@@ -169,6 +170,7 @@ export class DocumentOptions {
_raiseWhenDragged?: boolean; // whether a document is brought to front when dragged.
_hideContextMenu?: boolean; // whether the context menu can be shown
_viewType?: string; // sub type of a collection
+ viewType?: string; // sub type of a collection
_gridGap?: number; // gap between items in masonry view
_viewScale?: number; // how much a freeform view has been scaled (zoomed)
_overflow?: string; // set overflow behavior
@@ -395,6 +397,7 @@ export namespace Docs {
_yMargin: 10,
nativeDimModifiable: true,
treeViewGrowsHorizontally: true,
+ nativeHeightUnfrozen: true,
forceReflow: true,
links: '@links(self)',
},
@@ -596,6 +599,7 @@ export namespace Docs {
DocumentType.PRESELEMENT,
{
layout: { view: PresElementBox, dataField: defaultDataKey },
+ options: { title: 'pres element template', _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' },
},
],
[
@@ -655,6 +659,13 @@ export namespace Docs {
layout: { view: SchemaRowBox, dataField: defaultDataKey },
},
],
+ [
+ DocumentType.LOADING,
+ {
+ layout: { view: LoadingBox, dataField: '' },
+ options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
+ },
+ ],
]);
const suffix = 'Proto';
@@ -793,7 +804,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string) {
+ function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) {
const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_"
const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
@@ -804,21 +815,36 @@ export namespace Docs {
dataProps.isPrototype = true;
dataProps.author = Doc.CurrentUserEmail;
dataProps.creationDate = new DateField();
- dataProps[`${fieldKey}-lastModified`] = new DateField();
+ if (fieldKey) {
+ dataProps[`${fieldKey}-lastModified`] = new DateField();
+ dataProps[fieldKey] = data;
+
+ // so that the list of annotations is already initialised, prevents issues in addonly.
+ // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
+ dataProps[fieldKey + '-annotations'] = new List<Doc>();
+ dataProps[fieldKey + '-sidebar'] = new List<Doc>();
+ }
- dataProps[fieldKey] = data;
+ // users placeholderDoc as proto if it exists
+ const dataDoc = Doc.assign(placeholderDoc ? Doc.GetProto(placeholderDoc) : Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
- // so that the list of annotations is already initialised, prevents issues in addonly.
- // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
- dataProps[fieldKey + '-annotations'] = new List<Doc>();
- dataProps[fieldKey + '-sidebar'] = new List<Doc>();
- const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true);
+ if (placeholderDoc) {
+ dataDoc.proto = proto;
+ }
const viewFirstProps: { [id: string]: any } = {};
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);
+ let viewDoc: Doc;
+ // determines whether viewDoc should be created using placeholder Doc or default
+ if (placeholderDoc) {
+ placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
+ placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
+ viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+ } else {
+ viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
+ }
Doc.assign(viewDoc, viewProps, true, true);
![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
@@ -830,12 +856,13 @@ export namespace Docs {
updateCachedAcls(dataDoc);
updateCachedAcls(viewDoc);
+
return viewDoc;
}
- export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}) {
+ export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
const imgField = url instanceof ImageField ? url : new ImageField(url);
- return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options });
+ return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
}
export function PresDocument(options: DocumentOptions = {}) {
@@ -846,12 +873,12 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script ? script : undefined, { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined });
}
- export function VideoDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options);
+ export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+ return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc);
}
- export function YoutubeDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options);
+ export function YoutubeDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+ return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options, undefined, undefined, undefined, overwriteDoc);
}
export function WebCamDocument(url: string, options: DocumentOptions = {}) {
@@ -866,8 +893,16 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options);
}
- export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any });
+ export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.AUDIO),
+ new AudioField(url),
+ { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any },
+ undefined,
+ undefined,
+ undefined,
+ overwriteDoc
+ );
}
export function RecordingDocument(url: string, options: DocumentOptions = {}) {
@@ -881,6 +916,9 @@ export namespace Docs {
export function ColorDocument(options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.COLOR), '', options);
}
+ export function LoadingDocument(file: File | string, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.LOADING), undefined, { _height: 150, _width: 200, title: typeof file == 'string' ? file : file.name, ...options }, undefined, '');
+ }
export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = 'text') {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
@@ -971,13 +1009,13 @@ export namespace Docs {
return I;
}
- export function PdfDocument(url: string, options: DocumentOptions = {}) {
+ export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
const width = options._width || undefined;
const height = options._height || undefined;
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.PDF), new PdfField(url), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options, undefined, undefined, undefined, overwriteDoc);
}
export function WebDocument(url: string, options: DocumentOptions = {}) {
@@ -1104,12 +1142,8 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) });
}
- export function PresElementBoxDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) });
- }
-
- export function SchemaRowDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.SCHEMAROW), undefined, { ...(options || {}) });
+ export function PresElementBoxDocument() {
+ return Prototypes.get(DocumentType.PRESELEMENT);
}
export function DataVizDocument(url: string, options?: DocumentOptions) {
@@ -1117,7 +1151,7 @@ export namespace Docs {
}
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);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: 'remove|add', ...options, viewType: CollectionViewType.Docking, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
export function DirectoryImportDocument(options: DocumentOptions = {}) {
@@ -1281,7 +1315,7 @@ export namespace DocUtils {
});
}
- export function DefaultFocus(doc: Doc, options?: DocFocusOptions) {
+ export function DefaultFocus(doc: Doc, options: DocFocusOptions) {
options?.afterFocus?.(false);
}
@@ -1362,11 +1396,17 @@ export namespace DocUtils {
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', thisContainer: Doc.name, documentView: Doc.name, heading: Doc.name, checked: 'boolean', containingTreeView: Doc.name },
- { _readOnly_: true }
- );
+ doc[key] = ScriptField.MakeScript(scripts[key], {
+ dragData: DragManager.DocumentDragData.name,
+ value: 'any',
+ _readOnly_: 'boolean',
+ scriptContext: 'any',
+ thisContainer: Doc.name,
+ documentView: Doc.name,
+ heading: Doc.name,
+ checked: 'boolean',
+ containingTreeView: Doc.name,
+ });
}
});
funcs &&
@@ -1444,8 +1484,16 @@ export namespace DocUtils {
return created;
}
- export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise<Opt<Doc>> {
- let ctor: ((path: string, options: DocumentOptions) => Doc | Promise<Doc | undefined>) | undefined = undefined;
+ /**
+ *
+ * @param type the type of file.
+ * @param path the path to the file.
+ * @param options the document options.
+ * @param overwriteDoc the placeholder loading doc.
+ * @returns
+ */
+ export async function DocumentFromType(type: string, path: string, options: DocumentOptions, overwriteDoc?: Doc): Promise<Opt<Doc>> {
+ let ctor: ((path: string, options: DocumentOptions, overwriteDoc?: Doc) => Doc | Promise<Doc | undefined>) | undefined = undefined;
if (type.indexOf('image') !== -1) {
ctor = Docs.Create.ImageDocument;
if (!options._width) options._width = 300;
@@ -1494,7 +1542,7 @@ export namespace DocUtils {
options = { ...options, _width: 400, _height: 512, title: path };
}
- return ctor ? ctor(path, options) : undefined;
+ return ctor ? ctor(path, options, overwriteDoc) : undefined;
}
export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void {
@@ -1525,7 +1573,7 @@ export namespace DocUtils {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
.filter(btnDoc => !btnDoc.hidden)
.map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
- .filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation)
+ .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail)
.map((dragDoc, i) => ({
description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
event: undoBatch((args: { x: number; y: number }) => {
@@ -1710,14 +1758,14 @@ export namespace DocUtils {
return dd;
}
- async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) {
+ async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) {
if (result instanceof Error) {
alert(`Upload failed: ${result.message}`);
return;
}
const full = { ...options, _width: 400, title: name };
const pathname = Utils.prepend(result.accessPaths.agnostic.client);
- const doc = await DocUtils.DocumentFromType(type, pathname, full);
+ const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
if (doc) {
const proto = Doc.GetProto(doc);
proto.text = result.rawText;
@@ -1746,6 +1794,12 @@ export namespace DocUtils {
proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
}
}
+ if (Upload.isVideoInformation(result)) {
+ proto['data-duration'] = result.duration;
+ }
+ if (rootDoc) {
+ Doc.removeCurrentlyLoading(rootDoc);
+ }
generatedDocuments.push(doc);
}
}
@@ -1774,16 +1828,23 @@ export namespace DocUtils {
return tbox;
}
- export async function uploadYoutubeVideo(videoId: string, options: DocumentOptions) {
+ export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
const generatedDocuments: Doc[] = [];
- for (const {
- source: { name, type },
- result,
- } of await Networking.UploadYoutubeToServer(videoId)) {
- name && type && processFileupload(generatedDocuments, name, type, result, options);
- }
- return generatedDocuments;
+ Networking.UploadYoutubeToServer(videoId).then(upfiles => {
+ const {
+ source: { name, type },
+ result,
+ } = upfiles.lastElement();
+ if ((result as any).message) {
+ if (overwriteDoc) {
+ overwriteDoc.isLoading = false;
+ overwriteDoc.loadingError = (result as any).message;
+ Doc.removeCurrentlyLoading(overwriteDoc);
+ }
+ } else name && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+ });
}
+
export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
const generatedDocuments: Doc[] = [];
const upfiles = await Networking.UploadFilesToServer(files);
@@ -1796,6 +1857,22 @@ export namespace DocUtils {
return generatedDocuments;
}
+ export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
+ const generatedDocuments: Doc[] = [];
+ Networking.UploadFilesToServer([file]).then(upfiles => {
+ const {
+ source: { name, type },
+ result,
+ } = upfiles.lastElement() ?? { source: { name: '', type: '' }, result: { message: 'upload failed' } };
+ if ((result as any).message) {
+ if (overwriteDoc) {
+ overwriteDoc.loadingError = (result as any).message;
+ Doc.removeCurrentlyLoading(overwriteDoc);
+ }
+ } else name && type && processFileupload(generatedDocuments, name, type, result, options, overwriteDoc);
+ });
+ }
+
// copies the specified drag factory document
export function copyDragFactory(dragFactory: Doc) {
if (!dragFactory) return undefined;
@@ -1832,8 +1909,8 @@ ScriptingGlobals.add(function makeDelegate(proto: any) {
return d;
});
ScriptingGlobals.add(function generateLinkTitle(self: Doc) {
- const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null).title : '<?>';
- const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : '<?>';
+ const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null)?.title : '<?>';
+ const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null)?.title : '<?>';
const relation = self.linkRelationship || 'to';
return `${anchor1title} (${relation}) ${anchor2title}`;
});
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 012ee163c..dd11e6466 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -3263,12 +3263,6 @@
const canDelete = rowOrCol && !rowOrCol.isRoot && (rowOrCol.contentItems.length > 1 || (parRowOrCol && parRowOrCol.contentItems.length > 1)); // bcz: added test for last stack
if (canDelete) {
rowOrCol.removeChild(stack);
- if (rowOrCol.contentItems.length === 1 && parRowOrCol.contentItems.length === 1 && !parRowOrCol.isRoot) {
-
- saveScrollTops(rowOrCol.contentItems[0].element);
- parRowOrCol.replaceChild(rowOrCol, rowOrCol.contentItems[0]);
- restoreScrollTops(rowOrCol.contentItems[0].element);
- }
}
}
},
@@ -4062,6 +4056,14 @@
lm.items.AbstractContentItem.prototype.removeChild.call(this, contentItem, keepChild);
if (this.contentItems.length === 1 && this.config.isClosable === true) {
+ if (["row","column"].includes(this.contentItems[0].type) || ["row","column"].includes(this.parent.type)) {
+ let parent = this.parent;
+ let correctRowOrCol = this.contentItems[0];
+ saveScrollTops(correctRowOrCol.element);
+ parent.replaceChild(this, correctRowOrCol);
+ restoreScrollTops(correctRowOrCol.element);
+ }
+
// bcz: this has the effect of removing children from the DOM and then re-adding them above where they were before.
// in the case of things like an iFrame with a YouTube video, the video will reload for now reason. So let's try leaving these "empty" rows alone.
// childItem = this.contentItems[0];
@@ -4724,15 +4726,35 @@
*/
} else {
type = isVertical ? 'column' : 'row';
- rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
- this.parent.replaceChild(this, rowOrColumn);
+ if (this.parent.contentItems.length === 1) {
+ let grandparent = this.parent.parent;
+ let correctRowOrCol = this.layoutManager.createContentItem({ type: type }, this);
+ grandparent.replaceChild(this.parent, correctRowOrCol);
+ correctRowOrCol.addChild(contentItem, 0, true);
+ let newstack = this.contentItems[0];
+ if (newstack.isComponent) {
+ newstack = this.layoutManager.createContentItem({
+ type: 'stack',
+ header: contentItem.config.header || {}
+ }, this);
+ newstack._$init();
+ newstack.addChild(this.contentItems[0]);
+ }
+ correctRowOrCol.addChild(newstack, insertBefore ? 0 : undefined, true);
+ newstack.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ correctRowOrCol.callDownwards('setSize');
+ } else {
+ rowOrColumn = this.layoutManager.createContentItem({ type: type }, this);
+ this.parent.replaceChild(this, rowOrColumn);
- rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
- rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
+ rowOrColumn.addChild(contentItem, insertBefore ? 0 : undefined, true);
+ rowOrColumn.addChild(this, insertBefore ? undefined : 0, true);
- this.config[dimension] = 50;
- contentItem.config[dimension] = 50;
- rowOrColumn.callDownwards('setSize');
+ this.config[dimension] = 50;
+ contentItem.config[dimension] = 50;
+ rowOrColumn.callDownwards('setSize');
+ }
}
},
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 99a8c895f..cd2ea4c87 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,12 +1,14 @@
+import { forOwn } from "lodash";
import { reaction } from "mobx";
import * as rp from 'request-promise';
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
import { Id } from "../../fields/FieldSymbols";
+import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
-import { ScriptField } from "../../fields/ScriptField";
+import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
import { nullAudio } from "../../fields/URLField";
import { SetCachedGroups, SharingPermissions } from "../../fields/util";
@@ -143,7 +145,7 @@ export class CurrentUserUtils {
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", width:200, system: true};
+ const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
@@ -154,7 +156,7 @@ export class CurrentUserUtils {
/// Initializes collection of templates for notes and click functions
static setupDocTemplates(doc: Doc, field="myTemplates") {
- DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(opts), { title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"});
+ DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(), { });
const templates = [
CurrentUserUtils.setupNoteTemplates(doc),
CurrentUserUtils.setupClickEditorTemplates(doc)
@@ -196,6 +198,7 @@ export class CurrentUserUtils {
makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}),
makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
+ makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.VID, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.BUTTON,"data", { iconTemplate:DocumentType.FONTICON}),
//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
@@ -205,7 +208,7 @@ export class CurrentUserUtils {
DocUtils.AssignOpts(DocCast(doc[field]), {}, iconTemplates);
}
- /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyPresentation
+ /// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyTrail
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},
@@ -267,7 +270,7 @@ export class CurrentUserUtils {
{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, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _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,
@@ -321,16 +324,17 @@ export class CurrentUserUtils {
/// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents
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())";
+ const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
+ const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails";
return [
- { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", },
+ { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} },
{ 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: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}},
- { title: "Trails", target: this.setupTrails(doc, "myTrails"), icon: "pres-trail", },
+ { title: "Trails", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}},
{ title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}}));
}
@@ -458,28 +462,33 @@ export class CurrentUserUtils {
static setupDashboards(doc: Doc, field:string) {
var myDashboards = DocCast(doc[field]);
+ const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`;
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,}
const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
+ const contextMenuScripts = [/*newDashboard*/] as string[];
+ const contextMenuLabels = [/*"Create New Dashboard"*/] as string[];
+ const contextMenuIcons = [/*"plus"*/] as string[];
+ const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)', 'resetDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
+ const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
+ const childContextMenuLabels = ["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
+ const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
const reqdOpts:DocumentOptions = {
title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true,
targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true, treeViewTruncateTitleWidth: 150, ignoreClick: true,
buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias",
_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
- contextMenuLabels: new List<string>(["Create New Dashboard"]),
- contextMenuIcons: new List<string>(["plus"]),
- childContextMenuLabels: new List<string>(["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard"]),// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
- childContextMenuIcons: new List<string>(["chalkboard", "tv", "camera", "users", "times"]), // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
+ contextMenuLabels:new List<string>(contextMenuLabels),
+ contextMenuIcons:new List<string>(contextMenuIcons),
+ childContextMenuLabels:new List<string>(childContextMenuLabels),
+ childContextMenuIcons:new List<string>(childContextMenuIcons),
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 = DocUtils.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
- const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
myDashboards.contextMenuScripts = new List<ScriptField>(contextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
}
@@ -492,31 +501,6 @@ export class CurrentUserUtils {
return myDashboards;
}
- /// initializes the left sidebar Trails pane
- static setupTrails(doc: Doc, field:string) {
- var myTrails = DocCast(doc[field]);
- const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true,
- title: "New trail", toolTip: "Create new trail", btnType: ButtonType.ClickButton, buttonText: "New trail", icon: "plus", system: true };
- const reqdBtnScript = {onClick: `createNewPresentation()`};
- const newTrailButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myTrails?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
-
- const reqdOpts:DocumentOptions = {
- title: "My Trails", _showTitle: "title", _height: 100,
- treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
- treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
- contextMenuIcons: new List<string>(["plus"]),
- contextMenuLabels: new List<string>(["Create New Trail"]),
- _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
- explainer: "All of the trails that you have created will appear here."
- };
- myTrails = DocUtils.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]);
@@ -597,14 +581,17 @@ export class CurrentUserUtils {
/// 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 = (opts: DocumentOptions, scripts: {[key:string]:string}) =>
+ const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string}, funcs?: {[key:string]:string}) =>
DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ??
- CurrentUserUtils.createToolButton(opts), scripts);
+ CurrentUserUtils.createToolButton(opts), scripts, funcs);
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
{ 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" }}
- ];
+ { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
+ // { scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, opts: { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, width: 20}},
+ // { scripts: { onClick:""}, opts: { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, width: 20}, funcs: { buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}},
+ // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, width: 20,} },
+ ];
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,
@@ -619,8 +606,8 @@ export class CurrentUserUtils {
return [
{ 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: "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_);}'} },
@@ -677,15 +664,15 @@ export class CurrentUserUtils {
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
{ title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_)'}},
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}, width: 20, scripts: {}},
+ { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: {}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
{ title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
{ title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Text", icon: "text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
- { title: "Ink", icon: "ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', inearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`} }, // Always available
- { title: "Web", icon: "web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected
- { title: "Schema", icon: "schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+ { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected
+ { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
];
}
@@ -717,13 +704,13 @@ export class CurrentUserUtils {
return this.setupContextMenuButton(params, menuBtnDoc);
} else {
const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: true,
+ childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
linearViewSubMenu: true, linearViewExpandable: true, };
const items = params.subMenu?.map(sub =>
this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
);
return DocUtils.AssignScripts(
- DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), undefined, params.funcs);
+ DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), params.scripts, params.funcs);
}
});
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
@@ -768,7 +755,7 @@ export class CurrentUserUtils {
childContextMenuLabels: new List<string>(["Add to Dashboards",]),
childContextMenuIcons: new List<string>(["user-plus",]),
"acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15,
+ childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15,
// NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar
_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
@@ -952,6 +939,8 @@ ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "docu
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 createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
+ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "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)");
ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
+ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); \ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 52b643c04..50190061a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -154,7 +154,7 @@ export class DocumentManager {
CollectionDockingView.AddSplit(doc, 'right');
finished?.();
};
- public jumpToDocument = async (
+ public jumpToDocument = (
targetDoc: Doc, // document to display
willZoom: boolean, // whether to zoom doc to take up most of screen
createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
@@ -165,13 +165,13 @@ export class DocumentManager {
finished?: () => void,
originalTarget?: Doc,
noSelect?: boolean,
- presZoom?: number
- ): Promise<void> => {
+ presZoomScale?: number
+ ): void => {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
const docView = getFirstDocView(targetDoc, originatingDoc);
const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
- const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? targetDoc : targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
+ const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
var wasHidden = resolvedTarget.hidden;
if (wasHidden) {
runInAction(() => {
@@ -207,7 +207,7 @@ export class DocumentManager {
finished?.();
};
const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
- const contextDocs = docContext.length ? await DocListCastAsync(docContext[0].data) : undefined;
+ const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
const targetDocContext = contextDoc || annotatedDoc;
const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
@@ -218,7 +218,7 @@ export class DocumentManager {
annoContainerView.focus(targetDoc, {
originalTarget,
willZoom,
- scale: presZoom,
+ scale: presZoomScale,
afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
focusAndFinish(true);
@@ -228,7 +228,7 @@ export class DocumentManager {
);
return;
} else if (!docView && targetDoc.type !== DocumentType.MARKER) {
- annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
+ annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
}
}
if (focusView) {
@@ -237,7 +237,7 @@ export class DocumentManager {
focusView.focus(originalTarget ?? targetDoc, {
originalTarget,
willZoom,
- scale: presZoom,
+ scale: presZoomScale,
afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
focusAndFinish(forceDidFocus || didFocus);
@@ -265,7 +265,9 @@ export class DocumentManager {
afterFocus: async () => {
targetDocContext._viewTransition = undefined;
if (targetDocContext.layoutKey === 'layout_icon') {
- targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom));
+ targetDocContextView.iconify(() =>
+ this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
+ );
}
return ViewAdjustment.doNothing;
},
@@ -309,7 +311,7 @@ export class DocumentManager {
const docContextView = this.getFirstDocumentView(docContext[0]);
if (docContextView) {
return docContextView.iconify(() =>
- this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoom)
+ this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
);
}
}
@@ -335,7 +337,7 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
const showDoc = context || doc;
const bestAlias = showDoc === Doc.GetProto(showDoc) ? DocListCast(showDoc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : showDoc;
- CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc));
+ CollectionDockingView.AddSplit(bestAlias ? bestAlias : Doc.MakeAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
}
}
ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 6386c87a0..664933de0 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -344,8 +344,7 @@ export namespace DragManager {
}
Object.assign(dragDiv.style, { width: '', height: '', overflow: '' });
dragDiv.hidden = false;
- const scaleXs: number[] = [],
- scaleYs: number[] = [],
+ const scalings: number[] = [],
xs: number[] = [],
ys: number[] = [];
@@ -355,8 +354,18 @@ export namespace DragManager {
top: Number.MAX_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
};
+ let rot = 0;
const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const dragElements = eles.map(ele => {
+ let useDim = false;
+ if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
+ ele = ele.parentElement.parentElement.parentElement;
+ const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1');
+ if (rotStr) rot = Number(rotStr);
+ } else {
+ useDim = true;
+ }
+ if (rot < 0) rot += 360;
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
const children = Array.from(dragElement.children);
@@ -376,17 +385,28 @@ export namespace DragManager {
}
}
const rect = ele.getBoundingClientRect();
- const scaleX = rect.width / (ele.offsetWidth || rect.width);
- const scaleY = scaleX; //ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
+ const w = ele.offsetWidth || rect.width;
+ const h = ele.offsetHeight || rect.height;
+ const rotR = -(rot / 180) * Math.PI;
+ const tl = [0, 0];
+ const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
+ const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
+ const br = [Math.cos(rotR) * w + Math.sin(rotR) * h, Math.cos(-rotR) * h - Math.sin(rotR) * w];
+ const minx = Math.min(tl[0], tr[0], br[0], bl[0]);
+ const maxx = Math.max(tl[0], tr[0], br[0], bl[0]);
+ const miny = Math.min(tl[1], tr[1], br[1], bl[1]);
+ const maxy = Math.max(tl[1], tr[1], br[1], bl[1]);
+ const scaling = rect.width / (Math.abs(maxx - minx) || 1);
elesCont.left = Math.min(rect.left, elesCont.left);
elesCont.top = Math.min(rect.top, elesCont.top);
elesCont.right = Math.max(rect.right, elesCont.right);
elesCont.bottom = Math.max(rect.bottom, elesCont.bottom);
- xs.push(rect.left + (options?.offsetX || 0));
- ys.push(rect.top + (options?.offsetY || 0));
- scaleXs.push(scaleX);
- scaleYs.push(scaleY);
+ xs.push(((0 - minx) / (maxx - minx)) * rect.width + rect.left);
+ ys.push(((0 - miny) / (maxy - miny)) * rect.height + rect.top);
+ scalings.push(scaling);
+ const width = useDim ? getComputedStyle(ele).width : '';
+ const height = useDim ? getComputedStyle(ele).height : '';
Object.assign(dragElement.style, {
opacity: '0.7',
position: 'absolute',
@@ -399,9 +419,9 @@ export namespace DragManager {
borderRadius: getComputedStyle(ele).borderRadius,
zIndex: globalCssVariables.contextMenuZindex,
transformOrigin: '0 0',
- width: `${rect.width / scaleX}px`,
- height: `${rect.height / scaleY}px`,
- transform: `translate(${xs[0]}px, ${ys[0]}px) scale(${scaleX}, ${scaleY})`,
+ width,
+ height,
+ transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
});
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
@@ -542,7 +562,7 @@ export namespace DragManager {
const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
lastPt = { x, y };
- dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`));
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`));
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
};
const upHandler = (e: PointerEvent) => {
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 4af51b9a0..85700da37 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -230,58 +230,33 @@ export namespace InteractionUtils {
points.push({ X: right, Y: bottom });
points.push({ X: left, Y: bottom });
points.push({ X: left, Y: top });
+ break;
case 'triangle':
- // points.push({ X: left, Y: bottom });
- // points.push({ X: right, Y: bottom });
- // points.push({ X: (right + left) / 2, Y: top });
- // points.push({ X: left, Y: bottom });
-
- points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
-
- points.push({ X: right, Y: bottom });
- points.push({ X: right, Y: bottom });
points.push({ X: right, Y: bottom });
- points.push({ X: right, Y: bottom });
-
- points.push({ X: (right + left) / 2, Y: top });
- points.push({ X: (right + left) / 2, Y: top });
points.push({ X: (right + left) / 2, Y: top });
- points.push({ X: (right + left) / 2, Y: top });
-
- points.push({ X: left, Y: bottom });
points.push({ X: left, Y: bottom });
+ break;
case 'circle':
const centerX = (Math.max(left, right) + Math.min(left, right)) / 2;
const centerY = (Math.max(top, bottom) + Math.min(top, bottom)) / 2;
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
- if (centerX - Math.min(left, right) < centerY - Math.min(top, bottom)) {
- for (var y = Math.min(top, bottom); y < Math.max(top, bottom); y++) {
- const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
- points.push({ X: x, Y: y });
- }
- for (var y = Math.max(top, bottom); y > Math.min(top, bottom); y--) {
- const x = Math.sqrt(Math.pow(radius, 2) - Math.pow(y - centerY, 2)) + centerX;
- const newX = centerX - (x - centerX);
- points.push({ X: newX, Y: y });
- }
- points.push({ X: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(top, bottom) - centerY, 2)) + centerX, Y: Math.min(top, bottom) });
- } else {
- for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
- const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
- points.push({ X: x, Y: y });
- }
- for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
- const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
- const newY = centerY - (y - centerY);
- points.push({ X: x, Y: newY });
- }
- points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
+ for (var x = Math.min(left, right); x < Math.max(left, right); x++) {
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
+ points.push({ X: x, Y: y });
}
+ for (var x = Math.max(left, right); x > Math.min(left, right); x--) {
+ const y = Math.sqrt(Math.pow(radius, 2) - Math.pow(x - centerX, 2)) + centerY;
+ const newY = centerY - (y - centerY);
+ points.push({ X: x, Y: newY });
+ }
+ points.push({ X: Math.min(left, right), Y: Math.sqrt(Math.pow(radius, 2) - Math.pow(Math.min(left, right) - centerX, 2)) + centerY });
+ break;
+
case 'line':
points.push({ X: left, Y: top });
points.push({ X: right, Y: bottom });
- return points;
+ break;
}
return points;
}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
index 55c5ca87f..51742d455 100644
--- a/src/client/util/ReportManager.tsx
+++ b/src/client/util/ReportManager.tsx
@@ -57,7 +57,7 @@ export class ReportManager extends React.Component<{}> {
ReportManager.Instance = this;
this.octokit = new Octokit({
- auth: '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'
+ auth: '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'
});
}
@@ -104,6 +104,20 @@ export class ReportManager extends React.Component<{}> {
}
}
+ // turns an upload link into a servable link
+ // ex:
+ // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
+ // -> http://localhost:1050/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
+ private fileLinktoServerLink = (fileLink: string) => {
+ const serverUrl = 'https://browndash.com/';
+
+ const regex = 'public'
+ const publicIndex = fileLink.indexOf(regex) + regex.length;
+
+ const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
+ return finalUrl;
+ }
+
public async reportIssue() {
if (this.bugTitle === '' || this.bugDescription === ''
|| this.bugType === '' || this.bugPriority === '') {
@@ -111,14 +125,15 @@ export class ReportManager extends React.Component<{}> {
return;
}
-
if (this.toGithub) {
+ const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink)
+
const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
owner: 'brown-dash',
repo: 'Dash-Web',
title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
- body: `${this.bugDescription} \n\nfiles:\n${(this.fileLinks ?? []).join('\n')}`,
+ body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
labels: [
'from-dash-app',
this.bugType,
@@ -140,7 +155,7 @@ export class ReportManager extends React.Component<{}> {
// if we're down here, then we're good to go. reset the fields.
this.setBugTitle('');
this.setBugDescription('');
- this.toGithub = false;
+ // this.toGithub = false;
this.setFileLinks([]);
this.setBugType('');
this.setBugPriority('');
@@ -176,10 +191,10 @@ export class ReportManager extends React.Component<{}> {
(<div className="settings-content">
<h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
<label>Please leave a title for the bug.</label><br />
- <input type="text" placeholder='title' onChange={(e) => this.bugTitle = e.target.value} required/>
+ <input type="text" placeholder='title' onChange={(e) => this.setBugTitle(e.target.value)} required/>
<br />
<label>Please leave a description for the bug and how it can be recreated.</label>
- <textarea placeholder='description' onChange={(e) => this.bugDescription = e.target.value} required/>
+ <textarea placeholder='description' onChange={(e) => this.setBugDescription(e.target.value)} required/>
<br />
{/* {<label>Send to github issues? </label>
<input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
@@ -187,14 +202,14 @@ export class ReportManager extends React.Component<{}> {
<label>Please label the issue</label>
<div className='flex-select'>
- <select name="bugType">
+ <select name="bugType" onChange={e => this.bugType = e.target.value}>
<option value="" disabled selected>Type</option>
<option value="bug">Bug</option>
<option value="cosmetic">Poor Design or Cosmetic</option>
<option value="documentation">Poor Documentation</option>
</select>
- <select name="bigPriority">
+ <select name="bigPriority" onChange={e => this.bugPriority = e.target.value}>
<option value="" disabled selected>Priority</option>
<option value="priority-low">Low</option>
<option value="priority-medium">Medium</option>
@@ -205,7 +220,7 @@ export class ReportManager extends React.Component<{}> {
<div>
<label>Upload media that shows the bug (optional)</label>
- <input type="file" name="file" multiple accept='audio/*, video/*' onChange={e => this.uploadFiles(e.target)}/>
+ <input type="file" name="file" multiple accept='audio/*, video/*, image/*' onChange={e => this.uploadFiles(e.target)}/>
</div>
<br />
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index ea2bf6551..6dcdcb71b 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,6 +7,8 @@
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import * as ts from 'typescript';
import { Doc, Field } from '../../fields/Doc';
+import { ObjectField } from '../../fields/ObjectField';
+import { ScriptField } from '../../fields/ScriptField';
import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
@@ -177,6 +179,10 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
}
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
+ const captured = options.capturedVariables ?? {};
+ const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, '');
+ const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
+ if (found) return found as CompiledScript;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
if (options.params && !options.params.this) options.params.this = Doc.name;
if (options.params && !options.params.self) options.params.self = Doc.name;
@@ -241,6 +247,8 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (options.globals) {
ScriptingGlobals.resetScriptingGlobals();
}
+ !signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
+ //console.log('COMPILED: ' + script + ':' + signature);
return result;
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 7a555d5f8..a3d6f5227 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -91,7 +91,7 @@ export namespace SelectionManager {
}
export function Views(): Array<DocumentView> {
- return Array.from(manager.SelectedViews.keys()).filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+ return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
}
export function SelectedSchemaDoc(): Doc | undefined {
return manager.SelectedSchemaDocument;
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index cf143c5e8..a185c8936 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -30,7 +30,7 @@ export enum ColorScheme {
export class SettingsManager extends React.Component<{}> {
public static Instance: SettingsManager;
static _settingsStyle = addStyleSheet();
- @observable private isOpen = false;
+ @observable public isOpen = false;
@observable private passwordResultText = '';
@observable private playgroundMode = false;
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 2de636f21..932e94664 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -107,7 +107,7 @@
.user-sort {
text-align: left;
margin-left: 10;
- width: 100px;
+ width: 100%;
cursor: pointer;
}
@@ -122,6 +122,7 @@
background: #e8e8e8;
padding-left: 10px;
padding-right: 10px;
+ width: 100%;
overflow-y: scroll;
overflow-x: hidden;
text-align: left;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 895bd3374..4b0310e76 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -374,7 +374,7 @@ export class SharingManager extends React.Component<{}> {
if (!uniform) dropdownValues.unshift('-multiple-');
if (override) dropdownValues.unshift('None');
return dropdownValues
- .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any))
+ .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
.map(permission => (
<option key={permission} value={permission}>
{permission}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.js
index beb030635..502e0fbac 100644
--- a/src/client/util/request-image-size.js
+++ b/src/client/util/request-image-size.js
@@ -15,15 +15,18 @@ const HttpError = require('standard-http-error');
module.exports = function requestImageSize(options) {
let opts = {
- encoding: null
+ encoding: null,
};
if (options && typeof options === 'object') {
opts = Object.assign(options, opts);
} else if (options && typeof options === 'string') {
- opts = Object.assign({
- uri: options
- }, opts);
+ opts = Object.assign(
+ {
+ uri: options,
+ },
+ opts
+ );
} else {
return Promise.reject(new Error('You should provide an URI string or a "request" options object.'));
}
@@ -38,9 +41,8 @@ module.exports = function requestImageSize(options) {
return reject(new HttpError(res.statusCode, res.statusMessage));
}
- let buffer = new Buffer.from([]);
+ let buffer = Buffer.from([]);
let size;
- let imageSizeError;
res.on('data', chunk => {
buffer = Buffer.concat([buffer, chunk]);
@@ -48,8 +50,8 @@ module.exports = function requestImageSize(options) {
try {
size = imageSize(buffer);
} catch (err) {
- imageSizeError = err;
- return;
+ reject(err);
+ return req.abort();
}
if (size) {
@@ -58,11 +60,11 @@ module.exports = function requestImageSize(options) {
}
});
- res.on('error', err => reject(err));
+ res.on('error', reject);
res.on('end', () => {
if (!size) {
- return reject(imageSizeError);
+ return reject(new Error('Image has no size'));
}
size.downloaded = buffer.length;
@@ -70,6 +72,6 @@ module.exports = function requestImageSize(options) {
});
});
- req.on('error', err => reject(err));
+ req.on('error', reject);
});
-}; \ No newline at end of file
+};
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index 1e6a377de..cbe14060a 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -3,7 +3,7 @@
.contextMenu-cont {
position: absolute;
display: flex;
- z-index: 100000;
+ z-index: $contextMenu-zindex;
box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
flex-direction: column;
background: whitesmoke;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index c9908b4b0..6a530e3ae 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -118,27 +118,11 @@ export class ContextMenu extends React.Component {
static readonly buffer = 20;
get pageX() {
- const x = this._pageX;
- if (x < 0) {
- return 0;
- }
- const width = this._width;
- if (x + width > window.innerWidth - ContextMenu.buffer) {
- return window.innerWidth - ContextMenu.buffer - width;
- }
- return x;
+ return this._pageX + this._width > window.innerWidth - ContextMenu.buffer ? window.innerWidth - ContextMenu.buffer - this._width : Math.max(0, this._pageX);
}
get pageY() {
- const y = this._pageY;
- if (y < 0) {
- return 0;
- }
- const height = this._height;
- if (y + height > window.innerHeight - ContextMenu.buffer) {
- return window.innerHeight - ContextMenu.buffer - height;
- }
- return y;
+ return this._pageY + this._height > window.innerHeight - ContextMenu.buffer ? window.innerHeight - ContextMenu.buffer - this._height : Math.max(0, this._pageY);
}
_onDisplay?: () => void = undefined;
@@ -223,7 +207,15 @@ export class ContextMenu extends React.Component {
render() {
return !this._display ? null : (
- <div className="contextMenu-cont" style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}>
+ <div
+ className="contextMenu-cont"
+ ref={action((r: any) => {
+ if (r) {
+ this._width = Number(getComputedStyle(r).width.replace('px', ''));
+ this._height = Number(getComputedStyle(r).height.replace('px', ''));
+ }
+ })}
+ style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}>
{!this.itemsNeedSearch ? null : (
<span className={'search-icon'}>
<span className="icon-background">
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index 3db23b86f..b8a6f6c05 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -1,3 +1,6 @@
+@import "./global/globalCssVariables";
+
+
.dashboard-view {
padding: 50px;
display: flex;
@@ -7,8 +10,10 @@
.left-menu {
display: flex;
+ justify-content: flex-start;
flex-direction: column;
- width: 300px;
+ width: 250px;
+ min-width: 250px;
}
.all-dashboards {
@@ -20,7 +25,8 @@
}
.text-button {
- padding: 10px 0;
+ cursor: pointer;
+ padding: 3px 0;
&:hover {
font-weight: 500;
}
@@ -30,17 +36,46 @@
}
}
+.new-dashboard-button {
+ font-weight: 600;
+ padding-bottom: 10px;
+}
+
+.dashboard-container-new {
+ border-radius: 10px;
+ width: 250px;
+ height: 200px;
+ font-size: 120px;
+ font-weight: 100;
+ text-align: center;
+ border: solid 2px $light-gray;
+ margin: 0 0px 30px 30px;
+ cursor: pointer;
+ color: $light-gray;
+ display: flex;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ color: $light-blue;
+ border: solid 2px $light-blue;
+ }
+}
+
.dashboard-container {
border-radius: 10px;
+ cursor: pointer;
width: 250px;
- border: solid .5px grey;
+ height: 200px;
+ outline: solid 2px $light-gray;
display: flex;
flex-direction: column;
- margin: 0 30px 30px 30px;
+ margin: 0 0px 30px 30px;
overflow: hidden;
- &:hover {
- border: solid 1.5px grey;
+ &:hover{
+ outline: solid 2px $light-blue;
}
.title {
@@ -58,9 +93,47 @@
flex-direction: row;
justify-content: space-between;
align-items: center;
+ padding: 0px 10px;
}
.more {
z-index: 100;
}
+}
+
+.new-dashboard {
+ color: $dark-gray;
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+ justify-content: space-between;
+
+ .header {
+ font-size: 1.5em;
+ font-weight: 600;
+ }
+
+ .title-input,
+ .color-picker {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ font-weight: 500;
+ gap: 5px;
+ z-index: 5;
+
+ .input {
+ border-radius: 10px;
+ padding: 5px 10px;
+ }
+ }
+
+ .button-bar {
+ display: flex;
+ gap: 5px;
+ flex-direction: row;
+ justify-content: flex-end;
+ }
} \ No newline at end of file
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 84b1017b4..192e55431 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,13 +1,17 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { DataSym, Doc, DocListCast, DocListCastAsync } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
-import { Cast, ImageCast, StrCast } from '../../fields/Types';
+import { PrefetchProxy } from '../../fields/Proxy';
+import { listSpec } from '../../fields/Schema';
+import { ScriptField } from '../../fields/ScriptField';
+import { Cast, DocCast, ImageCast, StrCast } from '../../fields/Types';
import { DocServer } from '../DocServer';
-import { Docs, DocumentOptions } from '../documents/Documents';
+import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { CollectionViewType } from '../documents/DocumentTypes';
import { HistoryUtil } from '../util/History';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
@@ -17,7 +21,10 @@ import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionView } from './collections/CollectionView';
import { ContextMenu } from './ContextMenu';
import './DashboardView.scss';
+import { Colors } from './global/globalEnums';
import { MainViewModal } from './MainViewModal';
+import { ButtonType } from './nodes/button/FontIconBox';
+import { FaPlus } from 'react-icons/fa';
enum DashboardGroup {
MyDashboards,
@@ -35,6 +42,7 @@ export class DashboardView extends React.Component {
@observable private selectedDashboardGroup = DashboardGroup.MyDashboards;
@observable private newDashboardName: string | undefined = undefined;
+ @observable private newDashboardColor: string | undefined = undefined;
@action abortCreateNewDashboard = () => {
this.newDashboardName = undefined;
};
@@ -47,12 +55,10 @@ export class DashboardView extends React.Component {
this.selectedDashboardGroup = group;
};
- clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => {
- if (e.detail === 2) {
- Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
- Doc.ActiveDashboard = dashboard;
- Doc.ActivePage = 'dashboard';
- }
+ clickDashboard = (e: React.MouseEvent, dashboard: Doc) => {
+ Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
+ Doc.ActiveDashboard = dashboard;
+ Doc.ActivePage = 'dashboard';
};
getDashboards = () => {
@@ -76,26 +82,45 @@ export class DashboardView extends React.Component {
};
@undoBatch
- createNewDashboard = async (name: string) => {
- DashboardView.createNewDashboard(undefined, name);
- this.abortCreateNewDashboard();
+ createNewDashboard = async (name: string, background?: string) => {
+ setTimeout(() => {
+ this.abortCreateNewDashboard();
+ }, 100);
+ DashboardView.createNewDashboard(undefined, name, background);
};
@computed
get namingInterface() {
+ const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1;
+ const placeholder = `Dashboard ${dashboardCount}`;
return (
- <div>
- <input className="password-inputs" placeholder="Untitled Dashboard" onChange={e => this.setNewDashboardName((e.target as any).value)} />
- <button className="password-submit" onClick={this.abortCreateNewDashboard}>
- Cancel
- </button>
- <button
- className="password-submit"
- onClick={() => {
- this.createNewDashboard(this.newDashboardName!);
- }}>
- Create
- </button>
+ <div className="new-dashboard">
+ <div className="header">Create New Dashboard</div>
+ <div className="title-input">
+ Title
+ <input className="input" placeholder={placeholder} onChange={e => this.setNewDashboardName((e.target as any).value)} />
+ </div>
+ <div className="color-picker">
+ Background
+ <ColorPicker
+ onChange={color => {
+ this.newDashboardColor = color;
+ }}
+ />
+ </div>
+ <div className="button-bar">
+ <Button text="Cancel" borderRadius={10} hoverStyle={'gray'} fontSize={FontSize.SECONDARY} onClick={this.abortCreateNewDashboard} />
+ <Button
+ text="Create"
+ borderRadius={10}
+ backgroundColor={Colors.LIGHT_BLUE}
+ hoverStyle={'darken'}
+ fontSize={FontSize.SECONDARY}
+ onClick={() => {
+ this.createNewDashboard(this.newDashboardName!, this.newDashboardColor);
+ }}
+ />
+ </div>
</div>
);
}
@@ -137,12 +162,19 @@ export class DashboardView extends React.Component {
<>
<div className="dashboard-view">
<div className="left-menu">
- <div
- className="text-button"
- onClick={() => {
- this.setNewDashboardName('');
- }}>
- New
+ <div className="new-dashboard-button">
+ <Button
+ icon={<FaPlus />}
+ hoverStyle="darken"
+ backgroundColor={Colors.LIGHT_BLUE}
+ size={Size.MEDIUM}
+ fontSize={FontSize.HEADER}
+ text="New"
+ onClick={() => {
+ this.setNewDashboardName('');
+ }}
+ borderRadius={50}
+ />
</div>
<div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}>
My Dashboards
@@ -153,7 +185,7 @@ export class DashboardView extends React.Component {
</div>
<div className="all-dashboards">
{this.getDashboards().map(dashboard => {
- const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href;
+ const href = ImageCast(dashboard.thumb)?.url.href;
return (
<div
className="dashboard-container"
@@ -176,14 +208,23 @@ export class DashboardView extends React.Component {
this._downY = e.clientY;
}}
onClick={e => {
+ e.preventDefault();
+ e.stopPropagation();
this.onContextMenu(dashboard, e);
}}>
- <FontAwesomeIcon color="black" size="lg" icon="bars" />
+ <IconButton isCircle={true} size={Size.SMALL} hoverStyle="gray" icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} />
</div>
</div>
</div>
);
})}
+ <div
+ className="dashboard-container-new"
+ onClick={() => {
+ this.setNewDashboardName('');
+ }}>
+ +
+ </div>
</div>
</div>
<MainViewModal
@@ -191,7 +232,7 @@ export class DashboardView extends React.Component {
isDisplayed={this.newDashboardName !== undefined}
interactive={true}
closeOnExternalClick={this.abortCreateNewDashboard}
- dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }}
/>
</>
);
@@ -253,8 +294,89 @@ export class DashboardView extends React.Component {
}
};
- public static createNewDashboard = (id?: string, name?: string) => {
- const presentation = Doc.MakeCopy(Doc.UserDoc().emptyPresentation as Doc, true);
+ public static resetDashboard = (dashboard: Doc) => {
+ const config = StrCast(dashboard.dockingConfig);
+ const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+ const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? [];
+
+ const components =
+ docids.map(docid => ({
+ type: 'component',
+ component: 'DocumentFrameRenderer',
+ title: 'Untitled Tab 1',
+ width: 600,
+ props: {
+ documentId: docid,
+ },
+ componentName: 'lm-react-component',
+ isClosable: true,
+ reorderEnabled: true,
+ componentState: null,
+ })) ?? [];
+ const reset = {
+ isClosable: true,
+ reorderEnabled: true,
+ title: '',
+ openPopouts: [],
+ maximisedItemId: null,
+ settings: {
+ hasHeaders: true,
+ constrainDragToContainer: true,
+ reorderEnabled: true,
+ selectionEnabled: false,
+ popoutWholeStack: false,
+ blockedPopoutsThrowError: true,
+ closePopoutsOnUnload: true,
+ showPopoutIcon: true,
+ showMaximiseIcon: true,
+ showCloseIcon: true,
+ responsiveMode: 'onload',
+ tabOverlapAllowance: 0,
+ reorderOnTabMenuClick: false,
+ tabControlOffset: 10,
+ },
+ dimensions: {
+ borderWidth: 3,
+ borderGrabWidth: 5,
+ minItemHeight: 10,
+ minItemWidth: 20,
+ headerHeight: 27,
+ dragProxyWidth: 300,
+ dragProxyHeight: 200,
+ },
+ labels: {
+ close: 'close',
+ maximise: 'maximise',
+ minimise: 'minimise',
+ popout: 'new tab',
+ popin: 'pop in',
+ tabDropdown: 'additional tabs',
+ },
+ content: [
+ {
+ type: 'row',
+ isClosable: true,
+ reorderEnabled: true,
+ title: '',
+ content: [
+ {
+ type: 'stack',
+ width: 100,
+ isClosable: true,
+ reorderEnabled: true,
+ title: '',
+ activeItemIndex: 0,
+ content: components,
+ },
+ ],
+ },
+ ],
+ };
+ Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true);
+ return reset;
+ };
+
+ public static createNewDashboard = (id?: string, name?: string, background?: string) => {
const dashboards = Doc.MyDashboards;
const dashboardCount = DocListCast(dashboards.data).length + 1;
const freeformOptions: DocumentOptions = {
@@ -264,6 +386,7 @@ export class DashboardView extends React.Component {
_height: 1000,
_fitWidth: true,
_backgroundGridShow: true,
+ backgroundColor: background,
title: `Untitled Tab 1`,
};
const title = name ? name : `Dashboard ${dashboardCount}`;
@@ -276,13 +399,67 @@ export class DashboardView extends React.Component {
dashboardDoc.data = new List<Doc>(dashboardTabs);
dashboardDoc['pane-count'] = 1;
- Doc.ActivePresentation = presentation;
-
Doc.AddDocToList(dashboards, 'data', dashboardDoc);
+
+ DashboardView.SetupDashboardTrails(dashboardDoc);
+
// open this new dashboard
Doc.ActiveDashboard = dashboardDoc;
Doc.ActivePage = 'dashboard';
+ Doc.ActivePresentation = undefined;
};
+
+ public static SetupDashboardTrails(dashboardDoc: Doc) {
+ // this section is creating the button document itself === myTrails = new Button
+ const reqdBtnOpts: DocumentOptions = {
+ _forceActive: true,
+ _width: 30,
+ _height: 30,
+ _stayInCollection: true,
+ _hideContextMenu: true,
+ title: 'New trail',
+ toolTip: 'Create new trail',
+ btnType: ButtonType.ClickButton,
+ buttonText: 'New trail',
+ icon: 'plus',
+ system: true,
+ };
+ const reqdBtnScript = { onClick: `createNewPresentation()` };
+ const myTrailsBtn = DocUtils.AssignScripts(Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
+
+ // createa a list of presentations (as a tree view collection) and store it on the new dashboard
+ // instead of assigning Doc.UserDoc().myrails we want to assign Doc.AxtiveDashboard.myTrails
+ // but we don't want to create the list of trails here-- but rather in createDashboard
+ const reqdOpts: DocumentOptions = {
+ title: 'My Trails',
+ _showTitle: 'title',
+ _height: 100,
+ treeViewHideTitle: true,
+ _fitWidth: true,
+ _gridGap: 5,
+ _forceActive: true,
+ childDropAction: 'alias',
+ treeViewTruncateTitleWidth: 150,
+ ignoreClick: true,
+ buttonMenu: true,
+ buttonMenuDoc: myTrailsBtn,
+ contextMenuIcons: new List<string>(['plus']),
+ contextMenuLabels: new List<string>(['Create New Trail']),
+ _lockedPosition: true,
+ boxShadow: '0 0',
+ childDontRegisterViews: true,
+ targetDropAction: 'same',
+ system: true,
+ explainer: 'All of the trails that you have created will appear here.',
+ };
+ const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeViewChildDoubleClick: 'openPresentation(documentView.rootDoc)' });
+ dashboardDoc.myTrails = new PrefetchProxy(myTrails);
+
+ 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)!));
+ }
+ }
}
export function AddToList(MySharedDocs: Doc, arg1: string, dash: any) {
@@ -298,6 +475,9 @@ ScriptingGlobals.add(function shareDashboard(dashboard: Doc) {
ScriptingGlobals.add(function removeDashboard(dashboard: Doc) {
DashboardView.removeDashboard(dashboard);
}, 'Remove Dashboard from Dashboards');
+ScriptingGlobals.add(function resetDashboard(dashboard: Doc) {
+ DashboardView.resetDashboard(dashboard);
+}, 'move all dashboard tabs to single stack');
ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
DashboardView.openDashboard(Doc.MakeAlias(dashboard));
}, 'adds Dashboard to set of Dashboards');
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
index f4f96da8a..0bdcdc303 100644
--- a/src/client/views/DictationOverlay.tsx
+++ b/src/client/views/DictationOverlay.tsx
@@ -1,9 +1,8 @@
import { computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import "normalize.css";
import * as React from 'react';
import { DictationManager } from '../util/DictationManager';
-import "./Main.scss";
+import './Main.scss';
import { MainViewModal } from './MainViewModal';
@observer
@@ -29,44 +28,53 @@ export class DictationOverlay extends React.Component {
this.dictationOverlayVisible = false;
this.dictationSuccess = undefined;
DictationOverlay.Instance.hasActiveModal = false;
- setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
+ setTimeout(() => (this.dictatedPhrase = DictationManager.placeholder), 500);
}, duration);
- }
+ };
public cancelDictationFade = () => {
if (this.overlayTimeout) {
clearTimeout(this.overlayTimeout);
this.overlayTimeout = undefined;
}
- }
+ };
- @computed public get dictatedPhrase() { return this._dictationState; }
- @computed public get dictationSuccess() { return this._dictationSuccessState; }
- @computed public get dictationOverlayVisible() { return this._dictationDisplayState; }
- @computed public get isListening() { return this._dictationListeningState; }
+ @computed public get dictatedPhrase() {
+ return this._dictationState;
+ }
+ @computed public get dictationSuccess() {
+ return this._dictationSuccessState;
+ }
+ @computed public get dictationOverlayVisible() {
+ return this._dictationDisplayState;
+ }
+ @computed public get isListening() {
+ return this._dictationListeningState;
+ }
- public set dictatedPhrase(value: string) { runInAction(() => this._dictationState = value); }
- public set dictationSuccess(value: boolean | undefined) { runInAction(() => this._dictationSuccessState = value); }
- public set dictationOverlayVisible(value: boolean) { runInAction(() => this._dictationDisplayState = value); }
- public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); }
+ public set dictatedPhrase(value: string) {
+ runInAction(() => (this._dictationState = value));
+ }
+ public set dictationSuccess(value: boolean | undefined) {
+ runInAction(() => (this._dictationSuccessState = value));
+ }
+ public set dictationOverlayVisible(value: boolean) {
+ runInAction(() => (this._dictationDisplayState = value));
+ }
+ public set isListening(value: DictationManager.Controls.ListeningUIStatus) {
+ runInAction(() => (this._dictationListeningState = value));
+ }
render() {
const success = this.dictationSuccess;
const result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
const dialogueBoxStyle = {
- background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
- borderColor: this.isListening ? "red" : "black",
- fontStyle: "italic"
+ background: success === undefined ? 'gainsboro' : success ? 'lawngreen' : 'red',
+ borderColor: this.isListening ? 'red' : 'black',
+ fontStyle: 'italic',
};
const overlayStyle = {
- backgroundColor: this.isListening ? "red" : "darkslategrey"
+ backgroundColor: this.isListening ? 'red' : 'darkslategrey',
};
- return (<MainViewModal
- contents={result}
- isDisplayed={this.dictationOverlayVisible}
- interactive={false}
- dialogueBoxStyle={dialogueBoxStyle}
- overlayStyle={overlayStyle}
- closeOnExternalClick={this.initiateDictationFade}
- />);
+ return <MainViewModal contents={result} isDisplayed={this.dictationOverlayVisible} interactive={false} dialogueBoxStyle={dialogueBoxStyle} overlayStyle={overlayStyle} closeOnExternalClick={this.initiateDictationFade} />;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 886dd974b..043a83d16 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -3,11 +3,11 @@ import { DateField } from '../../fields/DateField';
import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Opt } from '../../fields/Doc';
import { InkTool } from '../../fields/InkField';
import { List } from '../../fields/List';
-import { ScriptField } from '../../fields/ScriptField';
import { Cast, ScriptCast } from '../../fields/Types';
import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util';
import { returnFalse } from '../../Utils';
import { DocUtils } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
import { InteractionUtils } from '../util/InteractionUtils';
import { UndoManager } from '../util/UndoManager';
import { DocumentView } from './nodes/DocumentView';
@@ -163,7 +163,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
doc.context = undefined;
if (recent) {
Doc.RemoveDocFromList(recent, 'data', doc);
- Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
+ doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
}
});
this.isAnyChildContentActive() && this.props.select(false);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 40fc8dae6..0bfe6e87a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -26,6 +26,7 @@ import { DashFieldView } from './nodes/formattedText/DashFieldView';
import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
import { TemplateMenu } from './TemplateMenu';
import React = require('react');
+import { DocumentType } from '../documents/DocumentTypes';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -234,7 +235,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get pinButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">{SelectionManager.Views().length > 1 ? 'Pin multiple documents to presentation' : 'Pin to presentation'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">{SelectionManager.Views().length > 1 ? 'Pin multiple documents to trail (use shift to pin with view)' : 'Pin to trail (use shift to pin with view)'}</div>}>
<div
className="documentButtonBar-icon"
style={{ color: 'white' }}
@@ -243,7 +244,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
.views()
.filter(v => v)
.map(dv => dv!.rootDoc);
- TabDocView.PinDoc(docs, { pinDocView: true, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+ TabDocView.PinDoc(docs, { pinDocView: e.shiftKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
}}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
</div>
@@ -433,7 +434,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
</div>
) : null}
- <div className="documentButtonBar-button">{this.recordButton}</div>
+ {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.recordButton}</div>}
{
Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
/*<div className="documentButtonBar-button">
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index b490278c3..2e8d31478 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -22,6 +22,129 @@ $resizeHandler: 8px;
grid-template-columns: $resizeHandler 1fr $resizeHandler;
pointer-events: none;
+ .documentDecorations-topbar {
+ display: flex;
+ grid-column-start: 1;
+ grid-column-end: 4;
+ flex-direction: row;
+ gap: 2px;
+
+
+ .documentDecorations-openButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #3ce312;
+ border: solid 1.5px #0a620a;
+ color: #3ce312;
+ transition: 0.1s ease;
+ font-size: 11px;
+ opacity: 1;
+ pointer-events: all;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ border-radius: 100%;
+ cursor: pointer;
+
+ &:hover {
+ color: #02600d;
+ }
+ }
+
+ .documentDecorations-closeButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #fb9d75;
+ border: solid 1.5px #a94442;
+ color: #fb9d75;
+ transition: 0.1s ease;
+ opacity: 1;
+ pointer-events: all;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ border-radius: 100%;
+ cursor: pointer;
+
+ &:hover {
+ color: #a94442;
+ }
+
+ > svg {
+ margin: 0;
+ }
+ }
+
+ .documentDecorations-minimizeButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #ffdd00;
+ border: solid 1.5px #a94442;
+ color: #ffdd00;
+ transition: 0.1s ease;
+ font-size: 11px;
+ opacity: 1;
+ grid-column: 2;
+ pointer-events: all;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ border-radius: 100%;
+ cursor: pointer;
+
+ &:hover {
+ color: #a94442;
+ }
+
+ > svg {
+ margin: 0;
+ }
+ }
+
+ .documentDecorations-title-Dark,
+ .documentDecorations-title {
+ opacity: 1;
+ width: calc(100% - 60px); // = margin-left + margin-right
+ grid-column: 3;
+ pointer-events: auto;
+ overflow: hidden;
+ text-align: center;
+ display: flex;
+ height: 20px;
+ border-radius: 8px;
+ outline: none;
+ border: none;
+
+ .documentDecorations-titleSpan,
+ .documentDecorations-titleSpan-Dark {
+ width: 100%;
+ border-radius: 8px;
+ background: $light-gray;
+ display: inline-block;
+ cursor: move;
+ }
+ .documentDecorations-titleSpan-Dark {
+ background: hsla(0, 0%, 0%, 0.412);
+ }
+ }
+
+ .documentDecorations-title-Dark {
+ color: white;
+ background: black;
+ }
+
+ .documentDecorations-titleBackground {
+ background: $light-gray;
+ border-radius: 8px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ }
+ }
+
.documentDecorations-centerCont {
grid-column: 2;
background: none;
@@ -225,76 +348,12 @@ $resizeHandler: 8px;
cursor: ew-resize;
}
- .documentDecorations-title-Dark,
- .documentDecorations-title {
- opacity: 1;
- width: calc(100% - 8px); // = margin-left + margin-right
- grid-column: 2;
- grid-column-end: 2;
- pointer-events: auto;
- overflow: hidden;
- text-align: center;
- display: flex;
- margin-left: 6px; // closeButton width (14) - leftColumn width (8)
- margin-right: 2px;
- height: 20px;
- position: absolute;
- border-radius: 8px;
- background: rgba(159, 159, 159, 0.1);
-
- .documentDecorations-titleSpan,
- .documentDecorations-titleSpan-Dark {
- width: 100%;
- border-radius: 8px;
- background: #ffffffa0;
- position: absolute;
- display: inline-block;
- cursor: move;
- }
- .documentDecorations-titleSpan-Dark {
- background: hsla(0, 0%, 0%, 0.412);
- }
- }
- .documentDecorations-title-Dark {
- color: white;
- background: black;
- }
-
- .documentDecorations-titleBackground {
- background: #ffffffcf;
- border-radius: 8px;
- width: 100%;
- height: 100%;
- position: absolute;
- }
-
.focus-visible {
margin-left: 0px;
}
}
-.documentDecorations-openButton {
- display: flex;
- align-items: center;
- opacity: 1;
- grid-column-start: 3;
- pointer-events: all;
- cursor: pointer;
-}
-.documentDecorations-closeButton {
- display: flex;
- align-items: center;
- opacity: 1;
- grid-column: 1;
- pointer-events: all;
- width: 14px;
- cursor: pointer;
-
- > svg {
- margin: 0;
- }
-}
.documentDecorations-background {
background: lightblue;
@@ -329,7 +388,7 @@ $resizeHandler: 8px;
justify-content: center;
align-items: center;
gap: 5px;
- background: $medium-gray;
+ background: $light-gray;
}
.linkButtonWrapper {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3589e014a..d5db45494 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,8 +1,10 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
+import { IconButton } from 'browndash-components';
import { action, computed, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
+import { FaUndo } from 'react-icons/fa';
import { DateField } from '../../fields/DateField';
import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc';
import { Document } from '../../fields/documentSchemas';
@@ -28,6 +30,7 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { ImageBox } from './nodes/ImageBox';
+import { VideoBox } from './nodes/VideoBox';
import React = require('react');
@observer
@@ -189,7 +192,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
onCloseClick = (forceDeleteOrIconify: boolean | undefined) => {
const views = SelectionManager.Views()
.slice()
- .filter(v => v);
+ .filter(v => v && v.props.renderDepth > 0);
if (forceDeleteOrIconify === false && this._iconifyBatch) return;
this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
if (!this._iconifyBatch) {
@@ -476,6 +479,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
const doc = Document(docView.rootDoc);
+ if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
+ doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
+ }
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
let docheight = doc._height || 0;
@@ -497,7 +503,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
let actualdW = Math.max(width + dW * scale, 20);
let actualdH = Math.max(height + dH * scale, 20);
- const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen);
+ const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
if (fixedAspect) {
if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
if (dragRight && modifyNativeDim) {
@@ -528,7 +534,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
} else doc._height = actualdH;
}
} else {
- const maxHeight = 0; //Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
+ const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
dW && (doc._width = actualdW);
dH && (doc._autoHeight = false);
@@ -673,7 +679,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
// Rotation constants: Only allow rotation on ink and images
- const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox;
+ const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox || seldoc.ComponentView instanceof VideoBox;
const rotation = NumCast(seldoc.rootDoc._jitterRotation);
const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
@@ -716,9 +722,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
}}>
- {hideDeleteButton ? <div /> : topBtn('close', this.hasIcons ? 'times' : 'window-maximize', undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), 'Close')}
- {titleArea}
- {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Tab (ctrl: as alias, shift: in new collection)')}
+ <div className="documentDecorations-topbar">
+ {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
+ {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
+ {hideResizers ? <div /> : titleArea}
+ {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
+ </div>
{hideResizers ? null : (
<>
<div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
@@ -737,7 +746,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
{useRotation && (
<div key="rot" className={`documentDecorations-rotation`} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
- {'⟲'}
+ <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
</div>
)}
diff --git a/src/client/views/GestureOverlay.scss b/src/client/views/GestureOverlay.scss
index f5cbbffb1..bfe2d5c64 100644
--- a/src/client/views/GestureOverlay.scss
+++ b/src/client/views/GestureOverlay.scss
@@ -1,8 +1,9 @@
.gestureOverlay-cont {
- width: 100vw;
- height: 100vh;
+ width: 100%;
+ height: 100%;
position: absolute;
touch-action: none;
+ top: 0;
.pointerBubbles {
width: 100%;
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 6f28ef9eb..850688e7e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,7 +1,6 @@
import React = require('react');
import * as fitCurve from 'fit-curve';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
+import { action, computed, observable, runInAction, trace } from 'mobx';
import { Doc } from '../../fields/Doc';
import { InkData, InkTool } from '../../fields/InkField';
import { List } from '../../fields/List';
@@ -41,9 +40,13 @@ import { RadialMenu } from './nodes/RadialMenu';
import HorizontalPalette from './Palette';
import { Touchable } from './Touchable';
import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
+import { observer } from 'mobx-react';
+interface GestureOverlayProps {
+ isActive: boolean;
+}
@observer
-export class GestureOverlay extends Touchable {
+export class GestureOverlay extends Touchable<GestureOverlayProps> {
static Instance: GestureOverlay;
@observable public InkShape: string = '';
@@ -83,7 +86,7 @@ export class GestureOverlay extends Touchable {
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- constructor(props: Readonly<{}>) {
+ constructor(props: any) {
super(props);
GestureOverlay.Instance = this;
@@ -495,7 +498,7 @@ export class GestureOverlay extends Touchable {
}
this._strokes = [];
- this._points = [];
+ this._points.length = 0;
this._possibilities = [];
document.removeEventListener('touchend', this.handleHandUp);
}
@@ -619,14 +622,14 @@ export class GestureOverlay extends Touchable {
// get the two targets at the ends of the line
const ep1 = this._points[0];
- const ep2 = this._points[this._points.length - 1];
+ const ep2 = this._points.lastElement();
const target1 = document.elementFromPoint(ep1.X, ep1.Y);
const target2 = document.elementFromPoint(ep2.X, ep2.Y);
const ge = new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
bubbles: true,
detail: {
- points: this._points,
+ points: this._points.slice(),
gesture: GestureUtils.Gestures.Line,
bounds: B,
},
@@ -652,8 +655,8 @@ export class GestureOverlay extends Touchable {
if (this.Tool !== ToolglassTools.None && xInGlass && yInGlass) {
switch (this.Tool) {
case ToolglassTools.InkToText:
- this._strokes.push(new Array(...this._points));
- this._points = [];
+ this._strokes.push(this._points.slice());
+ this._points.length = 0;
CognitiveServices.Inking.Appliers.InterpretStrokes(this._strokes).then(results => {
const wordResults = results.filter((r: any) => r.category === 'line');
const possibilities: string[] = [];
@@ -675,7 +678,7 @@ export class GestureOverlay extends Touchable {
break;
case ToolglassTools.IgnoreGesture:
this.dispatchGesture(GestureUtils.Gestures.Stroke);
- this._points = [];
+ this._points.length = 0;
break;
}
}
@@ -683,7 +686,7 @@ export class GestureOverlay extends Touchable {
else if (this.InkShape) {
this.makePolygon(this.InkShape, false);
this.dispatchGesture(GestureUtils.Gestures.Stroke);
- this._points = [];
+ this._points.length = 0;
if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) {
this.InkShape = '';
Doc.ActiveTool = InkTool.None;
@@ -743,15 +746,16 @@ export class GestureOverlay extends Touchable {
(controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)
);
if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
- this._points = controlPoints;
+ this._points.length = 0;
+ this._points.push(...controlPoints);
this.dispatchGesture(GestureUtils.Gestures.Stroke);
// TODO: nda - check inks to group here
checkInksToGroup();
}
- this._points = [];
+ this._points.length = 0;
}
} else {
- this._points = [];
+ this._points.length = 0;
}
CollectionFreeFormViewChrome.Instance?.primCreated();
};
@@ -803,7 +807,7 @@ export class GestureOverlay extends Touchable {
}
}
}
- this._points = [];
+ this._points.length = 0;
switch (shape) {
//must push an extra point in the end so InteractionUtils knows pointer is up.
//must be (points[0].X,points[0]-1)
@@ -922,7 +926,7 @@ export class GestureOverlay extends Touchable {
new CustomEvent<GestureUtils.GestureEvent>('dashOnGesture', {
bubbles: true,
detail: {
- points: stroke ?? this._points,
+ points: stroke ?? this._points.slice(),
gesture: gesture as any,
bounds: this.getBounds(stroke ?? this._points),
text: data,
@@ -1067,8 +1071,8 @@ export class GestureOverlay extends Touchable {
render() {
return (
- <div className="gestureOverlay-cont" ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
- {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>}
+ <div className="gestureOverlay-cont" style={{ pointerEvents: this.props.isActive ? 'all' : 'none' }} ref={this._overlayRef} onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+ {this.showMobileInkOverlay ? <MobileInkOverlay /> : null}
{this.elements}
<div
@@ -1091,7 +1095,8 @@ export class GestureOverlay extends Touchable {
pointerEvents: 'none',
touchAction: 'none',
display: this.showBounds ? 'unset' : 'none',
- }}></div>
+ }}
+ />
<TouchScrollableMenu options={this._possibilities} bounds={this.svgBounds} selectedIndex={this._selectedIndex} x={this._menuX} y={this._menuY} />
</div>
);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 85f579975..50518eec1 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -84,6 +84,7 @@ export class KeyManager {
});
private unmodified = action((keyname: string, e: KeyboardEvent) => {
+ const hasFffView = SelectionManager.Views().some(dv => dv.props.CollectionFreeFormDocumentView?.());
switch (keyname) {
case 'u':
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
@@ -160,16 +161,16 @@ export class KeyManager {
break;
case 'arrowleft':
UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), 'nudge left');
- break;
+ return { stopPropagation: hasFffView, preventDefault: hasFffView };
case 'arrowright':
UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), 'nudge right');
- break;
+ return { stopPropagation: hasFffView, preventDefault: hasFffView };
case 'arrowup':
UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, -1)), 'nudge up');
- break;
+ return { stopPropagation: hasFffView, preventDefault: hasFffView };
case 'arrowdown':
UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 1)), 'nudge down');
- break;
+ return { stopPropagation: hasFffView, preventDefault: hasFffView };
}
return {
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index dae1c10bb..b83ba97e7 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -24,8 +24,8 @@ import React = require('react');
import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
-import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { BoolCast, Cast, FieldValue, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { InkData, InkField } from '../../fields/InkField';
+import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
@@ -35,20 +35,17 @@ import { Transform } from '../util/Transform';
import { UndoManager } from '../util/UndoManager';
import { ContextMenu } from './ContextMenu';
import { ViewBoxBaseComponent } from './DocComponent';
+import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
import { Colors } from './global/globalEnums';
import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
+import './InkStroke.scss';
import { InkStrokeProperties } from './InkStrokeProperties';
import { InkTangentHandles } from './InkTangentHandles';
import { DocComponentView } from './nodes/DocumentView';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { INK_MASK_SIZE } from './global/globalCssVariables.scss';
-import './InkStroke.scss';
-import Color = require('color');
-import { ComputedField } from '../../fields/ScriptField';
-import { listSpec } from '../../fields/Schema';
-import { List } from '../../fields/List';
import { StyleProp } from './StyleProvider';
+import Color = require('color');
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -460,7 +457,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
width: this.layoutDoc[WidthSym](),
transform: `scale(${this.props.NativeDimScaling?.() || 1})`,
transformOrigin: 'top left',
- top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
+ //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
}}>
<FormattedTextBox
{...OmitKeys(this.props, ['children']).omit}
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 5d42cd97f..f86a1d211 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,35 +1,71 @@
-
- .lightboxView-navBtn {
- margin: auto;
- position: absolute;
- right: 10;
- top: 10;
- background: transparent;
- border-radius: 8;
- color:white;
- opacity: 0.7;
- width: 35;
- &:hover {
- opacity: 1;
- }
+.lightboxView-navBtn {
+ margin: auto;
+ position: absolute;
+ right: 10;
+ top: 10;
+ background: transparent;
+ border-radius: 8;
+ color: white;
+ opacity: 0.7;
+ width: 25;
+ flex-direction: column;
+ display: flex;
+ &:hover {
+ opacity: 1;
}
- .lightboxView-tabBtn {
- margin: auto;
- position: absolute;
- right: 35;
- top: 10;
- background: transparent;
- border-radius: 8;
- color:white;
- opacity: 0.7;
- width: 35;
- &:hover {
- opacity: 1;
- }
+}
+.lightboxView-tabBtn {
+ margin: auto;
+ position: absolute;
+ right: 38;
+ top: 10;
+ background: transparent;
+ border-radius: 8;
+ color: white;
+ opacity: 0.7;
+ width: 25;
+ flex-direction: column;
+ display: flex;
+ &:hover {
+ opacity: 1;
+ }
+}
+.lightboxView-penBtn {
+ margin: auto;
+ position: absolute;
+ right: 70;
+ top: 10;
+ background: transparent;
+ border-radius: 8;
+ color: white;
+ opacity: 0.7;
+ width: 25;
+ flex-direction: column;
+ display: flex;
+ &:hover {
+ opacity: 1;
+ }
+}
+.lightboxView-exploreBtn {
+ margin: auto;
+ position: absolute;
+ right: 100;
+ top: 10;
+ background: transparent;
+ border-radius: 8;
+ color: white;
+ opacity: 0.7;
+ width: 25;
+ flex-direction: column;
+ display: flex;
+ &:hover {
+ opacity: 1;
}
+}
.lightboxView-frame {
- position: absolute;
- top: 0; left: 0;
+ position: absolute;
+ top: 0;
+ left: 0;
width: 100%;
height: 100%;
background: #000000bb;
@@ -51,4 +87,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 4fd145f2d..c2b6de99b 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -1,9 +1,9 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
-import 'normalize.css';
import * as React from 'react';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { InkTool } from '../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
import { DocUtils } from '../documents/Documents';
@@ -12,8 +12,11 @@ import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
import { Transform } from '../util/Transform';
import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
import { TabDocView } from './collections/TabDocView';
+import { GestureOverlay } from './GestureOverlay';
import './LightboxView.scss';
+import { MainView } from './MainView';
import { DocumentView } from './nodes/DocumentView';
import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
@@ -37,7 +40,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
private static _savedState: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }>;
private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
@observable private static _future: Opt<Doc[]> = [];
- private static _docView: Opt<DocumentView>;
+ @observable private static _docView: Opt<DocumentView>;
private static openInTabFunc: any;
static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }> }[] = [];
@action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc) {
@@ -51,11 +54,18 @@ export class LightboxView extends React.Component<LightboxViewProps> {
if (!doc) {
this._docFilters && (this._docFilters.length = 0);
this._future = this._history = [];
+ Doc.ActiveTool = InkTool.None;
+ MainView.Instance._exploreMode = false;
} else {
if (doc) {
const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
}
+ CollectionStackedTimeline.CurrentlyPlaying?.forEach(doc => {
+ DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => {
+ dv.ComponentView?.Pause?.();
+ });
+ });
//TabDocView.PinDoc(doc, { hidePresBox: true });
this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
if (doc !== LightboxView.LightboxDoc) {
@@ -91,7 +101,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
return true;
}
public static IsLightboxDocView(path: DocumentView[]) {
- return path.includes(this._docView!);
+ return (path ?? []).includes(this._docView!);
}
@computed get leftBorder() {
return Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]);
@@ -262,45 +272,49 @@ export class LightboxView extends React.Component<LightboxViewProps> {
clipPath: `path('${Doc.UserDoc().renderStyle === 'comic' ? wavyBorderPath(this.lightboxWidth(), this.lightboxHeight()) : undefined}')`,
}}>
{/* <CollectionMenu /> TODO:glr This is where it would go*/}
- <DocumentView
- ref={action((r: DocumentView | null) => {
- LightboxView._docView = r !== null ? r : undefined;
- r &&
- setTimeout(
- action(() => {
- const target = LightboxView._docTarget;
- const doc = LightboxView._doc;
- const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
- if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
- //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button
- })
- );
- })}
- Document={LightboxView.LightboxDoc}
- DataDoc={undefined}
- LayoutTemplate={LightboxView.LightboxDocTemplate}
- addDocument={undefined}
- isDocumentActive={returnFalse}
- isContentActive={returnTrue}
- addDocTab={this.addDocTab}
- pinToPres={TabDocView.PinDoc}
- rootSelected={returnTrue}
- docViewPath={returnEmptyDoclist}
- docFilters={this.docFilters}
- removeDocument={undefined}
- styleProvider={DefaultStyleProvider}
- ScreenToLocalTransform={this.lightboxScreenToLocal}
- PanelWidth={this.lightboxWidth}
- PanelHeight={this.lightboxHeight}
- focus={DocUtils.DefaultFocus}
- whenChildContentsActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- renderDepth={0}
- />
+
+ <GestureOverlay isActive={true}>
+ <DocumentView
+ ref={action((r: DocumentView | null) => {
+ LightboxView._docView = r !== null ? r : undefined;
+ r &&
+ setTimeout(
+ action(() => {
+ const target = LightboxView._docTarget;
+ const doc = LightboxView._doc;
+ //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+ if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
+ //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button
+ })
+ );
+ })}
+ Document={LightboxView.LightboxDoc}
+ DataDoc={undefined}
+ LayoutTemplate={LightboxView.LightboxDocTemplate}
+ addDocument={undefined}
+ isDocumentActive={returnFalse}
+ isContentActive={returnTrue}
+ addDocTab={this.addDocTab}
+ pinToPres={TabDocView.PinDoc}
+ onBrowseClick={MainView.Instance.exploreMode}
+ rootSelected={returnTrue}
+ docViewPath={returnEmptyDoclist}
+ docFilters={this.docFilters}
+ removeDocument={undefined}
+ styleProvider={DefaultStyleProvider}
+ ScreenToLocalTransform={this.lightboxScreenToLocal}
+ PanelWidth={this.lightboxWidth}
+ PanelHeight={this.lightboxHeight}
+ focus={DocUtils.DefaultFocus}
+ whenChildContentsActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ renderDepth={0}
+ />
+ </GestureOverlay>
</div>
{this.navBtn(
@@ -328,6 +342,15 @@ export class LightboxView extends React.Component<LightboxViewProps> {
)}
<LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} />
<div
+ className="lightboxView-navBtn"
+ title={'toggle fit width'}
+ onClick={e => {
+ e.stopPropagation();
+ LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
+ }}>
+ <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
+ </div>
+ <div
className="lightboxView-tabBtn"
title={'open in tab'}
onClick={e => {
@@ -340,13 +363,24 @@ export class LightboxView extends React.Component<LightboxViewProps> {
<FontAwesomeIcon icon={'file-download'} size="2x" />
</div>
<div
- className="lightboxView-navBtn"
- title={'toggle fit width'}
+ className="lightboxView-penBtn"
+ title="toggle pen annotation"
+ style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
onClick={e => {
e.stopPropagation();
- LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
}}>
- <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
+ <FontAwesomeIcon color={Doc.ActiveTool === InkTool.Pen ? 'black' : 'white'} icon={'pen'} size="2x" />
+ </div>
+ <div
+ className="lightboxView-exploreBtn"
+ title="toggle explore mode to navigate among documents only"
+ style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }}
+ onClick={action(e => {
+ e.stopPropagation();
+ MainView.Instance._exploreMode = !MainView.Instance._exploreMode;
+ })}>
+ <FontAwesomeIcon color={MainView.Instance._exploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" />
</div>
</div>
);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 4cb1183d0..3e6539c5e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -14,6 +14,8 @@ import { ReplayMovements } from '../util/ReplayMovements';
import { TrackMovements } from '../util/TrackMovements';
import { CollectionView } from './collections/CollectionView';
import { MainView } from './MainView';
+import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
+dotenv.config();
AssignAllExtensions();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index da79d2992..c5ac1cf52 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -1,5 +1,10 @@
@import 'global/globalCssVariables';
@import 'nodeModuleOverrides';
+h1,
+.h1 {
+ // reverts change to h1 made by normalize.css
+ font-size: 36px;
+}
.dash-tooltip {
font-size: 11px;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 06be4d194..857a5522c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -5,16 +5,17 @@ import * as fa from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
+import 'normalize.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { ScriptField } from '../../fields/ScriptField';
-import { StrCast } from '../../fields/Types';
+import { DocCast, 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';
import { Docs, DocUtils } from '../documents/Documents';
-import { CollectionViewType } from '../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { CaptureManager } from '../util/CaptureManager';
import { DocumentManager } from '../util/DocumentManager';
import { GroupManager } from '../util/GroupManager';
@@ -56,6 +57,7 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
import { RadialMenu } from './nodes/RadialMenu';
import { TaskCompletionBox } from './nodes/TaskCompletedBox';
+import { PresBox } from './nodes/trails';
import { WebBox } from './nodes/WebBox';
import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
@@ -63,6 +65,7 @@ import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
import { TopBar } from './topbar/TopBar';
+import 'browndash-components/dist/styles/global.min.css';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -86,7 +89,7 @@ export class MainView extends React.Component {
return this._hideUI ? 0 : 27;
} // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
@computed private get topOfDashUI() {
- return this._hideUI ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
+ return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
}
@computed private get topOfHeaderBarDoc() {
return this.topOfDashUI;
@@ -111,7 +114,7 @@ export class MainView extends React.Component {
}
@observable mainDoc: Opt<Doc>;
@computed private get mainContainer() {
- if (window.location.pathname.startsWith('/doc/')) {
+ if (window.location.pathname.startsWith('/doc/') && Doc.CurrentUserEmail === 'guest') {
DocServer.GetRefField(window.location.pathname.substring('/doc/'.length)).then(main => runInAction(() => (this.mainDoc = main as Doc)));
return this.mainDoc;
}
@@ -154,12 +157,18 @@ export class MainView extends React.Component {
if (!MainView.Live) {
DocServer.setPlaygroundFields([
'dataTransition',
+ 'viewTransition',
'treeViewOpen',
'showSidebar',
+ 'itemIndex', // for changing slides in presentations
'sidebarWidthPercent',
- 'viewTransition',
+ 'currentTimecode',
+ 'timelineHeightPercent',
+ 'presStatus',
'panX',
'panY',
+ 'overlayX',
+ 'overlayY',
'fitWidth',
'nativeWidth',
'nativeHeight',
@@ -174,6 +183,7 @@ export class MainView extends React.Component {
'chromeHidden',
'currentFrame',
'width',
+ 'height',
'nativeWidth',
]); // can play with these fields on someone else's
}
@@ -225,7 +235,13 @@ export class MainView extends React.Component {
if (pathname.length > 1 && pathname[0] === 'doc') {
Doc.MainDocId = pathname[1];
//!this.userDoc &&
- DocServer.GetRefField(pathname[1]).then(action(field => field instanceof Doc && (Doc.GuestTarget = field)));
+ DocServer.GetRefField(pathname[1]).then(
+ action(field => {
+ if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) {
+ Doc.GuestTarget = field;
+ }
+ })
+ );
}
}
@@ -517,11 +533,23 @@ export class MainView extends React.Component {
};
@action
- createNewPresentation = async () => {
- 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');
+ createNewPresentation = () => {
+ const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
+ CollectionDockingView.AddSplit(pres, 'right');
+ Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
Doc.ActivePresentation = pres;
- Doc.AddDocToList(Doc.MyTrails, 'data', pres);
+ };
+
+ @action
+ openPresentation = (pres: Doc) => {
+ if (pres.type === DocumentType.PRES) {
+ CollectionDockingView.AddSplit(pres, 'right');
+ Doc.MyTrails && (Doc.ActivePresentation = pres);
+ Doc.AddDocToList(Doc.MyTrails, 'data', pres);
+ this.closeFlyout();
+ } else {
+ PresBox.NavigateToDoc(DocCast(pres.presentationTargetDoc), pres);
+ }
};
@action
@@ -608,7 +636,7 @@ export class MainView extends React.Component {
@computed get dockingContent() {
return (
- <GestureOverlay>
+ <GestureOverlay isActive={LightboxView.LightboxDoc ? false : true}>
<div
key="docking"
className={`mainView-dockingContent${this._leftMenuFlyoutWidth ? '-flyout' : ''}`}
@@ -655,21 +683,16 @@ export class MainView extends React.Component {
const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
const locationParams = locationFields.length > 1 ? locationFields[1] : '';
if (doc.dockingConfig) return DashboardView.openDashboard(doc);
+ // prettier-ignore
switch (locationFields[0]) {
- case 'dashboard':
- return DashboardView.openDashboard(doc);
- case 'close':
- return CollectionDockingView.CloseSplit(doc, locationParams);
- case 'fullScreen':
- return CollectionDockingView.OpenFullScreen(doc);
- case 'lightbox':
- return LightboxView.AddDocTab(doc, location);
- case 'toggle':
- return CollectionDockingView.ToggleSplit(doc, locationParams);
- case 'inPlace':
- case 'add':
default:
- return CollectionDockingView.AddSplit(doc, locationParams);
+ case 'inPlace':
+ case 'add': return CollectionDockingView.AddSplit(doc, locationParams);
+ case 'dashboard': return DashboardView.openDashboard(doc);
+ case 'close': return CollectionDockingView.CloseSplit(doc, locationParams);
+ case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc);
+ case 'lightbox': return LightboxView.AddDocTab(doc, location);
+ case 'toggle': return CollectionDockingView.ToggleSplit(doc, locationParams);
}
};
@@ -714,7 +737,7 @@ export class MainView extends React.Component {
@computed get leftMenuPanel() {
return (
- <div key="menu" className="mainView-leftMenuPanel">
+ <div key="menu" className="mainView-leftMenuPanel" style={{ display: LightboxView.LightboxDoc ? 'none' : undefined }}>
<DocumentView
Document={Doc.MyLeftSidebarMenu}
DataDoc={undefined}
@@ -989,19 +1012,16 @@ export class MainView extends React.Component {
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null}
{((page: string) => {
+ // prettier-ignore
switch (page) {
- case 'dashboard':
default:
- return (
- <>
- <div style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
- <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
- </div>
- {this.mainDashboardArea}
- </>
- );
- case 'home':
- return <DashboardView />;
+ case 'dashboard': return (<>
+ <div key="dashdiv" style={{ position: 'relative', display: this._hideUI || LightboxView.LightboxDoc ? 'none' : undefined, zIndex: 2001 }}>
+ <CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
+ </div>
+ {this.mainDashboardArea}
+ </> );
+ case 'home': return <DashboardView />;
}
})(Doc.ActivePage)}
@@ -1018,7 +1038,7 @@ export class MainView extends React.Component {
<InkTranscription />
{this.snapLines}
<div className="mainView-webRef" ref={this.makeWebRef} />
- <LightboxView PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
</div>
);
}
diff --git a/src/client/views/MarqueeAnnotator.scss b/src/client/views/MarqueeAnnotator.scss
index c90d48a65..5c65f35e9 100644
--- a/src/client/views/MarqueeAnnotator.scss
+++ b/src/client/views/MarqueeAnnotator.scss
@@ -1,12 +1,11 @@
-
.marqueeAnnotator-annotationBox {
position: absolute;
background-color: rgba(245, 230, 95, 0.616);
}
-
.marqueeAnnotator-dragBox {
- position:absolute;
+ position: absolute;
background-color: transparent;
- opacity: 0.1;
-} \ No newline at end of file
+ opacity: 0.2;
+ cursor: default;
+}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index f90ad8bb5..d9a989309 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -271,8 +271,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
width: `${this._width}px`,
height: `${this._height}px`,
border: `${this._width === 0 ? '' : '2px dashed black'}`,
- opacity: 0.2,
- }}></div>
+ }}/>
);
}
}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 5242fabb8..6d7f2c037 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -12,6 +12,7 @@ import { DragManager } from '../util/DragManager';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { Transform } from '../util/Transform';
import { CollectionFreeFormLinksView } from './collections/collectionFreeForm/CollectionFreeFormLinksView';
+import { LightboxView } from './LightboxView';
import { DocumentView } from './nodes/DocumentView';
import './OverlayView.scss';
import { ScriptingRepl } from './ScriptingRepl';
@@ -90,7 +91,7 @@ export class OverlayWindow extends React.Component<OverlayWindowProps> {
};
render() {
- return (
+ return LightboxView.LightboxDoc ? null : (
<div className="overlayWindow-outerDiv" style={{ transform: `translate(${this.x}px, ${this.y}px)`, width: this.width, height: this.height }}>
<div className="overlayWindow-titleBar" onPointerDown={this.onPointerDown}>
{this.props.overlayOptions.title || 'Untitled'}
@@ -164,77 +165,79 @@ export class OverlayView extends React.Component {
docScreenToLocalXf = computedFn(
function docScreenToLocalXf(this: any, doc: Doc) {
- return () => new Transform(-NumCast(doc.x), -NumCast(doc.y), 1);
+ return () => new Transform(-NumCast(doc.overlayX), -NumCast(doc.overlayY), 1);
}.bind(this)
);
@computed get overlayDocs() {
- return DocListCast(Doc.MyOverlayDocs?.data).map(d => {
- let offsetx = 0,
- offsety = 0;
- const dref = React.createRef<HTMLDivElement>();
- const onPointerMove = action((e: PointerEvent, down: number[]) => {
- if (e.buttons === 1) {
- d.x = e.clientX + offsetx;
- d.y = e.clientY + offsety;
- }
- if (e.metaKey) {
- const dragData = new DragManager.DocumentDragData([d]);
- dragData.offset = [-offsetx, -offsety];
- dragData.dropAction = 'move';
- dragData.removeDocument = (doc: Doc | Doc[]) => {
- const docs = doc instanceof Doc ? [doc] : doc;
- docs.forEach(d => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, d));
- return true;
- };
- dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
- return dragData.removeDocument!(doc) ? addDocument(doc) : false;
- };
- DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
- return true;
- }
- return false;
- });
+ return LightboxView.LightboxDoc
+ ? null
+ : DocListCast(Doc.MyOverlayDocs?.data).map(d => {
+ let offsetx = 0,
+ offsety = 0;
+ const dref = React.createRef<HTMLDivElement>();
+ const onPointerMove = action((e: PointerEvent, down: number[]) => {
+ if (e.buttons === 1) {
+ d.overlayX = e.clientX + offsetx;
+ d.overlayY = e.clientY + offsety;
+ }
+ if (e.metaKey) {
+ const dragData = new DragManager.DocumentDragData([d]);
+ dragData.offset = [-offsetx, -offsety];
+ dragData.dropAction = 'move';
+ dragData.removeDocument = (doc: Doc | Doc[]) => {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(d => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, d));
+ return true;
+ };
+ dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+ return dragData.removeDocument!(doc) ? addDocument(doc) : false;
+ };
+ DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
+ return true;
+ }
+ return false;
+ });
- const onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
- offsetx = NumCast(d.x) - e.clientX;
- offsety = NumCast(d.y) - e.clientY;
- };
- return (
- <div
- className="overlayView-doc"
- ref={dref}
- key={d[Id]}
- onPointerDown={onPointerDown}
- style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.x}px, ${d.y}px)` }}>
- <DocumentView
- Document={d}
- rootSelected={returnTrue}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={this.removeOverlayDoc}
- PanelWidth={d[WidthSym]}
- PanelHeight={d[HeightSym]}
- ScreenToLocalTransform={this.docScreenToLocalXf(d)}
- renderDepth={1}
- isDocumentActive={returnTrue}
- isContentActive={returnTrue}
- whenChildContentsActiveChanged={emptyFunction}
- focus={DocUtils.DefaultFocus}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- />
- </div>
- );
- });
+ const onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+ offsetx = NumCast(d.overlayX) - e.clientX;
+ offsety = NumCast(d.overlayY) - e.clientY;
+ };
+ return (
+ <div
+ className="overlayView-doc"
+ ref={dref}
+ key={d[Id]}
+ onPointerDown={onPointerDown}
+ style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}>
+ <DocumentView
+ Document={d}
+ rootSelected={returnTrue}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={this.removeOverlayDoc}
+ PanelWidth={d[WidthSym]}
+ PanelHeight={d[HeightSym]}
+ ScreenToLocalTransform={this.docScreenToLocalXf(d)}
+ renderDepth={1}
+ isDocumentActive={returnTrue}
+ isContentActive={returnTrue}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={DocUtils.DefaultFocus}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ />
+ </div>
+ );
+ });
}
public static ShowSpinner() {
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
index 60b7d14a0..7be765ea9 100644
--- a/src/client/views/PreviewCursor.scss
+++ b/src/client/views/PreviewCursor.scss
@@ -1,11 +1,10 @@
-
.previewCursor-Dark,
.previewCursor {
color: black;
position: absolute;
transform-origin: left top;
top: 0;
- left:0;
+ left: 0;
pointer-events: none;
opacity: 1;
z-index: 1001;
@@ -13,4 +12,4 @@
.previewCursor-Dark {
color: white;
-} \ No newline at end of file
+}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 68f5f072d..119476210 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -1,12 +1,12 @@
import { action, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import 'normalize.css';
import * as React from 'react';
import { Doc } from '../../fields/Doc';
import { Cast, NumCast, StrCast } from '../../fields/Types';
import { returnFalse } from '../../Utils';
import { DocServer } from '../DocServer';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
+import { ImageUtils } from '../util/Import & Export/ImageUtils';
import { Transform } from '../util/Transform';
import { undoBatch, UndoManager } from '../util/UndoManager';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -16,15 +16,25 @@ import './PreviewCursor.scss';
export class PreviewCursor extends React.Component<{}> {
static _onKeyPress?: (e: KeyboardEvent) => void;
static _getTransform: () => Transform;
- static _addDocument: (doc: Doc | Doc[]) => void;
+ static _addDocument: (doc: Doc | Doc[]) => boolean;
static _addLiveTextDoc: (doc: Doc) => void;
static _nudge?: undefined | ((x: number, y: number) => boolean);
+ static _slowLoadDocuments?: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
constructor(props: any) {
super(props);
document.addEventListener('keydown', this.onKeyPress);
- document.addEventListener('paste', this.paste);
+ document.addEventListener('paste', this.paste, true);
}
paste = async (e: ClipboardEvent) => {
@@ -38,20 +48,16 @@ export class PreviewCursor extends React.Component<{}> {
if (plain) {
// tests for youtube and makes video document
if (plain.indexOf('www.youtube.com/watch') !== -1) {
- const url = plain.replace('youtube.com/watch?v=', 'youtube.com/embed/');
- undoBatch(() =>
- PreviewCursor._addDocument(
- Docs.Create.VideoDocument(url, {
- title: url,
- _width: 400,
- _height: 315,
- _nativeWidth: 600,
- _nativeHeight: 472.5,
- x: newPoint[0],
- y: newPoint[1],
- })
- )
- )();
+ const batch = UndoManager.StartBatch('youtube upload');
+ const generatedDocuments: Doc[] = [];
+ const options = {
+ title: plain,
+ _width: 400,
+ _height: 315,
+ x: newPoint[0],
+ y: newPoint[1],
+ };
+ PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end);
} else if (re.test(plain)) {
const url = plain;
undoBatch(() =>
@@ -104,16 +110,16 @@ export class PreviewCursor extends React.Component<{}> {
const re: any = /<img src="(.*?)"/g;
const arr: any[] = re.exec(e.clipboardData.getData('text/html'));
- undoBatch(() =>
- PreviewCursor._addDocument(
- Docs.Create.ImageDocument(arr[1], {
- _width: 300,
- title: arr[1],
- x: newPoint[0],
- y: newPoint[1],
- })
- )
- )();
+ undoBatch(() => {
+ const doc = Docs.Create.ImageDocument(arr[1], {
+ _width: 300,
+ title: arr[1],
+ x: newPoint[0],
+ y: newPoint[1],
+ });
+ ImageUtils.ExtractExif(doc);
+ PreviewCursor._addDocument(doc);
+ })();
} else if (e.clipboardData.items.length) {
const batch = UndoManager.StartBatch('collection view drop');
const files: File[] = [];
@@ -184,7 +190,17 @@ export class PreviewCursor extends React.Component<{}> {
addLiveText: (doc: Doc) => void,
getTransform: () => Transform,
addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
- nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean)
+ nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean),
+ slowLoadDocuments: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>
) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
@@ -192,6 +208,7 @@ export class PreviewCursor extends React.Component<{}> {
this._getTransform = getTransform;
this._addDocument = addDocument || returnFalse;
this._nudge = nudge;
+ this._slowLoadDocuments = slowLoadDocuments;
this.Visible = true;
}
render() {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 33f17047b..842664402 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -7,7 +7,7 @@ import { intersection } from 'lodash';
import { action, autorun, computed, Lambda, observable } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { List } from '../../fields/List';
@@ -33,6 +33,7 @@ import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector
import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
import './PropertiesView.scss';
import { DefaultStyleProvider } from './StyleProvider';
+import { listSpec } from '../../fields/Schema';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -58,7 +59,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@computed get selectedDocumentView() {
if (SelectionManager.Views().length) return SelectionManager.Views()[0];
- if (PresBox.Instance?._selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc);
+ if (PresBox.Instance?.selectedArray.size) return DocumentManager.Instance.getDocumentView(PresBox.Instance.rootDoc);
return undefined;
}
@computed get isPres(): boolean {
@@ -1156,13 +1157,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
createNewFilterDoc = () => {
if (this.selectedDoc) {
+ const curFilterDoc = DocCast(this.selectedDoc.currentFilter);
const currentDocFilters = this.selectedDoc._docFilters;
const currentDocRangeFilters = this.selectedDoc._docRangeFilters;
this.selectedDoc._docFilters = new List<string>();
this.selectedDoc._docRangeFilters = new List<string>();
- (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters;
- (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters;
- this.selectedDoc.currentFilter = FilterBox.createFilterDoc();
+ if (DocListCast(Doc.UserDoc().savedFilters).includes(curFilterDoc)) {
+ curFilterDoc._docFiltersList = currentDocFilters;
+ curFilterDoc._docRangeFiltersList = currentDocRangeFilters;
+ this.selectedDoc.currentFilter = FilterBox.createFilterDoc();
+ } else {
+ Doc.GetProto(curFilterDoc).data = undefined;
+ Doc.GetProto(curFilterDoc).title = 'Unnamed Filter';
+ curFilterDoc._docFiltersList = undefined;
+ curFilterDoc._docRangeFiltersList = undefined;
+ }
}
};
@@ -1612,7 +1621,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
if (this.isPres) {
- const selectedItem: boolean = PresBox.Instance?._selectedArray.size > 0;
+ const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0;
const type = PresBox.Instance.activeItem?.type;
return (
<div className="propertiesView" style={{ width: this.props.width }}>
@@ -1622,7 +1631,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-name" style={{ borderBottom: 0 }}>
{this.editableTitle}
<div className="propertiesView-presSelected">
- <div className="propertiesView-selectedCount">{PresBox.Instance?._selectedArray.size} selected</div>
+ <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div>
<div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div>
</div>
</div>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 90d9c3c43..12f41394d 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -73,7 +73,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
this.allMetadata.map(tag => (target[tag] = tag));
DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
this.addDocument(target);
- this._stackRef.current?.focusDocument(target);
+ this._stackRef.current?.focusDocument(target, {});
return target;
};
makeDocUnfiltered = (doc: Doc) => {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index c0ba170c6..8b256923a 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -169,7 +169,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
: 0;
case StyleProp.BackgroundColor: {
if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY;
- let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, StrCast(props?.Document.backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')));
+ let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
switch (doc?.type) {
case DocumentType.PRESELEMENT:
docColor = docColor || (darkScheme() ? '' : '');
@@ -271,7 +271,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
if (props?.pointerEvents?.() === 'none') return 'none';
const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none';
- if (!isInk) return 'all';
+ if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
return undefined;
case StyleProp.Decorations:
if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss
index b1cce8705..c99281323 100644
--- a/src/client/views/_nodeModuleOverrides.scss
+++ b/src/client/views/_nodeModuleOverrides.scss
@@ -44,8 +44,8 @@ div .lm_header {
position: absolute;
width: calc(100% - 60px);
overflow: scroll;
- background: #6b6b6b6b; //$dark-gray;
- border-radius: 5px;
+ background: transparent; //$dark-gray;
+ border-radius: 0px;
}
.lm_tab {
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 091ba8e74..ac3541f2c 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -154,7 +154,15 @@
background: $white;
}
+ .lm_controls {
+ height: 27px;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ }
+
.lm_controls > li {
+ height: 27px !important;
opacity: 1;
transform: scale(1);
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6d70cc0d2..e9b41de25 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { inheritParentAcls } from '../../../fields/util';
import { emptyFunction, incrementTitleCopy } from '../../../Utils';
@@ -56,11 +56,12 @@ export class CollectionDockingView extends CollectionSubView() {
constructor(props: SubCollectionViewProps) {
super(props);
- runInAction(() => (CollectionDockingView.Instance = this));
+ if (this.props.renderDepth < 0) runInAction(() => (CollectionDockingView.Instance = this));
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
DragManager.StartWindowDrag = this.StartOtherDrag;
+ this.rootDoc.myTrails; // this is equivalent to having a prefetchProxy for myTrails which is needed for the My Trails button in the UI which assumes that Doc.ActiveDashboard.myTrails is legit...
}
/**
@@ -73,7 +74,7 @@ export class CollectionDockingView extends CollectionSubView() {
public StartOtherDrag = (e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => {
this._flush = this._flush ?? UndoManager.StartBatch('golden layout drag');
const config = dragDocs.length === 1 ? CollectionDockingView.makeDocumentConfig(dragDocs[0]) : { type: 'row', content: dragDocs.map(doc => CollectionDockingView.makeDocumentConfig(doc)) };
- const dragSource = this._goldenLayout.createDragSource(document.createElement('div'), config);
+ const dragSource = CollectionDockingView.Instance?._goldenLayout.createDragSource(document.createElement('div'), config);
this.tabDragStart(dragSource, finishDrag);
dragSource._dragListener.onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 });
};
@@ -280,12 +281,13 @@ export class CollectionDockingView extends CollectionSubView() {
this.stateChanged();
return true;
}
-
setupGoldenLayout = async () => {
+ //const config = StrCast(this.props.Document.dockingConfig, JSON.stringify(DashboardView.resetDashboard(this.props.Document)));
const config = StrCast(this.props.Document.dockingConfig);
if (config) {
const matches = config.match(/\"documentId\":\"[a-z0-9-]+\"/g);
const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')) ?? [];
+
await Promise.all(docids.map(id => DocServer.GetRefField(id)));
if (this._goldenLayout) {
@@ -311,6 +313,8 @@ export class CollectionDockingView extends CollectionSubView() {
glay.root.layoutManager.on('itemDropped', this.tabItemDropped);
glay.root.layoutManager.on('dragStart', this.tabDragStart);
glay.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
+ } else {
+ console.log('ERROR: no config for dashboard!!');
}
};
@@ -395,10 +399,10 @@ export class CollectionDockingView extends CollectionSubView() {
const _height = Number(getComputedStyle(content).height.replace('px', ''));
return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
- const proto = Cast(img.proto, Doc, null)!;
- proto['data-nativeWidth'] = _width;
- proto['data-nativeHeight'] = _height;
- this.dataDoc.thumb = img;
+ const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
+ proto['thumb-nativeWidth'] = _width;
+ proto['thumb-nativeHeight'] = _height;
+ this.dataDoc.thumb = new ImageField(iconFile);
});
}
}
@@ -429,7 +433,8 @@ export class CollectionDockingView extends CollectionSubView() {
return newtab;
});
const copy = Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
- return DashboardView.openDashboard(await copy);
+ DashboardView.SetupDashboardTrails(copy);
+ return DashboardView.openDashboard(copy);
}
@action
@@ -455,7 +460,7 @@ export class CollectionDockingView extends CollectionSubView() {
};
tabDestroyed = (tab: any) => {
- if (tab.DashDoc?.type !== DocumentType.KVP) {
+ if (![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
}
@@ -475,6 +480,7 @@ export class CollectionDockingView extends CollectionSubView() {
};
stackCreated = (stack: any) => {
+ stack = stack.header ? stack : stack.origin;
stack.header?.element.on('mousedown', (e: any) => {
const dashboard = Doc.ActiveDashboard;
if (dashboard && e.target === stack.header?.element[0] && e.button === 2) {
@@ -497,7 +503,7 @@ export class CollectionDockingView extends CollectionSubView() {
.click(
action(() => {
//if (confirm('really close this?')) {
- if (!stack.parent.parent.isRoot || stack.parent.contentItems.length > 1) {
+ if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
stack.remove();
} else {
alert('cant delete the last stack');
@@ -532,7 +538,23 @@ export class CollectionDockingView extends CollectionSubView() {
};
render() {
- return <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef} />;
+ const href = ImageCast(this.rootDoc.thumb)?.url.href;
+ return this.props.renderDepth > -1 ? (
+ <div>
+ {href ? (
+ <img
+ style={{ background: 'white', top: 0, position: 'absolute' }}
+ src={href} // + '?d=' + (new Date()).getTime()}
+ width={this.props.PanelWidth()}
+ height={this.props.PanelHeight()}
+ />
+ ) : (
+ <p>nested dashboards has no thumbnail</p>
+ )}
+ </div>
+ ) : (
+ <div className="collectiondockingview-container" onPointerDown={this.onPointerDown} ref={this._containerRef} />
+ );
}
}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 0dc30e0fd..46e8494ab 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -34,7 +34,6 @@ import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocum
import { DocumentView } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { PresBox } from '../nodes/trails/PresBox';
import { DefaultStyleProvider } from '../StyleProvider';
import { CollectionDockingView } from './CollectionDockingView';
import { CollectionLinearView } from './collectionLinear';
@@ -726,18 +725,16 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
}
public static gotoKeyFrame(doc: Doc, newFrame: number) {
- if (!doc) {
- return;
- }
- const dataField = doc[Doc.LayoutFieldKey(doc)];
- const childDocs = DocListCast(dataField);
- const currentFrame = Cast(doc._currentFrame, 'number', null);
- if (currentFrame === undefined) {
- doc._currentFrame = 0;
- CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+ if (doc) {
+ const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]);
+ const currentFrame = Cast(doc._currentFrame, 'number', null);
+ if (currentFrame === undefined) {
+ doc._currentFrame = 0;
+ CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
+ }
+ CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+ doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
}
- CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
- doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
}
@undoBatch
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index fe98f307e..4d1f18e54 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -1,7 +1,7 @@
@import '../global/globalCssVariables';
.collectionNoteTakingView-DocumentButtons {
- display: flex;
+ display: none;
justify-content: space-between;
margin: auto;
}
@@ -51,6 +51,16 @@
display: flex;
}
+.collectionNoteTakingViewFieldColumn {
+ display: flex;
+ overflow: auto;
+}
+.collectionNoteTakingViewFieldColumn:hover {
+ .collectionNoteTakingView-DocumentButtons {
+ display: flex;
+ }
+}
+
// TODO:glr Turn this into a seperate class
.documentButtonMenu {
position: relative;
@@ -82,7 +92,6 @@
top: 0;
overflow-y: auto;
overflow-x: hidden;
- flex-wrap: wrap;
transition: top 0.5s;
> div {
@@ -102,6 +111,11 @@
height: auto;
}
+ .collectionNoteTakingView-columnStack {
+ height: 100%;
+ width: 100%;
+ display: inline-block;
+ }
.collectionNoteTakingView-Nodes {
width: 100%;
height: 100%;
@@ -113,6 +127,11 @@
left: 0;
width: 100%;
position: absolute;
+ margin: auto;
+ width: max-content;
+ height: max-content;
+ position: relative;
+ grid-auto-rows: 0px;
}
.collectionNoteTakingView-description {
@@ -133,6 +152,16 @@
margin-left: -5;
}
+ .collectionNoteTakingView-sectionDelete {
+ display: none;
+ position: absolute;
+ right: 0;
+ width: max-content;
+ height: max-content;
+ top: 10;
+ padding: 2;
+ }
+
// Documents in NoteTaking view
.collectionNoteTakingView-columnDoc {
display: flex;
@@ -347,14 +376,6 @@
}
}
}
-
- .collectionNoteTakingView-sectionDelete {
- position: absolute;
- right: 25px;
- top: 0;
- height: 100%;
- display: none;
- }
}
.collectionNoteTakingView-sectionHeader:hover {
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index b359ef420..b0f64ed60 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -3,7 +3,7 @@ import { CursorProperty } from 'csstype';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -11,7 +11,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Ty
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
-import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
@@ -19,7 +19,6 @@ import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { LightboxView } from '../LightboxView';
-import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -30,13 +29,6 @@ import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivid
import { CollectionSubView } from './CollectionSubView';
const _global = (window /* browser */ || global) /* node */ as any;
-export type collectionNoteTakingViewProps = {
- chromeHidden?: boolean;
- viewType?: CollectionViewType;
- NativeWidth?: () => number;
- NativeHeight?: () => number;
-};
-
/**
* CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1)
* add and remove columns (2) change column sizes and (3) move documents within and between columns. This
@@ -45,41 +37,42 @@ export type collectionNoteTakingViewProps = {
* the rest of Dash, so it may be worthwhile to transition the headers to simple documents.
*/
@observer
-export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() {
+export class CollectionNoteTakingView extends CollectionSubView() {
_disposers: { [key: string]: IReactionDisposer } = {};
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
+ notetakingCategoryField = 'NotetakingCategory';
+ public DividerWidth = 16;
@observable docsDraggedRowCol: number[] = [];
@observable _cursor: CursorProperty = 'grab';
@observable _scroll = 0;
@computed get chromeHidden() {
- return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
+ return BoolCast(this.layoutDoc.chromeHidden);
}
// columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
@computed get columnHeaders() {
const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
- const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset'));
- if (needsUnsetCategory) {
- setTimeout(() => columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)));
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
+ if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
+ setTimeout(() => {
+ const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
+ if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
+ if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
+ else this.dataDoc.columnHeaders = new List<SchemaHeaderField>();
+ }
+ });
}
- return columnHeaders;
- }
- // notetakingCategoryField returns the key to accessing a document's column value
- @computed get notetakingCategoryField() {
- return 'NotetakingCategory';
+ return columnHeaders ?? ([] as SchemaHeaderField[]);
}
@computed get headerMargin() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
}
@computed get xMargin() {
- return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth()));
- }
- // dividerWidth returns the width of a CollectionNoteTakingViewDivider
- @computed get dividerWidth() {
- return 32;
+ return NumCast(this.layoutDoc._xMargin, 5);
}
@computed get yMargin() {
- return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5);
+ return NumCast(this.layoutDoc._yMargin, 5);
}
@computed get gridGap() {
return NumCast(this.layoutDoc._gridGap, 10);
@@ -94,22 +87,13 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
// maxColWidth returns the maximum column width, which is slightly less than the total available space.
@computed get maxColWidth() {
- return this.props.PanelWidth() - 2 * this.xMargin;
+ return this.props.PanelWidth();
}
// availableWidth is the total amount of non-divider width. Since widths are stored relatively,
// we use availableWidth to convert from a percentage to a pixel count.
@computed get availableWidth() {
- const numDividers = this.columnHeaders.length - 1;
- return this.maxColWidth - numDividers * this.dividerWidth;
- }
-
- // Documents should NOT have column category fields until entering this view, so the contructor creates the 'New Column'
- // category for the user to then edit later.
- constructor(props: any) {
- super(props);
- if (this.columnHeaders === undefined) {
- this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column', undefined, undefined, 1)]);
- }
+ const numDividers = this.numGroupColumns - 1;
+ return this.maxColWidth - numDividers * this.DividerWidth;
}
// children is passed as a prop to the NoteTakingField, which uses this function
@@ -149,7 +133,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
});
// now we add back in the docs that we're dragging
- if (rowCol.length) {
+ if (rowCol.length && columnHeaders.length > rowCol[1]) {
const offset = 0;
sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged);
}
@@ -209,7 +193,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
};
// let's dive in and get the actual document we want to drag/move around
- focusDocument = (doc: Doc, options?: DocFocusOptions) => {
+ focusDocument = (doc: Doc, options: DocFocusOptions) => {
Doc.BrushDoc(doc);
let focusSpeed = 0;
const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
@@ -217,7 +201,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
const top = found.getBoundingClientRect().top;
const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
if (Math.floor(localTop[1]) !== 0) {
- smoothScroll((focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
}
}
const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
@@ -236,9 +220,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
if (this.props.childOpacity) {
return this.props.childOpacity();
}
- if (this.Document._currentFrame !== undefined) {
- return CollectionFreeFormDocumentView.getValues(doc, NumCast(this.Document._currentFrame))?.opacity;
- }
}
return this.props.styleProvider?.(doc, props, property);
};
@@ -315,12 +296,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
const existingWidth = existingHeader?.width ? existingHeader.width : 0;
- const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth - 2 * this.xMargin : this.maxColWidth - 2 * this.xMargin;
- if (d.type === DocumentType.RTF) {
- return maxWidth;
- }
- const width = d[WidthSym]();
- return width < maxWidth ? width : maxWidth;
+ const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
+ const width = d.fitWidth ? maxWidth : d[WidthSym]();
+ return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth);
}
// how to get the height of a document. Nothing special here.
@@ -332,8 +310,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
if (nw && nh) {
- // const colWid = this.columnWidth / this.numGroupColumns;
- // const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
const docWid = this.getDocWidth(d);
return Math.min(maxHeight, (docWid * nh) / nw);
}
@@ -392,26 +368,27 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
});
// we alter the pivot fields of the docs in case they are moved to a new column.
const colIndex = this.getColumnFromXCoord(xCoord);
- const colHeader = StrCast(this.columnHeaders[colIndex].heading);
+ const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading);
DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader));
// used to notify sections to re-render
this.docsDraggedRowCol.length = 0;
- this.docsDraggedRowCol.push(dropInd, this.getColumnFromXCoord(xCoord));
+ const columnFromCoord = this.getColumnFromXCoord(xCoord);
+ columnFromCoord !== undefined && this.docsDraggedRowCol.push(dropInd, columnFromCoord);
}
};
// getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate).
// This function is used to know which document a column SHOULD be in while it is being dragged.
- getColumnFromXCoord = (xCoord: number): number => {
+ getColumnFromXCoord = (xCoord: number): number | undefined => {
+ let colIndex: number | undefined = undefined;
const numColumns = this.columnHeaders.length;
const coords = [];
let colStartXCoord = 0;
for (let i = 0; i < numColumns; i++) {
coords.push(colStartXCoord);
- colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.dividerWidth;
+ colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.DividerWidth;
}
coords.push(this.PanelWidth);
- let colIndex = 0;
for (let i = 0; i < numColumns; i++) {
if (xCoord > coords[i] && xCoord < coords[i + 1]) {
colIndex = i;
@@ -423,20 +400,16 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
// getDocsFromXCoord returns the docs of a column based on the x-coordinate provided.
getDocsFromXCoord = (xCoord: number): Doc[] => {
- const colIndex = this.getColumnFromXCoord(xCoord);
- const colHeader = StrCast(this.columnHeaders[colIndex].heading);
- // const docs = this.childDocList
- const docs = this.childDocs;
const docsMatchingHeader: Doc[] = [];
- if (docs) {
- docs.map(d => {
- if (d instanceof Promise) return;
- const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
- if (sectionValue.toString() == colHeader) {
- docsMatchingHeader.push(d);
- }
- });
- }
+ const colIndex = this.getColumnFromXCoord(xCoord);
+ const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading);
+ this.childDocs?.map(d => {
+ if (d instanceof Promise) return;
+ const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+ if (sectionValue.toString() == colHeader) {
+ docsMatchingHeader.push(d);
+ }
+ });
return docsMatchingHeader;
};
@@ -511,7 +484,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
this.onPointerMove(true, e.clientX, e.clientY);
docus?.map((doc: Doc) => this.addDocument(doc));
const newDoc = this.childDocs.lastElement();
- const colHeader = StrCast(this.columnHeaders[colInd].heading);
+ const colHeader = colInd === undefined ? 'unset' : StrCast(this.columnHeaders[colInd].heading);
newDoc[this.notetakingCategoryField] = colHeader;
const docs = this.childDocList;
if (docs && targInd !== -1) {
@@ -566,13 +539,13 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
numGroupColumns={this.numGroupColumns}
gridGap={this.gridGap}
pivotField={this.notetakingCategoryField}
- dividerWidth={this.dividerWidth}
+ dividerWidth={this.DividerWidth}
maxColWidth={this.maxColWidth}
availableWidth={this.availableWidth}
PanelWidth={this.PanelWidth}
- key={heading?.heading ?? ''}
+ key={heading?.heading ?? 'unset'}
headings={this.headings}
- heading={heading?.heading ?? ''}
+ heading={heading?.heading ?? 'unset'}
headingObject={heading}
docList={docList}
yMargin={this.yMargin}
@@ -589,15 +562,19 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
@undoBatch
@action
addGroup = (value: string) => {
- for (const header of this.columnHeaders) {
- if (header.heading == value) {
- alert('You cannot use an existing column name. Please try a new column name');
- return value;
+ if (this.columnHeaders) {
+ for (const header of this.columnHeaders) {
+ if (header.heading == value) {
+ alert('You cannot use an existing column name. Please try a new column name');
+ return value;
+ }
}
}
const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
const newColWidth = 1 / (this.numGroupColumns + 1);
- return value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1) ? true : false;
+ value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1);
+ this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]()));
+ return true;
};
onContextMenu = (e: React.MouseEvent): void => {
@@ -642,10 +619,10 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
@computed get buttonMenu() {
const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
if (menuDoc) {
- const width: number = NumCast(menuDoc._width, 30);
- const height: number = NumCast(menuDoc._height, 30);
+ const width = NumCast(menuDoc._width, 30);
+ const height = NumCast(menuDoc._height, 30);
return (
- <div className="buttonMenu-docBtn" style={{ width: width, height: height }}>
+ <div className="buttonMenu-docBtn" style={{ width, height }}>
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
@@ -678,10 +655,10 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti
}
@computed get nativeWidth() {
- return this.props.NativeWidth?.() ?? Doc.NativeWidth(this.layoutDoc);
+ return Doc.NativeWidth(this.layoutDoc);
}
@computed get nativeHeight() {
- return this.props.NativeHeight?.() ?? Doc.NativeHeight(this.layoutDoc);
+ return Doc.NativeHeight(this.layoutDoc);
}
@computed get scaling() {
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 4610da4e3..84d1c0205 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -22,9 +22,6 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from '../EditableView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionNoteTakingView.scss';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
interface CSVFieldColumnProps {
Document: Doc;
@@ -64,19 +61,15 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
// columnWidth returns the width of a column in absolute pixels
@computed get columnWidth() {
- if (!this.props.columnHeaders || !this.props.headingObject) {
- return this.props.maxColWidth;
- }
- if (this.props.columnHeaders.length == 1) {
- return this.props.maxColWidth;
- }
+ if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length === 1) return '100%';
const i = this.props.columnHeaders.indexOf(this.props.headingObject);
- return this.props.columnHeaders[i].width * this.props.availableWidth;
+ return this.props.columnHeaders[i].width * 100 + '%';
}
private dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
+ public static ColumnMargin = 10;
@observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
@observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb';
_ele: HTMLElement | null = null;
@@ -148,15 +141,17 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
@undoBatch
@action
deleteColumn = () => {
- const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
- if (columnHeaders && this.props.headingObject) {
- const index = columnHeaders.indexOf(this.props.headingObject);
+ const acolumnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
+ if (acolumnHeaders && this.props.headingObject) {
+ const index = acolumnHeaders.indexOf(this.props.headingObject);
+ const columnHeaders = acolumnHeaders; // new List<SchemaHeaderField>(acolumnHeaders.map(header => header[Copy]())); // needed for undo to work properly. otherwise we end up changing field values in the undo stack since they are shared by reference
const newColIndex = index > 0 ? index - 1 : 1;
const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined;
const newHeading = newColHeader ? newColHeader.heading : 'unset';
this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading));
const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0;
columnHeaders.splice(index, 1);
+ //Doc.GetProto(this.props.Document).columnHeaders = columnHeaders;
this.props.resizeColumns(false, colWidth, index);
}
};
@@ -247,9 +242,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
ref={this._headerRef}
style={{
marginTop: 2 * this.props.yMargin,
- // width: (this.props.columnWidth) /
- // ((uniqueHeadings.length) || 1)
- width: this.columnWidth - 20,
+ width: 'calc(100% - 5px)',
}}>
<div
className="collectionNoteTakingView-sectionHeader-subCont"
@@ -257,48 +250,41 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}>
<EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
</div>
+ {(this.props.columnHeaders?.length ?? 0) > 1 && (
+ <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" size="lg" />
+ </button>
+ )}
</div>
) : null;
- const templatecols = `${this.columnWidth}px `;
+ const templatecols = this.columnWidth;
const type = this.props.Document.type;
return (
<>
{headingView}
- {
- <div style={{ height: '100%' }}>
- <div
- key={`${heading}-stack`}
- className={`collectionNoteTakingView-Nodes`}
- style={{
- padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
- margin: 'auto',
- width: 'max-content', //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
- height: 'max-content',
- position: 'relative',
- gridGap: this.props.gridGap,
- gridTemplateColumns: templatecols,
- gridAutoRows: '0px',
- }}>
- {this.props.renderChildren(this.props.docList)}
- </div>
+ <div className="collectionNoteTakingView-columnStack">
+ <div
+ key={`${heading}-stack`}
+ className={`collectionNoteTakingView-Nodes`}
+ style={{
+ padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
+ gridGap: this.props.gridGap,
+ gridTemplateColumns: templatecols,
+ }}>
+ {this.props.renderChildren(this.props.docList)}
+ </div>
- {!this.props.chromeHidden && type !== DocumentType.PRES ? (
- <div className="collectionNoteTakingView-DocumentButtons" style={{ width: this.columnWidth - 20, marginBottom: 10 }}>
- <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
- <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
- </div>
- <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
- <EditableView {...this.props.editableViewProps()} />
- </div>
- {this.props.columnHeaders?.length && this.props.columnHeaders.length > 1 && (
- <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}>
- <FontAwesomeIcon icon="trash" size="lg" />
- </button>
- )}
+ {!this.props.chromeHidden && type !== DocumentType.PRES ? (
+ <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
+ <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
+ <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
</div>
- ) : null}
- </div>
- }
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
+ <EditableView {...this.props.editableViewProps()} />
+ </div>
+ </div>
+ ) : null}
+ </div>
</>
);
}
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
index 8d659f790..a1309b11f 100644
--- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -57,7 +57,6 @@ export class CollectionNoteTakingViewDivider extends React.Component<DividerProp
width: 12,
borderRight: '4px solid #282828',
borderLeft: '4px solid #282828',
- margin: '0px 10px',
}}
/>
</div>
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index c296e1172..5a107d2ca 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -10,6 +10,7 @@
border-width: 0 2px 0 2px;
&:hover {
+ cursor: default;
.collectionStackedTimeline-hover {
display: block;
}
@@ -109,14 +110,15 @@
height: 100%;
width: 10px;
pointer-events: all;
- cursor: ew-resize;
z-index: 100;
}
.collectionStackedTimeline-resizer {
right: 0;
+ cursor: e-resize;
}
.collectionStackedTimeline-left-resizer {
left: 0;
+ cursor: w-resize;
}
}
@@ -143,5 +145,6 @@
transform: translate(0, -100%);
font-weight: bold;
+ pointer-events: none;
}
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 2543624d3..7bf798656 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,7 +2,7 @@ import React = require('react');
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
@@ -23,11 +23,13 @@ import { AudioWaveform } from '../AudioWaveform';
import { CollectionSubView } from '../collections/CollectionSubView';
import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
-import { DocAfterFocusFunc, DocFocusFunc, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView';
import { LabelBox } from '../nodes/LabelBox';
import './CollectionStackedTimeline.scss';
import { VideoBox } from '../nodes/VideoBox';
import { ImageField } from '../../../fields/URLField';
+import { StyleProp } from '../StyleProvider';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
export type CollectionStackedTimelineProps = {
Play: () => void;
@@ -244,6 +246,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
if (!wasSelecting) {
this._markerStart = this._markerEnd = this.toTimeline(clientX - rect.x, rect.width);
wasSelecting = true;
+ this._timelineWrapper && (this._timelineWrapper.style.cursor = 'ew-resize');
}
this._markerEnd = this.toTimeline(e.clientX - rect.x, rect.width);
return false;
@@ -260,6 +263,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
}
(!isClick || !wasSelecting) && (this._markerEnd = undefined);
+ this._timelineWrapper && (this._timelineWrapper.style.cursor = '');
}),
(e, doubleTap) => {
if (e.button !== 2) {
@@ -561,7 +565,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
<div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
<div
className="collectionStackedTimeline-timelineContainer"
- style={{ width: this.props.PanelWidth() }}
+ style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }}
onWheel={e => e.stopPropagation()}
onScroll={this.setScroll}
onMouseMove={e => this.isContentActive() && this.onHover(e)}
@@ -691,6 +695,7 @@ interface StackedTimelineAnchorProps {
width: number;
height: number;
toTimeline: (screen_delta: number, width: number) => number;
+ styleProvider?: (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
playLink: (linkDoc: Doc) => void;
setTime: (time: number) => void;
startTag: string;
@@ -801,12 +806,31 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
return [resetTitle];
};
+ innerStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
+ if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) {
+ return (
+ <div className="styleProvider-lock">
+ <FontAwesomeIcon
+ icon={'camera'}
+ style={{ color: 'red' }}
+ onClick={e => {
+ LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey);
+ e.stopPropagation();
+ }}
+ size="lg"
+ />
+ </div>
+ );
+ }
+ return this.props.styleProvider?.(doc, props, property);
+ };
+
// renders anchor LabelBox
renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
const anchor = observable({ view: undefined as any });
- const focusFunc = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, docTransform?: Transform) => {
+ const focusFunc = (doc: Doc, options: DocFocusOptions) => {
this.props.playLink(mark);
- this.props.focus(doc, { willZoom, scale, afterFocus, docTransform });
+ this.props.focus(doc, options);
};
return {
anchor,
@@ -817,6 +841,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
ref={action((r: DocumentView | null) => (anchor.view = r))}
Document={mark}
DataDoc={undefined}
+ styleProvider={this.innerStyleProvider}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={undefined}
LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 71834607c..ba29b1d6f 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -80,13 +80,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
}
@computed get xMargin() {
- return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth()));
+ return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth()));
}
@computed get yMargin() {
- return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5);
- } // 2 * this.gridGap)); }
+ return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth()));
+ }
+
@computed get gridGap() {
- return NumCast(this.layoutDoc._gridGap, 10);
+ return NumCast(this.layoutDoc._gridGap, 5);
}
// are we stacking or masonry?
@computed get isStackingView() {
@@ -250,7 +251,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
};
// let's dive in and get the actual document we want to drag/move around
- focusDocument = (doc: Doc, options?: DocFocusOptions) => {
+ focusDocument = (doc: Doc, options: DocFocusOptions) => {
Doc.BrushDoc(doc);
let focusSpeed = 0;
@@ -259,7 +260,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const top = found.getBoundingClientRect().top;
const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
if (Math.floor(localTop[1]) !== 0) {
- smoothScroll((focusSpeed = doc.presTransition || doc.presTransition === 0 ? NumCast(doc.presTransition) : 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
}
}
const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
@@ -494,8 +495,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
super.onExternalDrop(e, {}, (docs: Doc[]) => {
if (targInd === -1) {
this.addDocument(docs);
- }
- else {
+ } else {
const childDocs = this.childDocList;
if (childDocs) {
childDocs.splice(targInd, 0, ...docs);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e33bb77de..30759b766 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -212,7 +212,7 @@ export function CollectionSubView<X>(moreProps?: X) {
let added = false;
const dropAction = docDragData.dropAction || docDragData.userDropAction;
const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]);
- const someMoved = !docDragData.userDropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
+ const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop));
if ((!dropAction || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) {
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
@@ -446,6 +446,7 @@ export function CollectionSubView<X>(moreProps?: X) {
}
this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
}
+
slowLoadDocuments = async (
files: File[] | string,
options: DocumentOptions,
@@ -456,11 +457,24 @@ export function CollectionSubView<X>(moreProps?: X) {
clientY: number,
addDocument: (doc: Doc | Doc[]) => boolean
) => {
- const disposer = OverlayView.Instance.addElement(<ReactLoading type={'spinningBubbles'} color={'green'} height={250} width={250} />, { x: clientX - 125, y: clientY - 125 });
+ // create placeholder docs
+ // inside placeholder docs have some func that
+
+ let pileUpDoc = undefined;
if (typeof files === 'string') {
- generatedDocuments.push(...(await DocUtils.uploadYoutubeVideo(files, options)));
+ const loading = Docs.Create.LoadingDocument(files, options);
+ generatedDocuments.push(loading);
+ Doc.addCurrentlyLoading(loading);
+ DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
} else {
- generatedDocuments.push(...(await DocUtils.uploadFilesToDocs(files, options)));
+ generatedDocuments.push(
+ ...files.map(file => {
+ const loading = Docs.Create.LoadingDocument(file, options);
+ Doc.addCurrentlyLoading(loading);
+ DocUtils.uploadFileToDoc(file, {}, loading);
+ return loading;
+ })
+ );
}
if (generatedDocuments.length) {
// Creating a dash document
@@ -476,7 +490,8 @@ export function CollectionSubView<X>(moreProps?: X) {
if (completed) completed(set);
else {
if (isFreeformView && generatedDocuments.length > 1) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!);
+ pileUpDoc = DocUtils.pileup(generatedDocuments, options.x as number, options.y as number)!;
+ addDocument(pileUpDoc);
} else {
generatedDocuments.forEach(addDocument);
}
@@ -488,7 +503,6 @@ export function CollectionSubView<X>(moreProps?: X) {
alert('Document upload failed - possibly an unsupported file type.');
}
}
- disposer();
};
}
@@ -501,5 +515,4 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
import { DragManager, dropActionType } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { OverlayView } from '../OverlayView';
import { CollectionView, CollectionViewProps } from './CollectionView';
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index c0561e42c..1aef29b2f 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -2,7 +2,6 @@
.collectionTreeView-container {
transform-origin: top left;
- height: 100%;
}
.collectionTreeView-dropTarget {
border-width: $COLLECTION_BORDER_WIDTH;
@@ -62,7 +61,6 @@
.editableView-container-editing {
display: block;
text-overflow: ellipsis;
- font-size: 1vw;
white-space: nowrap;
}
}
@@ -80,9 +78,7 @@
}
.collectionTreeView-titleBar {
- display: inline-block;
width: 100%;
- height: max-content;
.contentFittingDocumentView {
display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason)
}
@@ -104,6 +100,7 @@
text-overflow: ellipsis;
white-space: pre-wrap;
min-width: 10px;
+ grid-column: 2;
}
.docContainer-system {
font-variant: all-small-caps;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index dce792d19..fe5dc17f5 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -83,7 +83,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
return this.doc === Doc.MyDashboards;
}
- @observable _explainerHeight = 0; // height of the description of the tree view
+ @observable _titleHeight = 0; // height of the title bar
MainEle = () => this._mainEle;
@@ -111,7 +111,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
computeHeight = () => {
if (!this._isDisposing) {
const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', ''));
- const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot());
+ const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6;
this.layoutDoc._autoHeightMargins = bodyHeight;
this.props.setHeight?.(bodyHeight + titleHeight);
}
@@ -298,7 +298,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
}
@computed get titleBar() {
return this.dataDoc === null ? null : (
- <div className="collectionTreeView-titleBar" key={this.doc[Id]} style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}} ref={r => (this._titleRef = r)}>
+ <div
+ className="collectionTreeView-titleBar"
+ ref={action((r: any) => (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.props.ScreenToLocalTransform().Scale))}
+ key={this.doc[Id]}
+ style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}>
{this.outlineMode ? this.documentTitle : this.editableTitle}
</div>
);
@@ -378,40 +382,45 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
return [
- <div
- className="collectionTreeView-contents"
- key="tree"
- style={{
- ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
- overflow: 'auto',
- height: '100%', //this.layoutDoc._autoHeight ? "max-content" : "100%"
- }}>
- {titleBar}
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
+ {!this.buttonMenu && !this.noviceExplainer ? null : (
+ <div className="documentButtonMenu">
+ {this.buttonMenu}
+ {this.noviceExplainer}
+ </div>
+ )}
<div
- className="collectionTreeView-container"
+ className="collectionTreeView-contents"
+ key="tree"
style={{
- transform: this.outlineMode ? `scale(${this.nativeDimScaling})` : '',
- paddingLeft: `${this.marginX()}px`,
- width: this.outlineMode ? `calc(${100 / this.nativeDimScaling}%)` : '',
- }}
- onContextMenu={this.onContextMenu}>
- {!this.buttonMenu && !this.noviceExplainer ? null : (
- <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
- {this.buttonMenu}
- {this.noviceExplainer}
- </div>
- )}
+ ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+ overflow: 'auto',
+ width: '100%',
+ height: '100%',
+ }}>
+ {titleBar}
<div
- className="collectionTreeView-dropTarget"
+ className="collectionTreeView-container"
style={{
- background: background(),
- height: `calc(100% - ${this._explainerHeight}px)`,
- pointerEvents: pointerEvents(),
+ transform: this.outlineMode ? `scale(${this.nativeDimScaling})` : '',
+ paddingLeft: `${this.marginX()}px`,
+ width: this.outlineMode ? `calc(${100 / this.nativeDimScaling}%)` : '',
+ minHeight: `calc(100% - ${this._titleHeight}px)`,
}}
- onWheel={e => e.stopPropagation()}
- onDrop={this.onTreeDrop}
- ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
- <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
+ onContextMenu={this.onContextMenu}>
+ <div
+ className="collectionTreeView-dropTarget"
+ style={{
+ background: background(),
+ pointerEvents: pointerEvents(),
+ height: `max-content`,
+ minHeight: '100%',
+ }}
+ onWheel={e => e.stopPropagation()}
+ onDrop={this.onTreeDrop}
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
+ <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
+ </div>
</div>
</div>
</div>,
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 1ee77d4ce..dcaad5632 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -15,7 +15,6 @@ import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import { DashboardView } from '../DashboardView';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { CollectionCarousel3DView } from './CollectionCarousel3DView';
@@ -42,6 +41,7 @@ interface CollectionViewProps_ extends FieldViewProps {
isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
layoutEngine?: () => string;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
+ setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void;
// property overrides for child documents
childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
@@ -116,6 +116,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
switch (type) {
default:
case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
+ case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />;
case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />;
case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />;
@@ -153,7 +154,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
!Doc.noviceMode && subItems.push({ description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' });
subItems.push({ description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' });
- if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
const existingVm = ContextMenu.Instance.findByDescription(category);
const catItems = existingVm && 'subitems' in existingVm ? existingVm.subitems : [];
catItems.push({ description: 'Add a Perspective...', addDivider: true, noexpand: true, subitems: subItems, icon: 'eye' });
@@ -187,7 +188,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
!Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' });
- if (!Doc.noviceMode) {
+ if (!Doc.noviceMode && false) {
optionItems.push({
description: 'Create Branch',
event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'),
@@ -204,9 +205,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
icon: 'project-diagram',
});
}
- if (this.Document._viewType === CollectionViewType.Docking) {
- optionItems.push({ description: 'Create Dashboard', event: () => DashboardView.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 73574bdb3..9938245ea 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -56,7 +56,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return this._document && Doc.Layout(this._document);
}
@computed get tabColor() {
- return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor)));
+ let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor)));
+ if (tabColor === 'transparent') return 'black';
+ return tabColor;
}
@computed get tabTextColor() {
return this._document?.type === DocumentType.PRES ? 'black' : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color)));
@@ -94,6 +96,18 @@ export class TabDocView extends React.Component<TabDocViewProps> {
const iconWrap = document.createElement('div');
const closeWrap = document.createElement('div');
+ const getChild = () => {
+ let child = this.view?.ContentDiv?.children[0];
+ while (child?.children.length) {
+ const next = Array.from(child.children).find(c => c.className?.toString().includes('SVGAnimatedString') || typeof c.className === 'string');
+ if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
+ if (next?.className?.toString().includes(DashFieldView.name)) break;
+ if (next) child = next;
+ else break;
+ }
+ return child;
+ };
+
titleEle.size = StrCast(doc.title).length + 3;
titleEle.value = doc.title;
titleEle.onkeydown = (e: KeyboardEvent) => {
@@ -117,14 +131,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
action(e => {
if (this.view) {
SelectionManager.SelectView(this.view, false);
- let child = this.view.ContentDiv!.children[0];
- while (child.children.length) {
- const next = Array.from(child.children).find(c => c.className?.toString().includes('SVGAnimatedString') || typeof c.className === 'string');
- if (next?.className?.toString().includes(DocumentView.ROOT_DIV)) break;
- if (next?.className?.toString().includes(DashFieldView.name)) break;
- if (next) child = next;
- else break;
- }
+ const child = getChild();
simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
} else {
this._activated = true;
@@ -163,6 +170,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
};
+ tab.element[0].oncontextmenu = (e: MouseEvent) => {
+ let child = getChild();
+ if (child) {
+ simulateMouseClick(child, e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ };
+
// select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected
titleEle.onpointerdown = action((e: any) => {
if (e.target.className !== 'lm_iconWrap') {
@@ -213,73 +229,75 @@ export class TabDocView extends React.Component<TabDocViewProps> {
public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) {
const docList = docs instanceof Doc ? [docs] : docs;
- // all docs will be added to the ActivePresentation as stored on CurrentUserUtils
- const curPres = Doc.ActivePresentation;
- if (curPres) {
- const batch = UndoManager.StartBatch('pinning doc');
- docList.forEach(doc => {
- // Edge Case 1: Cannot pin document to itself
- if (doc === curPres) {
- alert('Cannot pin presentation document to itself');
- return;
- }
- const pinDoc = Doc.MakeAlias(doc);
- pinDoc.presentationTargetDoc = doc;
- pinDoc.title = doc.title + ' - Slide';
- pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
- pinDoc.presMovement = PresMovement.Zoom;
- pinDoc.groupWithUp = false;
- pinDoc.context = curPres;
- // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
- pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
- pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
- pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
- pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
- pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
- pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header
- pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
- const presArray: Doc[] = PresBox.Instance?.sortArray();
- const size: number = PresBox.Instance?._selectedArray.size;
- const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
- const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
+ const batch = UndoManager.StartBatch('pinning doc');
+ const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
- PresBox.pinDocView(pinDoc, pinProps);
- pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
- Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
- if (!pinProps?.audioRange && duration !== undefined) {
- pinDoc.mediaStart = 'manual';
- pinDoc.mediaStop = 'manual';
- pinDoc.presStartTime = NumCast(doc.clipStart);
- pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
- }
- //save position
- if (pinProps?.activeFrame !== undefined) {
- pinDoc.presActiveFrame = pinProps?.activeFrame;
- pinDoc.title = doc.title + ' (move)';
- pinDoc.presMovement = PresMovement.Pan;
- }
- if (pinDoc.isInkMask) {
- pinDoc.presHideAfter = true;
- pinDoc.presHideBefore = true;
- pinDoc.presMovement = PresMovement.None;
- }
- if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
- PresBox.Instance?.clearSelectedArray();
- pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
- });
- if (
- CollectionDockingView.Instance &&
- !Array.from(CollectionDockingView.Instance.tabMap)
- .map(d => d.DashDoc)
- .includes(curPres)
- ) {
- const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
- if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
- CollectionDockingView.AddSplit(curPres, 'right');
- setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+ if (!Doc.ActivePresentation) {
+ Doc.AddDocToList(Doc.MyTrails, 'data', curPres);
+ Doc.ActivePresentation = curPres;
+ }
+
+ docList.forEach(doc => {
+ // Edge Case 1: Cannot pin document to itself
+ if (doc === curPres) {
+ alert('Cannot pin presentation document to itself');
+ return;
+ }
+ const pinDoc = Doc.MakeAlias(doc);
+ pinDoc.presentationTargetDoc = doc;
+ pinDoc.title = doc.title + ' - Slide';
+ pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
+ pinDoc.presMovement = pinProps?.pinDocView && !pinProps?.pinWithView ? PresMovement.None : PresMovement.Zoom;
+ pinDoc.groupWithUp = false;
+ pinDoc.context = curPres;
+ // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
+ pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
+ pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
+ pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
+ pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
+ pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
+ pinDoc.treeViewGrowsHorizontally = true; // the document expands horizontally when displayed as a tree view header
+ pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
+ const presArray: Doc[] = PresBox.Instance?.sortArray();
+ const size: number = PresBox.Instance?.selectedArray.size;
+ const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
+ const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
+
+ if (!pinProps?.audioRange && duration !== undefined) {
+ pinDoc.mediaStart = 'manual';
+ pinDoc.mediaStop = 'manual';
+ pinDoc.presStartTime = NumCast(doc.clipStart);
+ pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
+ }
+ PresBox.pinDocView(pinDoc, pinProps, doc);
+ pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
+ Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
+ //save position
+ if (pinProps?.activeFrame !== undefined) {
+ pinDoc.presActiveFrame = pinProps?.activeFrame;
+ pinDoc.title = doc.title + ' (move)';
+ pinDoc.presMovement = PresMovement.Pan;
+ }
+ if (pinDoc.isInkMask) {
+ pinDoc.presHideAfter = true;
+ pinDoc.presHideBefore = true;
+ pinDoc.presMovement = PresMovement.None;
}
- setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
+ if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
+ PresBox.Instance?.clearSelectedArray();
+ pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
+ });
+ if (
+ !Array.from(CollectionDockingView.Instance?.tabMap ?? [])
+ .map(d => d.DashDoc)
+ .includes(curPres)
+ ) {
+ const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
+ if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
+ CollectionDockingView.AddSplit(curPres, 'right');
+ setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
}
+ setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
componentDidMount() {
@@ -363,10 +381,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame);
};
@action
- focusFunc = (doc: Doc, options?: DocFocusOptions) => {
+ focusFunc = (doc: Doc, options: DocFocusOptions) => {
const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
- if (shrinkwrap && this._document) {
- const focusSpeed = 1000;
+ if (options?.willZoom !== false && shrinkwrap && this._document) {
+ const focusSpeed = NumCast(this._document.focusSpeed, 500);
shrinkwrap();
this._document._viewTransition = `transform ${focusSpeed}ms`;
setTimeout(
@@ -398,7 +416,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap);
@computed get docView() {
- return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? null : (
+ return !this._activated || !this._document ? null : (
<>
<DocumentView
key={this._document[Id]}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index ce87e6f89..57bb5274d 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,13 +53,16 @@
}
.bullet {
+ grid-column: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
position: relative;
width: $TREE_BULLET_WIDTH;
+ min-height: 20px;
color: $medium-gray;
- margin-top: 3px;
- // transform: scale(1.3, 1.3); // bcz: why was this here? It makes displaying images in the treeView for documents that have icons harder.
border: #80808030 1px solid;
- border-radius: 4px;
+ border-radius: 5px;
}
}
@@ -112,8 +115,15 @@
.treeView-header-editing,
.treeView-header {
+ display: flex; // needed for PresBox's treeView
border: transparent 1px solid;
- display: flex;
+ align-items: center;
+ width: 100%;
+ border-radius: 5px;
+
+ &:hover {
+ background-color: #bdddf5;
+ }
//align-items: center;
::-webkit-scrollbar {
@@ -140,6 +150,7 @@
.treeView-rightButtons {
display: flex;
+ grid-column: 3;
align-items: center;
margin-left: 0.25rem;
opacity: 0.75;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index aa1330762..14563f990 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
@@ -575,7 +575,7 @@ export class TreeView extends React.Component<TreeViewProps> {
<ul
key={expandKey + 'more'}
title="click to change sort order"
- className={this.doc.treeViewHideTitle ? 'no-indent' : ''}
+ className={''} //this.doc.treeViewHideTitle ? 'no-indent' : ''}
onPointerDown={e => {
downX = e.clientX;
downY = e.clientY;
@@ -692,7 +692,7 @@ export class TreeView extends React.Component<TreeViewProps> {
<FontAwesomeIcon size="sm" icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} />
)
) : (
- <div className="treeView-bulletIcons">
+ <div className="treeView-bulletIcons" style={{ color: Doc.IsSystem(DocCast(this.doc.proto)) ? 'red' : undefined }}>
<div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}>
<FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} />
</div>
@@ -736,7 +736,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}}
/>
)}
- {this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
+ {Doc.noviceMode ? null : this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
<span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
{this.treeViewExpandedView}
</span>
@@ -782,7 +782,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);
- refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document);
+ refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document, {});
ignoreEvent = (e: any) => {
if (this.props.isContentActive(true)) {
e.stopPropagation();
@@ -953,7 +953,6 @@ export class TreeView extends React.Component<TreeViewProps> {
<div
className={`treeView-header` + (editing ? '-editing' : '')}
key="titleheader"
- style={{ width: StrCast(this.doc.treeViewHeaderWidth, 'max-content') }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -1038,7 +1037,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderBorder() {
const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None);
- const sortings = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } };
+ const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) ?? {}) as { [key: string]: { color: string; label: string } };
return (
<div className={`treeView-border${this.props.treeView.outlineMode ? TreeViewType.outline : ''}`} style={{ borderColor: sortings[sorting]?.color }}>
{!this.treeViewOpen ? null : this.renderContent}
@@ -1058,7 +1057,7 @@ export class TreeView extends React.Component<TreeViewProps> {
render() {
TraceMobx();
const hideTitle = this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode;
- return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? (
+ return this.props.renderedIds?.indexOf(this.doc[Id]) !== -1 ? (
'<' + this.doc.title + '>' // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
) : (
<div
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 052cbd3bb..0d061a325 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -44,7 +44,6 @@ import { PresBox } from '../../nodes/trails/PresBox';
import { VideoBox } from '../../nodes/VideoBox';
import { CreateImage } from '../../nodes/WebBoxRenderer';
import { StyleProp } from '../../StyleProvider';
-import { CollectionDockingView } from '../CollectionDockingView';
import { CollectionSubView } from '../CollectionSubView';
import { TreeViewType } from '../CollectionTreeView';
import { TabDocView } from '../TabDocView';
@@ -53,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
import React = require('react');
-import e = require('connect-flash');
export type collectionFreeformViewProps = {
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -90,6 +88,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
private _cachedPool: Map<string, PoolData> = new Map();
private _lastTap = 0;
private _batch: UndoManager.Batch | undefined = undefined;
+ private _brushtimer: any;
+ private _brushtimer1: any;
// private isWritingMode: boolean = true;
// private writingModeDocs: Doc[] = [];
@@ -116,10 +116,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _marqueeViewRef = React.createRef<MarqueeView>();
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
+ @observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region
@computed get views() {
- const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1).map(ele => ele.ele);
- const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask === -1).map(ele => ele.ele);
+ const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
+ const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele);
if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>);
return renderableEles;
}
@@ -130,7 +131,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
}
@computed get fitContentsToBox() {
- return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay;
+ return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay;
}
@computed get contentBounds() {
const cb = Cast(this.rootDoc.contentBounds, listSpec('number'));
@@ -154,8 +155,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
+ const dv = this.props.DocumentView?.();
const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
- return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ // if freeform has a native aspect, then the panel height needs to be adjusted to match it
+ const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
+ return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
return Transform.Identity()
@@ -189,6 +193,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
elementFunc = () => this._layoutElements;
shrinkWrap = () => {
+ if (this.props.DocumentView?.().nativeWidth) return;
const vals = this.fitToContentVals;
this.layoutDoc._panX = vals.bounds.cx;
this.layoutDoc._panY = vals.bounds.cy;
@@ -233,11 +238,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const newBoxes = newBox instanceof Doc ? [newBox] : newBox;
for (const newBox of newBoxes) {
if (newBox.activeFrame !== undefined) {
- const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field]);
- CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field}-indexed`]);
- CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field]);
+ const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]);
+ CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}-indexed`]);
+ CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]);
delete newBox.activeFrame;
- CollectionFreeFormDocumentView.animFields.forEach((field, i) => field !== 'opacity' && (newBox[field] = vals[i]));
+ CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i]));
}
}
if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) {
@@ -275,10 +280,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const layoutDoc = Doc.Layout(d);
if (this.Document._currentFrame !== undefined) {
CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false);
- const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000));
- vals.x = x + NumCast(vals.x) - dropPos[0];
- vals.y = y + NumCast(vals.y) - dropPos[1];
- vals._scrollTop = this.Document.editScrollProgressivize ? vals._scrollTop : undefined;
+ const pvals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); // get filled in values (uses defaults when not value is specified) for position
+ const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000), false); // get non-default values for everything else
+ vals.x = x + NumCast(pvals.x) - dropPos[0];
+ vals.y = y + NumCast(pvals.y) - dropPos[1];
CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals);
} else {
d.x = x + NumCast(d.x) - dropPos[0];
@@ -1100,10 +1105,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
if (this.Document._isGroup) return;
- setTimeout(
- action(() => (this._viewTransition = 0)),
- (this._viewTransition = transitionTime)
- ); // set transition to be smooth, then reset
+ this._viewTransition = transitionTime;
const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
this.layoutDoc[this.scaleFieldKey] = scale;
const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
@@ -1111,9 +1113,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
+ return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset
}
- focusDocument = (doc: Doc, options?: DocFocusOptions) => {
+ focusDocument = (doc: Doc, options: DocFocusOptions) => {
const state = HistoryUtil.getState();
// TODO This technically isn't correct if type !== "doc", as
@@ -1132,22 +1135,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
// SelectionManager.DeselectAll();
// }
- if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) {
+ if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
this.props.focus(doc, options);
} else {
const xfToCollection = options?.docTransform ?? Transform.Identity();
const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined };
const newState = HistoryUtil.getState();
- const cantTransform = /*this.props.isAnnotationOverlay ||*/ (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
+ const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined);
if (!cantTransform) {
// only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
- newState.initializers![this.Document[Id]] = { panX: panX, panY: panY };
+ newState.initializers![this.Document[Id]] = { panX, panY };
HistoryUtil.pushState(newState);
}
// focus on the document in the collection
const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
- const focusSpeed = options?.instant ? 0 : didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0;
+ const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0;
// glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
if (didMove) {
scale && (this.Document[this.scaleFieldKey] = scale);
@@ -1166,8 +1169,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.Document._panY = restoreState.panY;
this.Document[this.scaleFieldKey] = restoreState.scale;
}
- runInAction(() => (this._viewTransition = 0));
}
+ runInAction(() => (this._viewTransition = 0));
return resetView;
};
const xf = !cantTransform
@@ -1176,7 +1179,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
? new Transform(NumCast(this.rootDoc.x), NumCast(this.rootDoc.y), this.rootDoc[WidthSym]() / Doc.NativeWidth(this.rootDoc))
: new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1);
- this.props.focus(cantTransform ? doc : this.rootDoc, {
+ this.props.focus(!cantTransform ? this.rootDoc : doc, {
...options,
docTransform: xf,
afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), Math.max(0, focusSpeed - (Date.now() - startTime)))),
@@ -1199,10 +1202,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
scale: newScale,
};
}
- const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1);
- const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1);
- const cx = NumCast(this.layoutDoc._panX);
- const cy = NumCast(this.layoutDoc._panY);
+
+ const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth();
+ const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight();
+ const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1);
+ const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1);
+ const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
+ const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) {
return {
@@ -1212,8 +1218,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
}
return {
- panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
- panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot),
+ panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+ panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot),
};
};
@@ -1424,15 +1430,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@computed get doInternalLayoutComputation() {
TraceMobx();
const newPool = new Map<string, PoolData>();
+ // prettier-ignore
switch (this.layoutEngine) {
- case 'pass':
- return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
- case 'timeline':
- return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
- case 'pivot':
- return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
- case 'starburst':
- return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
+ case 'pass': return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
+ case 'timeline': return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
+ case 'pivot': return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
+ case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
}
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
@@ -1453,7 +1456,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
newPos.x !== lastPos.x ||
newPos.y !== lastPos.y ||
newPos.z !== lastPos.z ||
- newPos.zIndex !== lastPos.zIndex
+ newPos.zIndex !== lastPos.zIndex ||
+ newPos.transition !== lastPos.transition
) {
this._layoutPoolData.set(entry[0], newPos);
}
@@ -1521,14 +1525,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
componentDidMount() {
super.componentDidMount?.();
this.props.setContentView?.(this);
+ this.props.setBrushViewer?.(this.brushView);
setTimeout(
action(() => {
this._firstRender = false;
- this._disposers.layoutComputation = reaction(
- () => this.doLayoutComputation,
- elements => (this._layoutElements = elements || []),
- { fireImmediately: true, name: 'doLayout' }
- );
this._marqueeRef.current?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
@@ -1545,10 +1545,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2];
const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)];
const pbounds = {
- x: (cbounds.x - p[0]) * this.zoomScaling() + c[0],
- y: (cbounds.y - p[1]) * this.zoomScaling() + c[1],
- r: (cbounds.r - p[0]) * this.zoomScaling() + c[0],
- b: (cbounds.b - p[1]) * this.zoomScaling() + c[1],
+ x: cbounds.x - p[0] + c[0],
+ y: cbounds.y - p[1] + c[1],
+ r: cbounds.r - p[0] + c[0],
+ b: cbounds.b - p[1] + c[1],
};
this.layoutDoc._width = pbounds.r - pbounds.x;
this.layoutDoc._height = pbounds.b - pbounds.y;
@@ -1560,6 +1560,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
},
{ fireImmediately: true }
);
+
+ this._disposers.layoutComputation = reaction(
+ () => this.doLayoutComputation,
+ elements => (this._layoutElements = elements || []),
+ { fireImmediately: true, name: 'doLayout' }
+ );
})
);
}
@@ -1841,7 +1847,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@computed get placeholder() {
return (
<div className="collectionfreeformview-placeholder" style={{ background: StrCast(this.Document.backgroundColor) }}>
- <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span>
+ <span className="collectionfreeformview-placeholderSpan">{this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()}</span>
</div>
);
}
@@ -1855,6 +1861,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined}
nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge}
addDocTab={this.addDocTab}
+ slowLoadDocuments={this.slowLoadDocuments}
trySelectCluster={this.trySelectCluster}
activeDocuments={this.getActiveDocuments}
selectDocuments={this.selectDocuments}
@@ -1880,6 +1887,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
</div>
) : null}
<CollectionFreeFormViewPannableContents
+ brushView={this._brushedView}
isAnnotationOverlay={this.isAnnotationOverlay}
isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
@@ -1915,9 +1923,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
};
+ @action
+ brushView = (viewport: { width: number; height: number; panX: number; panY: number }) => {
+ this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2, opacity: 1 };
+ this._brushtimer1 && clearTimeout(this._brushtimer1);
+ this._brushtimer && clearTimeout(this._brushtimer);
+ this._brushtimer1 = setTimeout(
+ action(() => {
+ this._brushedView.opacity = 0;
+ this._brushtimer = setTimeout(
+ action(() => (this._brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 })),
+ 500
+ );
+ }),
+ 1000
+ );
+ };
+
render() {
TraceMobx();
- const clientRect = this._mainCont?.getBoundingClientRect();
return (
<div
className={'collectionfreeformview-container'}
@@ -2003,6 +2027,7 @@ interface CollectionFreeFormViewPannableContentsProps {
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
isAnnotationOverlayScrollable: boolean | undefined;
+ brushView: { panX: number; panY: number; width: number; height: number; opacity: number };
}
@observer
@@ -2122,6 +2147,21 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
//willChange: "transform"
}}>
{this.props.children()}
+ {!this.props.brushView.width ? null : (
+ <div
+ className="collectionFreeFormView-brushView"
+ style={{
+ zIndex: 1000,
+ opacity: this.props.brushView.opacity,
+ border: 'orange solid 2px',
+ position: 'absolute',
+ transform: `translate(${this.props.brushView.panX}px, ${this.props.brushView.panY}px)`,
+ width: this.props.brushView.width,
+ height: this.props.brushView.height,
+ transition: 'opacity 2s',
+ }}
+ />
+ )}
{this.presPaths}
{this.progressivize}
{this.zoomProgressivize}
@@ -2199,7 +2239,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview
- ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5);
+ await ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5);
}
return ViewAdjustment.doNothing;
},
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 41e4d6b6a..e0f5cbe5b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,16 +1,14 @@
-
.marqueeView {
position: inherit;
- top:0;
- left:0;
- width:100%;
- height:100%;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
overflow: hidden;
border-radius: inherit;
user-select: none;
}
-
.marqueeView:focus-within {
overflow: hidden;
}
@@ -22,13 +20,13 @@
border-color: black;
pointer-events: none;
.marquee-legend {
- bottom:-18px;
- left:0;
+ bottom: -18px;
+ left: 0;
position: absolute;
font-size: 9;
- white-space:nowrap;
+ white-space: nowrap;
}
.marquee-legend::after {
- content: "Press <space> for lasso"
+ content: 'Press <space> for lasso';
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 65a11cbcb..584c9690f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -40,6 +40,16 @@ interface MarqueeViewProps {
nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
ungroup?: () => void;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
+ slowLoadDocuments: (
+ files: File[] | string,
+ options: DocumentOptions,
+ generatedDocuments: Doc[],
+ text: string,
+ completed: ((doc: Doc[]) => void) | undefined,
+ clientX: number,
+ clientY: number,
+ addDocument: (doc: Doc | Doc[]) => boolean
+ ) => Promise<void>;
}
export interface MarqueeViewBounds {
@@ -330,7 +340,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
- PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge);
+ PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments);
}
this.clearSelection();
}
@@ -415,13 +425,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@undoBatch
@action
pinWithView = async () => {
- const scale = Math.min(this.props.PanelWidth() / this.Bounds.width, this.props.PanelHeight() / this.Bounds.height);
const doc = this.props.Document;
const viewOptions: PinViewProps = {
bounds: this.Bounds,
- scale: scale,
};
- TabDocView.PinDoc(doc, { pinWithView: viewOptions });
+ TabDocView.PinDoc(doc, { pinWithView: viewOptions, pinDocView: true });
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
};
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 0d7d67dd8..92f6bbb64 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -198,13 +198,7 @@ export class CollectionLinearView extends CollectionSubView() {
<div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: BoolCast(this.layoutDoc.linearViewIsExpanded) ? undefined : 'transparent' }}>
<div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu}>
{!expandable ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? 'Close' : 'Open'}</div>
- </>
- }
- placement="top">
+ <Tooltip title={<div className="dash-tooltip">{BoolCast(this.props.Document.linearViewIsExpanded) ? 'Close' : 'Open'}</div>} placement="top">
{menuOpener}
</Tooltip>
)}
@@ -213,7 +207,17 @@ export class CollectionLinearView extends CollectionSubView() {
type="checkbox"
checked={BoolCast(this.props.Document.linearViewIsExpanded)}
ref={this.addMenuToggle}
- onChange={action(() => (this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked))}
+ onChange={action(e => {
+ ScriptCast(this.Document.onClick)?.script.run({
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ _readOnly_: false,
+ scriptContext: this.props.scriptContext,
+ thisContainer: this.props.ContainingCollectionDoc,
+ documentView: this.props.docViewPath().lastElement(),
+ });
+ this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked;
+ })}
/>
<div
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index c5968d9a7..e68f9abe3 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -7,6 +7,10 @@ $medium-gray: #9f9f9f;
$dark-gray: #323232;
$black: #000000;
+$close-red: red;
+$minimize-yellow: yellow;
+$open-green: green;
+
$light-blue: #bdddf5;
$light-blue-transparent: #bdddf590;
$medium-blue: #4476f7;
@@ -39,11 +43,11 @@ $small-text: 9px;
// misc values
$border-radius: 0.3em;
$search-thumnail-size: 130;
-$topbar-height: 32px;
+$topbar-height: 37px;
$antimodemenu-height: 36px;
// dragged items
-$contextMenu-zindex: 100000; // context menu shows up over everything
+$contextMenu-zindex: 100002; // context menu shows up over everything
$radialMenu-zindex: 100000; // context menu shows up over everything
// borders
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 7bbd7c055..f96be9eef 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -349,8 +349,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`);
Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
- newDoc.x = this.rootDoc.x;
- newDoc.y = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ newDoc.overlayX = this.rootDoc.x;
+ newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
} else {
this.props.addDocument?.(newDoc);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 86566ac6a..a48906372 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,12 +1,14 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
+import { InkField } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { numberRange } from '../../../Utils';
+import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
@@ -16,11 +18,6 @@ import { StyleProp } from '../StyleProvider';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps } from './DocumentView';
import React = require('react');
-import { InkField } from '../../../fields/InkField';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { Field } from '../../util/ProsemirrorCopy/prompt';
-import { RefField } from '../../../fields/RefField';
-import { ObjectField } from '../../../fields/ObjectField';
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
@@ -36,9 +33,20 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
- public static animFields = ['_height', '_width', 'x', 'y', '_scrollTop', 'opacity']; // fields that are configured to be animatable using animation frames
+ public static animFields: { key: string; val?: number }[] = [
+ { key: '_height' },
+ { key: '_width' },
+ { key: 'x' },
+ { key: 'y' },
+ { key: '_jitterRotation', val: 0 },
+ { key: '_scrollTop' },
+ { key: 'opacity', val: 1 },
+ { key: 'viewScale', val: 1 },
+ { key: 'panX' },
+ { key: 'panY' },
+ ]; // fields that are configured to be animatable using animation frames
public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
- public static animDataFields = ['data', 'text']; // fields that are configured to be animatable using animation frames
+ public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
@observable _animPos: number[] | undefined = undefined;
@observable _contentView: DocumentView | undefined | null;
get displayName() {
@@ -62,10 +70,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return this.dataProvider?.opacity;
}
get BackgroundColor() {
- return this.dataProvider?.backgroundColor;
+ return this.dataProvider?.backgroundColor ?? Cast(this.Document._backgroundColor, 'string', null);
}
get Color() {
- return this.dataProvider?.color;
+ return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null);
}
get Highlight() {
return this.dataProvider?.highlight;
@@ -88,16 +96,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return this.props.styleProvider?.(doc, props, property);
};
- public static getValues(doc: Doc, time: number) {
+ public static getValues(doc: Doc, time: number, fillIn: boolean = true) {
return CollectionFreeFormDocumentView.animFields.reduce((p, val) => {
- p[val] = Cast(`${val}-indexed`, listSpec('number'), [NumCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number);
+ p[val.key] = Cast(doc[`${val.key}-indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number);
return p;
}, {} as { [val: string]: Opt<number> });
}
public static getStringValues(doc: Doc, time: number) {
return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => {
- p[val] = Cast(`${val}-indexed`, listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
+ p[val] = Cast(doc[`${val}-indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string);
return p;
}, {} as { [val: string]: Opt<string> });
}
@@ -117,14 +125,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
action(doc => {
doc._viewTransition = doc.dataTransition = 'all 1s';
CollectionFreeFormDocumentView.animFields.forEach(val => {
- const findexed = Cast(doc[`${val}-indexed`], listSpec('number'), null);
+ const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
});
CollectionFreeFormDocumentView.animStringFields.forEach(val => {
const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
});
- CollectionFreeFormDocumentView.animDataFields.forEach(val => {
+ CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
findexed?.length <= timecode + 1 && findexed.push(undefined as any);
});
@@ -140,8 +148,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
);
}
- public static gotoKeyframe(docs: Doc[]) {
- docs.forEach(doc => (doc._viewTransition = doc.dataTransition = 'all 1s'));
+ public static gotoKeyframe(docs: Doc[], duration = 1000) {
+ docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
setTimeout(
() =>
docs.forEach(doc => {
@@ -174,9 +182,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
// opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in
doc['opacity-indexed'] = new List<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
}
- CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedNumber(val, 'activeFrame', doc, currTimecode)));
+ CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
- CollectionFreeFormDocumentView.animDataFields.forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
+ CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
@@ -226,7 +234,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
};
- const mixBlendMode = undefined; // (StrCast(this.layoutDoc.mixBlendMode) as any) || (typeof background === 'string' && background && !background.startsWith('linear') && DashColor(background).alpha() !== 1 ? 'multiply' : undefined);
return (
<div
className={'collectionFreeFormDocumentView-container'}
@@ -238,7 +245,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
transformOrigin: '50% 50%',
transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
zIndex: this.ZInd,
- mixBlendMode: mixBlendMode,
display: this.ZInd === -99 ? 'none' : undefined,
}}>
{this.props.renderCutoffProvider(this.props.Document) ? (
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d065c62fb..6d6609317 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -43,6 +43,7 @@ import { VideoBox } from './VideoBox';
import { WebBox } from './WebBox';
import React = require('react');
import XRegExp = require('xregexp');
+import { LoadingBox } from './LoadingBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -266,6 +267,7 @@ export class DocumentContentsView extends React.Component<
DataVizBox,
HTMLtag,
ComparisonBox,
+ LoadingBox,
}}
bindings={bindings}
jsx={layoutFrame}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index a37de7f69..9ffbf8e37 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
import { StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { Hypothesis } from '../../util/HypothesisUtils';
@@ -287,7 +287,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
) : null}
{this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
- <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+ <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
) : null}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 9aaaf1e68..6cadeec41 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -2,6 +2,7 @@
.documentView-effectsWrapper {
border-radius: inherit;
+ transition: inherit;
}
// documentViews have a docView-hack tag which is replaced by this tag when capturing bitmaps (when the dom is converted to an html string)
@@ -25,7 +26,6 @@
height: 100%;
border-radius: inherit;
transition: outline 0.3s linear;
- cursor: grab;
// background: $white; //overflow: hidden;
transform-origin: left top;
@@ -212,10 +212,12 @@
display: flex;
width: 100%;
height: 100%;
+ transition: inherit;
.contentFittingDocumentView-previewDoc {
position: relative;
display: inline;
+ transition: inherit;
}
.contentFittingDocumentView-input {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 74143a731..145d8bf3d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,8 +1,9 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
@@ -10,7 +11,7 @@ import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -20,6 +21,7 @@ import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
+import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
@@ -37,7 +39,6 @@ import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
-import { InkingStroke } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
@@ -52,8 +53,6 @@ import { RadialMenu } from './RadialMenu';
import { ScriptingBox } from './ScriptingBox';
import { PresBox } from './trails/PresBox';
import React = require('react');
-import { DictationManager } from '../../util/DictationManager';
-import { Tooltip } from '@material-ui/core';
const { Howl } = require('howler');
interface Window {
@@ -81,12 +80,13 @@ export interface DocFocusOptions {
instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
}
export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
-export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void;
+export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
export interface DocComponentView {
updateIcon?: () => void; // updates the icon representation of the document
getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
+ brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
@@ -498,8 +498,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
.ScreenToLocalTransform()
.scale(this.NativeDimScaling)
.transformDirection(x - left, y - top);
- dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]);
- dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
+ // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
+ // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
dragData.dropAction = dropAction;
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
@@ -534,7 +534,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
};
- focus = (anchor: Doc, options?: DocFocusOptions) => {
+ focus = (anchor: Doc, options: DocFocusOptions) => {
LightboxView.SetCookie(StrCast(anchor['cookies-set']));
// copying over VIEW fields immediately allows the view type to switch to create the right _componentView
Array.from(Object.keys(Doc.GetProto(anchor)))
@@ -582,7 +582,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
console.log
);
UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
- } else if (!Doc.IsSystem(this.rootDoc)) {
+ } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
@@ -881,9 +881,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const existingOnClick = cm.findByDescription('OnClick...');
const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
- const zorders = cm.findByDescription('ZOrder...');
- const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
if (this.props.bringToFront !== emptyFunction) {
+ const zorders = cm.findByDescription('ZOrder...');
+ const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'expand-arrows-alt' });
zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'expand-arrows-alt' });
zorderItems.push({
@@ -891,8 +891,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))),
icon: 'expand-arrows-alt',
});
+ !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
}
- !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
!Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
!Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
@@ -1001,7 +1001,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
LightboxView.LightboxDoc !== this.rootDoc &&
this.thumb &&
!Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
- !Doc.isBrushedHighlightedDegree(this.props.Document) &&
+ (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) &&
!this._componentView?.isAnyChildContentActive?.()
? true
: false;
@@ -1259,46 +1259,47 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
);
- const titleView = !showTitle ? null : (
- <div
- className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
- key="title"
- style={{
- position: this.headerMargin ? 'relative' : 'absolute',
- height: this.titleHeight,
- width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
- color: lightOrDark(background),
- background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
- }}>
- <EditableView
- ref={this._titleRef}
- contents={showTitle
- .split(';')
- .map(field => field.trim())
- .map(field => targetDoc[field]?.toString())
- .join('\\')}
- display={'block'}
- fontSize={10}
- GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
- SetValue={undoBatch((input: string) => {
- if (input?.startsWith('#')) {
- if (this.props.showTitle) {
- this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
+ const titleView =
+ !showTitle || Doc.noviceMode ? null : (
+ <div
+ className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
+ key="title"
+ style={{
+ position: this.headerMargin ? 'relative' : 'absolute',
+ height: this.titleHeight,
+ width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
+ color: lightOrDark(background),
+ background,
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ }}>
+ <EditableView
+ ref={this._titleRef}
+ contents={showTitle
+ .split(';')
+ .map(field => field.trim())
+ .map(field => targetDoc[field]?.toString())
+ .join('\\')}
+ display={'block'}
+ fontSize={10}
+ GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
+ SetValue={undoBatch((input: string) => {
+ if (input?.startsWith('#')) {
+ if (this.props.showTitle) {
+ this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
+ } else {
+ Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
+ }
} else {
- Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
+ var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
+ if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
+ if (showTitle.includes('Date') || showTitle === 'author') return true;
+ Doc.SetInPlace(targetDoc, showTitle, value, true);
}
- } else {
- var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
- if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
- if (showTitle.includes('Date') || showTitle === 'author') return true;
- Doc.SetInPlace(targetDoc, showTitle, value, true);
- }
- return true;
- })}
- />
- </div>
- );
+ return true;
+ })}
+ />
+ </div>
+ );
return this.props.hideTitle || (!showTitle && !showCaption) ? (
this.contents
) : (
@@ -1322,7 +1323,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observable _isHovering = false;
@observable _: string = '';
_hoverTimeout: any = undefined;
- @computed get renderDoc() {
+ renderDoc = (style: object) => {
TraceMobx();
const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
const isButton = this.props.Document.type === DocumentType.FONTICON;
@@ -1344,8 +1345,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
);
})}
style={{
+ ...style,
background: isButton || thumb ? undefined : this.backgroundColor,
opacity: this.opacity,
+ cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
color: StrCast(this.layoutDoc.color, 'inherit'),
fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
fontSize: Cast(this.Document._fontSize, 'string', null),
@@ -1358,7 +1361,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
)
);
- }
+ };
render() {
TraceMobx();
const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : Doc.DocBrushStatus.unbrushed) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
@@ -1369,13 +1372,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== '[pres element template]'; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
- const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
const boxShadow = this.props.treeViewDoc
? null
: highlighting && this.borderRounding && highlightStyle !== 'dashed'
? `0 0 0 ${highlightIndex}px ${highlightColor}`
: this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+ const renderDoc = this.renderDoc({
+ borderRadius: this.borderRounding,
+ outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
+ border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
+ boxShadow,
+ clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
+ });
+ const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc;
// Return surrounding highlight
return (
<div
@@ -1391,19 +1401,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
display: this.hidden ? 'inline' : undefined,
borderRadius: this.borderRounding,
pointerEvents: this.pointerEvents,
- outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : 'solid 0px',
- border: highlighting && this.borderRounding && highlightStyle === 'dashed' ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
- boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
}}>
{!borderPath.path ? (
- internal
+ animRenderDoc
) : (
<>
{/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
- {internal}
+ {animRenderDoc}
</div> */}
- {internal}
+ {animRenderDoc}
<div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
<svg style={{ overflow: 'visible' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
<path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
@@ -1552,7 +1558,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
- focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options);
+ focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options);
getBounds = () => {
if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index dc3fc0396..04d252abe 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -379,7 +379,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
* Changes the title of the filterDoc
*/
onTitleValueChange = (val: string) => {
- this.props.Document.title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`;
+ Doc.GetProto(this.props.Document).title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`;
return true;
};
@@ -389,11 +389,9 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get flyoutPanel() {
return DocListCast(Doc.UserDoc().savedFilters).map(doc => {
return (
- <>
- <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 50, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}>
- {StrCast(doc.title)}
- </div>
- </>
+ <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 20, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}>
+ {StrCast(doc.title)}
+ </div>
);
});
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9590bcb15..959c641a8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -74,7 +74,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
selected: this.props.isSelected(),
}),
- ({ forceFull, scrSize, selected }) => (this._curSuffix = this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 || !selected ? '_l' : '_o'),
+ ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
{ fireImmediately: true, delay: 1000 }
);
this._disposers.path = reaction(
@@ -136,6 +136,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
crop = (region: Doc | undefined, addCrop?: boolean) => {
if (!region) return;
const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).backgroundColor = 'transparent';
Doc.GetProto(region).lockedPosition = true;
Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
Doc.GetProto(region).isPushpin = true;
@@ -230,7 +231,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return url.href; //Why is this here
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
return url.href.replace(ext, this._curSuffix + ext);
@@ -339,7 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
)}
</div>
- {this.considerDownloadIcon}
+ {!Doc.noviceMode && this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
<FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} />
</div>
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 93ca22d5d..27e79a83b 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -228,7 +228,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
<DocumentView
ref={r => {
const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor);
+ targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor, {});
}}
Document={this._targetDoc!}
moveDocument={returnFalse}
@@ -249,7 +249,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
searchFilterDocs={returnEmptyDoclist}
ContainingCollectionDoc={undefined}
ContainingCollectionView={undefined}
- renderDepth={-1}
+ renderDepth={0}
suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
new file mode 100644
index 000000000..d63ed2575
--- /dev/null
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -0,0 +1,35 @@
+.loadingBoxContainer {
+ display: flex;
+ flex-direction: column;
+ align-content: center;
+ justify-content: center;
+ background-color: #fdfdfd;
+ height: 100%;
+ align-items: center;
+}
+
+.textContainer {
+ margin: 5px;
+}
+
+.textContainer {
+ justify-content: center;
+ align-content: center;
+}
+
+.textContainer,
+.text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 80%;
+ text-align: center;
+}
+
+.headerText {
+ text-align: center;
+ font-weight: bold;
+}
+
+.spinner {
+ text-align: center;
+}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
new file mode 100644
index 000000000..8c5255f80
--- /dev/null
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -0,0 +1,68 @@
+import { action, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import ReactLoading from 'react-loading';
+import { Doc } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
+import { Networking } from '../../Network';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from './FieldView';
+import './LoadingBox.scss';
+
+/**
+ * LoadingBox Class represents a placeholder doc for documents that are currently
+ * being uploaded to the server and being fetched by the client. The LoadingBox doc is then used to
+ * generate the actual type of doc that is required once the document has been successfully uploaded.
+ *
+ * Design considerations:
+ * We are using the docToFiles map in Documents to keep track of all files being uploaded in one session of the client.
+ * If the file is not found we assume an error has occurred with the file upload, e.g. it has been interrupted by a client refresh
+ * or network issues. The docToFiles essentially gets reset everytime the page is refreshed.
+ *
+ * TODOs:
+ * 1) ability to query server to retrieve files that already exist if users upload duplicate files.
+ * 2) ability to restart upload if there is an error
+ * 3) detect network error and notify the user
+ * 4 )if file upload gets interrupted, it still gets uploaded to the server if there are no network interruptions which leads to unused space. this could be
+ * handled with (1)
+ * 5) Fixing the stacking view bug
+ * 6) Fixing the CSS
+ *
+ * @author naafiyan
+ */
+@observer
+export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(LoadingBox, fieldKey);
+ }
+
+ _timer: any;
+ @observable progress = '';
+ componentDidMount() {
+ if (!Doc.CurrentlyLoading?.includes(this.rootDoc)) {
+ this.rootDoc.loadingError = 'Upload interrupted, please try again';
+ } else {
+ const updateFunc = async () => {
+ const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc.title));
+ runInAction(() => (this.progress = result.progress));
+ this._timer = setTimeout(updateFunc, 1000);
+ };
+ this._timer = setTimeout(updateFunc, 1000);
+ }
+ }
+ componentWillUnmount() {
+ clearTimeout(this._timer);
+ }
+
+ render() {
+ return (
+ <div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}>
+ <div className="textContainer">
+ <p className="headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p>
+ <span className="text">{StrCast(this.rootDoc.title)}</span>
+ {this.rootDoc.loadingError ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 345407c2f..c7001f846 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -18,6 +18,7 @@ import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { Colors } from '../global/globalEnums';
+import { LightboxView } from '../LightboxView';
import { CreateImage } from '../nodes/WebBoxRenderer';
import { PDFViewer } from '../pdf/PDFViewer';
import { SidebarAnnos } from '../SidebarAnnos';
@@ -195,6 +196,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
+ brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
+ this._pdfViewer?.brushView(view);
+ };
scrollFocus = (doc: Doc, smooth: boolean) => {
let didToggle = false;
if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
@@ -438,7 +442,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected();
+ isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index 2e6f6bc26..287cccd8f 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -41,30 +41,22 @@ button {
.controls {
display: flex;
align-items: center;
- justify-content: space-evenly;
+ justify-content: center;
position: absolute;
- // padding: 14px;
- //width: 100%;
- max-width: 500px;
- // max-height: 20%;
+ width: 100%;
flex-wrap: wrap;
background: rgba(255, 255, 255, 0.25);
box-shadow: 0 8px 32px 0 rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(4px);
+ // backdrop-filter: blur(4px);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.18);
- // transform: translateY(150%);
transition: all 0.3s ease-in-out;
- // opacity: 0%;
bottom: 34.5px;
height: 60px;
- right: 2px;
- // bottom: -150px;
}
.controls:active {
bottom: 40px;
- // bottom: -150px;
}
.actions button {
@@ -134,8 +126,10 @@ button {
.controls-inner-container {
display: flex;
flex-direction: row;
- align-content: center;
position: relative;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
}
.record-button-wrapper {
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 1c9b0bc0e..4883ad538 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -397,8 +397,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
onPointerDown={e => e.stopPropagation()}
onChange={e => this.viewChanged(e, parameter)}
value={typeof this.rootDoc[parameter] === 'string' ? 'S' + StrCast(this.rootDoc[parameter]) : typeof this.rootDoc[parameter] === 'number' ? 'N' + NumCast(this.rootDoc[parameter]) : 'B' + BoolCast(this.rootDoc[parameter])}>
- {types.map(type => (
- <option className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}>
+ {types.map((type, i) => (
+ <option key={i} className="scriptingBox-viewOption" value={(typeof type === 'string' ? 'S' : typeof type === 'number' ? 'N' : 'B') + type}>
{' '}
{type.toString()}{' '}
</option>
@@ -666,7 +666,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const definedParameters = !this.compileParams.length ? null : (
<div className="scriptingBox-plist" style={{ width: '30%' }}>
{this.compileParams.map((parameter, i) => (
- <div className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
<EditableView
display={'block'}
maxHeight={72}
@@ -745,7 +745,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
{!this.compileParams.length || !this.paramsNames ? null : (
<div className="scriptingBox-plist">
{this.paramsNames.map((parameter: string, i: number) => (
- <div className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+ <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
<div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}>
<div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div>
{this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null}
@@ -805,7 +805,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// renders script UI if _applied = false and params UI if _applied = true
render() {
- console.log(ReactTextareaAutocomplete);
TraceMobx();
return (
<div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}>
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index aa51714da..5e1359441 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
.mini-viewer {
cursor: grab;
@@ -83,6 +83,8 @@
.videoBox-ui-wrapper {
width: 0;
height: 0;
+ position: relative;
+ z-index: 100001;
}
.videoBox-ui {
@@ -97,7 +99,7 @@
height: 40px;
padding: 0 10px 0 7px;
transition: opacity 0.3s;
- z-index: 100001;
+ z-index: 10001;
.timecode-controls {
display: flex;
@@ -114,7 +116,8 @@
}
}
- .toolbar-slider.volume, .toolbar-slider.zoom {
+ .toolbar-slider.volume,
+ .toolbar-slider.zoom {
width: 50px;
}
@@ -157,7 +160,8 @@
}
}
-.videoBox-content-fullScreen, .videoBox-content-fullScreen-interactive {
+.videoBox-content-fullScreen,
+.videoBox-content-fullScreen-interactive {
display: flex;
justify-content: center;
align-items: flex-end;
@@ -175,16 +179,16 @@ video::-webkit-media-controls {
display: none !important;
}
-input[type="range"] {
+input[type='range'] {
-webkit-appearance: none;
background: none;
}
-input[type="range"]:focus {
+input[type='range']:focus {
outline: none;
}
-input[type="range"]::-webkit-slider-runnable-track {
+input[type='range']::-webkit-slider-runnable-track {
width: 100%;
height: 10px;
cursor: pointer;
@@ -193,7 +197,7 @@ input[type="range"]::-webkit-slider-runnable-track {
border-radius: 10px;
}
-input[type="range"]::-webkit-slider-thumb {
+input[type='range']::-webkit-slider-thumb {
box-shadow: 0;
border: 0;
height: 12px;
@@ -203,4 +207,4 @@ input[type="range"]::-webkit-slider-thumb {
cursor: pointer;
-webkit-appearance: none;
margin-top: -1px;
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0ff15f93b..f7f558bb4 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -30,6 +30,7 @@ import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import './VideoBox.scss';
+import { ObjectField } from '../../../fields/ObjectField';
const path = require('path');
/**
@@ -396,12 +397,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// 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;
+ const aspect = this.player!.videoWidth / (this.player!.videoHeight || 1);
+ if (aspect && !this.isCropped) {
+ 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;
+ this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
} else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
});
@@ -549,7 +553,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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="videoBox-contentContainer" key="container" style={{ mixBlendMode: 'multiply', cursor: this._fullScreen && !this._controlsVisible ? 'none' : 'default' }}>
<div className={classname} ref={this.setContentRef} onPointerDown={e => this._fullScreen && e.stopPropagation()}>
{this._fullScreen && (
<div
@@ -568,7 +572,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
key="video"
autoPlay={this._screenCapture}
ref={this.setVideoRef}
- style={this._fullScreen ? this.fullScreenSize() : {}}
+ style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -853,7 +857,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// 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(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -912,132 +916,46 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// 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 xf = this.props.ScreenToLocalTransform().inverse();
+ const height = this.props.PanelHeight();
+ const vidHeight = (height * this.heightPercent) / 100 / this.scaling();
+ const vidWidth = this.props.PanelWidth() / this.scaling();
+ const uiHeight = 25;
+ const uiMargin = 10;
+ const yBot = xf.transformPoint(0, vidHeight)[1];
+ // prettier-ignore
+ const yMid = (xf.transformPoint(0, 0)[1] +
+ xf.transformPoint(0, height / this.scaling())[1]) / 2;
+ const xPos = xf.transformPoint(vidWidth / 2, 0)[0];
+ const xRight = xf.transformPoint(vidWidth, 0)[0];
const opacity = this._scrubbing ? 0.3 : this._controlsVisible ? 1 : 0;
- return this._fullScreen || right - left < 50 ? null : (
+ return this._fullScreen || this.isCropped || (xRight - xPos) * 2 < 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 }}>
+ <div
+ className="videoBox-ui"
+ style={{
+ transformOrigin: 'top left',
+ transform: `rotate(${NumCast(this.rootDoc.jitterRotation)}deg) translate(${-(xRight - xPos) + 10}px, ${yBot - yMid - uiHeight - uiMargin}px)`,
+ left: xPos,
+ top: yMid,
+ height: uiHeight,
+ width: (xRight - xPos) * 2 - 20,
+ transition: this._clicking ? 'top 0.5s' : '',
+ opacity,
+ }}>
{this.UIButtons}
</div>
</div>
);
};
- @computed get UIButtons() {
- const bounds = this.props.docViewPath().lastElement().getBounds();
- const width = (bounds?.right || 0) - (bounds?.left || 0);
- const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
- return (
- <>
- <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
- <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
- </div>
-
- {this.timeline && width > 150 && (
- <div className="timecode-controls">
- <div className="timecode-current">{formatTime(curTime)}</div>
-
- {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
- <div className="timeline-slider">
- <input
- type="range"
- step="0.1"
- min={this.timeline.clipStart}
- max={this.timeline.clipEnd}
- value={curTime}
- className="toolbar-slider time-progress"
- onPointerDown={action((e: React.PointerEvent) => {
- e.stopPropagation();
- this._scrubbing = true;
- })}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
- onPointerUp={action((e: React.PointerEvent) => {
- e.stopPropagation();
- this._scrubbing = false;
- })}
- />
- </div>
- ) : (
- <div>/</div>
- )}
-
- <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
- </div>
- )}
-
- <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
- <FontAwesomeIcon icon="expand" />
- </div>
-
- {!this._fullScreen && width > 300 && (
- <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
- <FontAwesomeIcon icon="eye" />
- </div>
- )}
-
- {!this._fullScreen && width > 300 && (
- <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
- <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
- </div>
- )}
-
- <div
- className="videobox-button"
- title={this._muted ? 'unmute' : 'mute'}
- onPointerDown={e => {
- e.stopPropagation();
- this.toggleMute();
- }}>
- <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
- </div>
- {width > 300 && (
- <input
- type="range"
- style={{ width: `min(25%, 50px)` }}
- step="0.1"
- min="0"
- max="1"
- value={this._muted ? 0 : this._volume}
- className="toolbar-slider volume"
- onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
- />
- )}
-
- {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
- <>
- <div className="videobox-button" title="zoom">
- <FontAwesomeIcon icon="search-plus" />
- </div>
- <input
- type="range"
- step="0.1"
- min="1"
- max="5"
- value={this.timeline?._zoomFactor}
- className="toolbar-slider zoom"
- onPointerDown={(e: React.PointerEvent) => {
- e.stopPropagation();
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- this.zoom(Number(e.target.value));
- }}
- />
- </>
- )}
- </>
- );
- }
+ scrollFocus = (doc: Doc, smooth: boolean) => {
+ if (doc !== this.rootDoc) {
+ const showTime = Cast(doc._timecodeToShow, 'number', null);
+ showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
+ return 0.1;
+ }
+ };
// renders CollectionStackedTimeline
@computed get renderTimeline() {
@@ -1072,12 +990,63 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
);
}
+ @computed get isCropped() {
+ return this.dataDoc.videoCrop; // bcz: hack to identify a cropped video
+ }
// renders annotation layer
@computed get annotationLayer() {
return <div className="videoBox-annotationLayer" style={{ transition: this.transition, height: `${this.heightPercent}%` }} ref={this._annotationLayer} />;
}
+ crop = (region: Doc | undefined, addCrop?: boolean) => {
+ if (!region) return;
+ const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).backgroundColor = 'transparent';
+ Doc.GetProto(region).lockedPosition = true;
+ Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
+ Doc.GetProto(region).isPushpin = true;
+ region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
+ this.addDocument(region);
+ const anchx = NumCast(cropping.x);
+ const anchy = NumCast(cropping.y);
+ const anchw = NumCast(cropping._width);
+ const anchh = NumCast(cropping._height);
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ cropping.title = 'crop: ' + this.rootDoc.title;
+ cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
+ cropping.y = NumCast(this.rootDoc.y);
+ cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
+ cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
+ cropping.timecodeToHide = undefined;
+ cropping.timecodeToShow = undefined;
+ cropping.isLinkButton = undefined;
+ const croppingProto = Doc.GetProto(cropping);
+ croppingProto.annotationOn = undefined;
+ croppingProto.isPrototype = true;
+ croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
+ croppingProto.type = DocumentType.VID;
+ croppingProto.layout = VideoBox.LayoutString('data');
+ croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
+ croppingProto['data-nativeWidth'] = anchw;
+ croppingProto['data-nativeHeight'] = anchh;
+ croppingProto.videoCrop = true;
+ croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
+ croppingProto.viewScale = viewScale;
+ croppingProto.viewScaleMin = viewScale;
+ croppingProto.panX = anchx / viewScale;
+ croppingProto.panY = anchy / viewScale;
+ croppingProto.panXMin = anchx / viewScale;
+ croppingProto.panXMax = anchw / viewScale;
+ croppingProto.panYMin = anchy / viewScale;
+ croppingProto.panYMax = anchh / viewScale;
+ if (addCrop) {
+ DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ }
+ this.props.bringToFront(cropping);
+ return cropping;
+ };
+
savedAnnotations = () => this._savedAnnotations;
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -1140,6 +1109,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
savedAnnotations={this.savedAnnotations}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
+ anchorMenuCrop={this.crop}
/>
)}
{this.renderTimeline}
@@ -1147,6 +1117,112 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
);
}
+
+ @computed get UIButtons() {
+ const bounds = this.props.docViewPath().lastElement().getBounds();
+ const width = (bounds?.right || 0) - (bounds?.left || 0);
+ const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ return (
+ <>
+ <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
+ <FontAwesomeIcon icon={this._playing ? 'pause' : 'play'} />
+ </div>
+
+ {this.timeline && width > 150 && (
+ <div className="timecode-controls">
+ <div className="timecode-current">{formatTime(curTime)}</div>
+
+ {this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
+ <div className="timeline-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={this.timeline.clipStart}
+ max={this.timeline.clipEnd}
+ value={curTime}
+ className="toolbar-slider time-progress"
+ onPointerDown={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = true;
+ })}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setPlayheadTime(Number(e.target.value))}
+ onPointerUp={action((e: React.PointerEvent) => {
+ e.stopPropagation();
+ this._scrubbing = false;
+ })}
+ />
+ </div>
+ ) : (
+ <div>/</div>
+ )}
+
+ <div className="timecode-end">{formatTime(this.timeline.clipDuration)}</div>
+ </div>
+ )}
+
+ <div className="videobox-button" title={'full screen'} onPointerDown={this.onFullDown}>
+ <FontAwesomeIcon icon="expand" />
+ </div>
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={'show timeline'} onPointerDown={this.onTimelineHdlDown}>
+ <FontAwesomeIcon icon="eye" />
+ </div>
+ )}
+
+ {!this._fullScreen && width > 300 && (
+ <div className="videobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish trimming' : 'start trim'} onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} />
+ </div>
+ )}
+
+ <div
+ className="videobox-button"
+ title={this._muted ? 'unmute' : 'mute'}
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.toggleMute();
+ }}>
+ <FontAwesomeIcon icon={this._muted ? 'volume-mute' : 'volume-up'} />
+ </div>
+ {width > 300 && (
+ <input
+ type="range"
+ style={{ width: `min(25%, 50px)` }}
+ step="0.1"
+ min="0"
+ max="1"
+ value={this._muted ? 0 : this._volume}
+ className="toolbar-slider volume"
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.setVolume(Number(e.target.value))}
+ />
+ )}
+
+ {!this._fullScreen && this.heightPercent !== 100 && width > 300 && (
+ <>
+ <div className="videobox-button" title="zoom">
+ <FontAwesomeIcon icon="search-plus" />
+ </div>
+ <input
+ type="range"
+ step="0.1"
+ min="1"
+ max="5"
+ value={this.timeline?._zoomFactor}
+ className="toolbar-slider zoom"
+ onPointerDown={(e: React.PointerEvent) => {
+ e.stopPropagation();
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ this.zoom(Number(e.target.value));
+ }}
+ />
+ </>
+ )}
+ </>
+ );
+ }
}
VideoBox._nativeControls = false;
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 85986ff27..a41f66ef0 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -5,7 +5,7 @@
width: 100%;
top: 0;
left: 0;
- position: absolute;
+ position: relative;
display: flex;
.webBox-sideResizer {
@@ -182,6 +182,12 @@
height: 100%;
position: absolute;
top: 0;
+ body {
+ ::selection {
+ color: white;
+ background: orange;
+ }
+ }
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 6c2e42f86..460edb7c2 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -47,6 +47,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _outerRef: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -242,6 +243,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
@action componentWillUnmount() {
+ this._iframetimeout && clearTimeout(this._iframetimeout);
+ this._iframetimeout = undefined;
Object.values(this._disposers).forEach(disposer => disposer?.());
// this._iframe?.removeEventListener('wheel', this.iframeWheel, true);
// this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
@@ -275,6 +278,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
+ setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
+ brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
scrollFocus = (doc: Doc, smooth: boolean) => {
if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
@@ -285,7 +290,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
if (scrollTo !== undefined && this._initialScroll === undefined) {
- const focusSpeed = smooth ? 500 : 0;
+ const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0;
this.goTo(scrollTo, focusSpeed);
return focusSpeed;
} else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
@@ -332,7 +337,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale];
+ e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]);
if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) {
setTimeout(
action(() => (this._marqueeing = undefined)),
@@ -349,7 +354,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// bcz: hack - iframe grabs all events which messes up how we handle contextMenus. So this super naively simulates the event stack to get the specific menu items and the doc view menu items.
if (e.button === 2 || (e.button === 0 && e.altKey)) {
e.preventDefault();
- e.stopPropagation();
+ //e.stopPropagation();
ContextMenu.Instance.closeMenu();
ContextMenu.Instance.setIgnoreEvents(true);
}
@@ -363,12 +368,32 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
iframeClick = () => this._iframeClick;
iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale;
+ addStyleSheet(document: any, styleType: string = 'text/css') {
+ const style = document.createElement('style');
+ style.type = styleType;
+ const sheets = document.head.appendChild(style);
+ return (sheets as any).sheet;
+ }
+ addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
+ const propText =
+ typeof css === 'string'
+ ? css
+ : Object.keys(css)
+ .map(p => p + ':' + (p === 'content' ? "'" + css[p] + "'" : css[p]))
+ .join(';');
+ return sheet.insertRule(selectorPrefix + selector + '{' + propText + '}', sheet.cssRules.length);
+ }
+
+ _iframetimeout: any = undefined;
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
if (this._initialScroll !== undefined) {
this.setScrollPos(this._initialScroll);
}
+
+ this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
+
let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString());
if (requrlraw !== this._url.toString()) {
if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
@@ -387,16 +412,25 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
this.submitURL(requrlraw, undefined, true);
}
- if (iframe?.contentDocument) {
- iframe.contentDocument.addEventListener('pointerup', this.iframeUp);
- iframe.contentDocument.addEventListener('pointerdown', this.iframeDown);
- this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument.body.scrollHeight);
- setTimeout(
- action(() => (this._scrollHeight = Math.max(this._scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0))),
+ const iframeContent = iframe?.contentDocument;
+ if (iframeContent) {
+ iframeContent.addEventListener('pointerup', this.iframeUp);
+ iframeContent.addEventListener('pointerdown', this.iframeDown);
+ const initHeights = () => {
+ this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0);
+ if (this._scrollHeight) {
+ this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight);
+ this.layoutDoc.height = Math.min(this.layoutDoc[HeightSym](), (this.layoutDoc[WidthSym]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth));
+ }
+ };
+ initHeights();
+ this._iframetimeout && clearTimeout(this._iframetimeout);
+ this._iframetimeout = setTimeout(
+ action(() => initHeights),
5000
);
iframe.setAttribute('enable-annotation', 'true');
- iframe.contentDocument.addEventListener(
+ iframeContent.addEventListener(
'click',
undoBatch(
action((e: MouseEvent) => {
@@ -831,6 +865,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
fieldKey={this.annotationKey}
CollectionView={undefined}
setPreviewCursor={this.setPreviewCursor}
+ setBrushViewer={this.setBrushViewer}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.scrollXf}
@@ -859,7 +894,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}>
- <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered ? NumCast(this.scrollHeight, 50) : '100%', pointerEvents }}>
+ <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}>
{this.content}
{<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>}
{renderAnnotations(this.opaqueFilter)}
@@ -919,13 +954,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this.props.styleProvider?.(doc, props, property);
};
pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
- annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() ? 'all' : 'none');
+ annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none');
render() {
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
return (
- <div className="webBox" ref={this._mainCont} style={{ pointerEvents: this.pointerEvents(), display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
+ <div
+ className="webBox"
+ ref={this._mainCont}
+ style={{ pointerEvents: this.pointerEvents(), position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
<div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
<div
className="webBox-container"
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index c72b5ca9b..6d1751b25 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -275,7 +275,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
icon = 'globe-asia';
text = 'User Default';
}
- noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking];
+ noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
} else if (script?.script.originalScript.startsWith('setFont')) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
@@ -576,27 +576,19 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
- SelectionManager.Docs().map(doc => (doc._fontFamily = font));
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (checkResult) {
- return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- }
- if (editorView) RichTextMenu.Instance.setFontFamily(font);
- else Doc.UserDoc().fontFamily = font;
+ if (checkResult) return RichTextMenu.Instance?.fontFamily;
+ font && RichTextMenu.Instance.setFontFamily(font);
});
ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') {
const editorView = RichTextMenu.Instance.TextView?.EditorView;
const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
+ // prettier-ignore
switch (info) {
- case 'family':
- return style?.activeFamilies[0];
- case 'size':
- return style?.activeSizes[0];
- case 'color':
- return style?.activeColors[0];
- case 'highlight':
- return style?.activeHighlights[0];
+ case 'family': return style?.activeFamilies[0];
+ case 'size': return style?.activeSizes[0];
+ case 'color': return style?.activeColors[0];
+ case 'highlight': return style?.activeHighlights[0];
}
});
@@ -621,14 +613,8 @@ ScriptingGlobals.add(function setBulletList(mapStyle: 'bullet' | 'decimal', chec
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
-
- if (checkResult) {
- return editorView ? RichTextMenu.Instance.fontColor : Doc.UserDoc().fontColor;
- }
-
- if (editorView) color && RichTextMenu.Instance.setColor(color, editorView, editorView?.dispatch);
- else Doc.UserDoc().fontColor = color;
+ if (checkResult) return RichTextMenu.Instance.fontColor;
+ color && RichTextMenu.Instance.setColor(color);
});
// toggle: Set overlay status of selected document
@@ -650,14 +636,12 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
return RichTextMenu.Instance?.fontSize.replace('px', '');
}
if (typeof size === 'number') size = size.toString();
if (size && Number(size).toString() === size) size += 'px';
- if (editorView) RichTextMenu.Instance.setFontSize(size);
- else Doc.UserDoc()._fontSize = size;
+ RichTextMenu.Instance.setFontSize(size);
});
ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 81ac45521..6db199149 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -603,20 +603,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' });
}
if (highlights.indexOf('Todo Items') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'todo', { outline: 'black solid 1px' });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
}
if (highlights.indexOf('Important Items') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'important', { 'font-size': 'larger' });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' });
}
if (highlights.indexOf('Bold Text') !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror strong > span', { 'font-size': 'large' }, '');
addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
}
if (highlights.indexOf('Disagree Items') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'disagree', { 'text-decoration': 'line-through' });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' });
}
if (highlights.indexOf('Ignore Items') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-' + 'ignore', { 'font-size': '1' });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
}
if (highlights.indexOf('By Recent Minute') !== -1) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
@@ -1006,7 +1006,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
() => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
- if (autoHeight && newHeight && newHeight !== this.rootDoc.height) {
+ if (autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) {
this.props.setHeight?.(newHeight);
}
},
@@ -1387,10 +1387,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
selectOnLoad && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- if (this._editorView && !this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark)) {
+ if (this._editorView) {
this._editorView.state.storedMarks = [
...(this._editorView.state.storedMarks ?? []),
- schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }),
+ ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []),
...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
@@ -1697,9 +1697,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (children) {
- const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')), margins);
+ const proseHeight = !this.ProseRef
+ ? 0
+ : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')) + Number(getComputedStyle(child).marginTop.replace('px', '')) + Number(getComputedStyle(child).marginBottom.replace('px', '')), margins);
const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (this.props.setHeight && scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) {
+ if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => (this.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight);
if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
@@ -1860,7 +1862,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
className={`formattedTextBox-cont`}
ref={this._ref}
style={{
- overflow: this.autoHeight ? 'hidden' : undefined,
+ overflow: this.autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an autoHeight doc in its own tab, or in the lightbox
height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
pointerEvents: interactive ? undefined : 'none',
}}
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 31552cf1b..be501329f 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -168,6 +168,15 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
bind('Alt-Enter', () => (props.onKey?.(event, props) ? true : true));
bind('Ctrl-Enter', () => (props.onKey?.(event, props) ? true : true));
+ bind('Cmd-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
+ return true;
+ });
+
+ bind('Ctrl-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
+ return true;
+ });
// backspace = chainCommands(deleteSelection, joinBackward, selectNodeBackward);
bind('Backspace', (state: EditorState, dispatch: (tx: Transaction) => void) => {
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 0cbe60c0c..6c6d26af5 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -64,6 +64,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
super(props);
runInAction(() => {
RichTextMenu.Instance = this;
+ this.updateMenu(undefined, undefined, props);
this._canFade = false;
this.Pinned = true;
});
@@ -103,13 +104,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
return;
}
this.view = view;
- if (!view || !view.hasFocus()) {
- return;
- }
props && (this.editorProps = props);
// Don't do anything if the document/selection didn't change
- if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return;
+ if (view && view.hasFocus()) {
+ if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return;
+ }
// update active marks
const activeMarks = this.getActiveMarksOnSelection();
@@ -124,9 +124,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
- this._activeFontFamily = !activeFamilies.length ? 'Arial' : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
- this._activeFontSize = !activeSizes.length ? StrCast(this.TextView.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
- this._activeFontColor = !activeColors.length ? 'black' : activeColors.length > 0 ? String(activeColors[0]) : '...';
+ this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
+ this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
+ this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
// update link in current selection
@@ -144,13 +144,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
} else if (dontToggle) {
- toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
- const { from, $from, to, empty } = tx.selection;
- if (!tx.doc.rangeHasMark(from, to, mark.type)) {
- // hack -- should have just set the mark in the first place
- toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
- } else dispatch(tx);
- });
+ const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
+ dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise
} else {
toggleMark(mark.type, mark.attrs)(state, dispatch);
}
@@ -159,7 +154,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// finds font sizes and families in selection
getActiveAlignment() {
- if (this.view && this.TextView.props.isSelected(true)) {
+ if (this.view && this.TextView?.props.isSelected(true)) {
const path = (this.view.state.selection.$from as any).path;
for (let i = path.length - 3; i < path.length && i >= 0; i -= 3) {
if (path[i]?.type === this.view.state.schema.nodes.paragraph || path[i]?.type === this.view.state.schema.nodes.heading) {
@@ -172,7 +167,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// finds font sizes and families in selection
getActiveListStyle() {
- if (this.view && this.TextView.props.isSelected(true)) {
+ if (this.view && this.TextView?.props.isSelected(true)) {
const path = (this.view.state.selection.$from as any).path;
for (let i = 0; i < path.length; i += 3) {
if (path[i].type === this.view.state.schema.nodes.ordered_list) {
@@ -188,17 +183,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// finds font sizes and families in selection
getActiveFontStylesOnSelection() {
- if (!this.view) return { activeFamilies: [], activeSizes: [], activeColors: [], activeHighlights: [] };
-
- const activeFamilies: string[] = [];
- const activeSizes: string[] = [];
- const activeColors: string[] = [];
- const activeHighlights: string[] = [];
- if (this.TextView.props.isSelected(true)) {
+ const activeFamilies = new Set<string>();
+ const activeSizes = new Set<string>();
+ const activeColors = new Set<string>();
+ const activeHighlights = new Set<string>();
+ if (this.view && this.TextView?.props.isSelected(true)) {
const state = this.view.state;
const pos = this.view.state.selection.$from;
const marks: Mark[] = [...(state.storedMarks ?? [])];
- if (state.selection.empty) {
+ if (state.storedMarks !== null) {
+ } else if (state.selection.empty) {
const ref_node = this.reference_node(pos);
marks.push(...(ref_node !== this.view.state.doc && ref_node?.isText ? Array.from(ref_node.marks) : []));
} else {
@@ -207,13 +201,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
});
}
marks.forEach(m => {
- m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
- m.type === state.schema.marks.pFontColor && activeColors.push(m.attrs.color);
- m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize);
- m.type === state.schema.marks.marker && activeHighlights.push(String(m.attrs.highlight));
+ m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.family);
+ m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.color);
+ m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize);
+ m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight));
});
}
- return { activeFamilies, activeSizes, activeColors, activeHighlights };
+ return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) };
}
getMarksInSelection(state: EditorState) {
@@ -226,7 +220,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
//finds all active marks on selection in given group
getActiveMarksOnSelection() {
let activeMarks: MarkType[] = [];
- if (!this.view || !this.TextView.props.isSelected(true)) return activeMarks;
+ if (!this.view || !this.TextView?.props.isSelected(true)) return activeMarks;
const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
@@ -279,28 +273,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this._superscriptActive = false;
activeMarks.forEach(mark => {
+ // prettier-ignore
switch (mark.name) {
- case 'noAutoLinkAnchor':
- this._noLinkActive = true;
- break;
- case 'strong':
- this._boldActive = true;
- break;
- case 'em':
- this._italicsActive = true;
- break;
- case 'underline':
- this._underlineActive = true;
- break;
- case 'strikethrough':
- this._strikethroughActive = true;
- break;
- case 'subscript':
- this._subscriptActive = true;
- break;
- case 'superscript':
- this._superscriptActive = true;
- break;
+ case 'noAutoLinkAnchor': this._noLinkActive = true; break;
+ case 'strong': this._boldActive = true; break;
+ case 'em': this._italicsActive = true; break;
+ case 'underline': this._underlineActive = true; break;
+ case 'strikethrough': this._strikethroughActive = true; break;
+ case 'subscript': this._subscriptActive = true; break;
+ case 'superscript': this._superscriptActive = true; break;
}
});
}
@@ -342,14 +323,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (this.view.state.selection.from === 1 && this.view.state.selection.empty && (!this.view.state.doc.nodeAt(1) || !this.view.state.doc.nodeAt(1)?.marks.some(m => m.type.name === fontSize))) {
this.TextView.dataDoc.fontSize = fontSize;
this.view.focus();
- this.updateMenu(this.view, undefined, this.props);
} else {
const fmark = this.view.state.schema.marks.pFontSize.create({ fontSize });
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
- this.updateMenu(this.view, undefined, this.props);
}
- }
+ } else Doc.UserDoc()._fontSize = fontSize;
+ this.updateMenu(this.view, undefined, this.props);
};
setFontFamily = (family: string) => {
@@ -357,8 +337,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family });
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
- this.updateMenu(this.view, undefined, this.props);
- }
+ } else Doc.UserDoc()._fontFamily = family;
+ this.updateMenu(this.view, undefined, this.props);
};
setHighlight(color: String, view: EditorView, dispatch: any) {
@@ -368,13 +348,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(highlightMark, view.state, dispatch, false);
}
- setColor(color: String, view: EditorView, dispatch: any) {
+ setColor(color: string) {
if (this.view) {
- const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color });
+ const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color });
this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true);
- view.focus();
- this.updateMenu(this.view, undefined, this.props);
- }
+ this.view.focus();
+ } else Doc.UserDoc().fontColor = color;
+ this.updateMenu(this.view, undefined, this.props);
}
// TODO: remove doesn't work
@@ -430,7 +410,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
align = (view: EditorView, dispatch: any, alignment: 'left' | 'right' | 'center') => {
- if (this.TextView.props.isSelected(true)) {
+ if (this.TextView?.props.isSelected(true)) {
var tr = view.state.tr;
view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos, parent, index) => {
if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 2097b321f..e5ea7b3b0 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -276,7 +276,7 @@ export class RichTextRules {
this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
- return state.tr.deleteRange(start, end).insert(start, fieldView);
+ return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
}),
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
@@ -327,7 +327,10 @@ export class RichTextRules {
this.Document[DataSym].tags = `${tags + '#' + tag + ':'}`;
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
- return state.tr.deleteRange(start, end).insert(start, fieldView).insertText(' ');
+ return state.tr
+ .setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end)))
+ .replaceSelectionWith(fieldView, true)
+ .insertText(' ');
}),
// # heading
@@ -343,8 +346,14 @@ export class RichTextRules {
const node = (state.doc.resolve(start) as any).nodeAfter;
if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
-
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_mark) !== -1) {
+ }
+ return node
+ ? state.tr
+ .removeMark(start, end, schema.marks.user_mark)
+ .addMark(start, end, schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))
+ .addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) }))
+ : state.tr;
}),
new InputRule(new RegExp(/%\(/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index a0a2dd4f8..4d60a02f1 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -1,4 +1,4 @@
-@import "../../global/globalCssVariables";
+@import '../../global/globalCssVariables';
.presBox-cont {
cursor: auto;
@@ -231,8 +231,7 @@
margin-top: 10px;
}
- @media screen and (-webkit-min-device-pixel-ratio:0) {
-
+ @media screen and (-webkit-min-device-pixel-ratio: 0) {
.multiThumb-slider {
display: grid;
background-color: $white;
@@ -289,6 +288,7 @@
height: 10px;
-webkit-appearance: none;
margin-top: -1px;
+ background: transparent;
}
.toolbar-slider::-webkit-slider-thumb {
@@ -330,8 +330,6 @@
}
}
-
-
.slider-headers {
position: relative;
display: grid;
@@ -354,8 +352,8 @@
.slider-number {
border-radius: 3px;
- width: 30px;
margin: auto;
+ overflow: hidden;
}
}
@@ -382,7 +380,6 @@
border-bottom: solid 2px $medium-gray;
}
-
.ribbon-textInput {
border-radius: 2px;
height: 20px;
@@ -467,7 +464,6 @@
font-weight: 500;
position: relative;
-
.ribbon-final-button {
cursor: pointer;
position: relative;
@@ -687,7 +683,6 @@
max-width: 200px;
overflow: visible;
-
.presBox-dropdownOption {
cursor: pointer;
font-size: 11;
@@ -716,7 +711,7 @@
width: 85%;
min-width: max-content;
display: block;
- background: #FFFFFF;
+ background: #ffffff;
border: 0.5px solid #979797;
box-sizing: border-box;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
@@ -741,7 +736,7 @@
padding-top: 5px;
padding-bottom: 5px;
border: solid 1px $black;
- // overflow: auto;
+ // overflow: auto;
::-webkit-scrollbar {
-webkit-appearance: none;
@@ -953,8 +948,6 @@
min-width: 150px;
}
-
-
select {
background: $dark-gray;
color: $white;
@@ -999,8 +992,6 @@
}
}
-
-
.collectionViewBaseChrome-viewPicker {
min-width: 50;
width: 5%;
@@ -1080,7 +1071,7 @@
position: absolute;
top: 0;
left: 0;
- opacity: 0.1;
+ opacity: 0.5;
transition: all 0.4s;
color: $white;
width: 100%;
@@ -1165,8 +1156,6 @@
.presPanel-button-text:hover {
background-color: $medium-gray;
}
-
-
}
// .miniPres {
@@ -1241,4 +1230,4 @@
// background-color: #5a5a5a;
// }
// }
-// } \ No newline at end of file
+// }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index eb40089ec..0c4d514cd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -5,12 +5,14 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, DocListCastAsync, FieldResult } from '../../../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, FieldResult, Opt } from '../../../../fields/Doc';
+import { Copy } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
+import { ObjectField } from '../../../../fields/ObjectField';
import { listSpec } from '../../../../fields/Schema';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
@@ -19,8 +21,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
import { SettingsManager } from '../../../util/SettingsManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { MarqueeViewBounds } from '../../collections/collectionFreeForm';
-import { CollectionFreeFormViewChrome } from '../../collections/CollectionMenu';
+import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
import { CollectionView } from '../../collections/CollectionView';
import { TabDocView } from '../../collections/TabDocView';
import { ViewBoxBaseComponent } from '../../DocComponent';
@@ -43,7 +44,6 @@ export interface PinProps {
export interface PinViewProps {
bounds: MarqueeViewBounds;
- scale: number;
}
@observer
@@ -53,22 +53,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
/**
- * transitions & effects for documents
- * @param renderDoc
- * @param layoutDoc
+ * returns an entrance animation effect function to wrap a JSX element
+ * @param presEffectDoc presentation effects document that specifies the animation effect parameters
+ * @returns a function that will wrap a JSX animation element wrapping any JSX element
*/
- static renderEffectsDoc(renderDoc: any, layoutDoc: Doc, presDoc: Doc) {
+ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc) {
const effectProps = {
- left: presDoc.presEffectDirection === PresEffect.Left,
- right: presDoc.presEffectDirection === PresEffect.Right,
- top: presDoc.presEffectDirection === PresEffect.Top,
- bottom: presDoc.presEffectDirection === PresEffect.Bottom,
+ left: presEffectDoc.presEffectDirection === PresEffect.Left,
+ right: presEffectDoc.presEffectDirection === PresEffect.Right,
+ top: presEffectDoc.presEffectDirection === PresEffect.Top,
+ bottom: presEffectDoc.presEffectDirection === PresEffect.Bottom,
opposite: true,
- delay: presDoc.presTransition,
- // when: this.layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc,
+ delay: NumCast(presEffectDoc.presTransition),
};
//prettier-ignore
- switch (presDoc.presEffect) {
+ switch (StrCast(presEffectDoc.presEffect)) {
default:
case PresEffect.None: return renderDoc;
case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
@@ -80,37 +79,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
}
}
- public static EffectsProvider(layoutDoc: Doc, renderDoc: any) {
- return PresBox.Instance && layoutDoc === PresBox.Instance.childDocs[PresBox.Instance.itemIndex]?.presentationTargetDoc ? PresBox.renderEffectsDoc(renderDoc, layoutDoc, PresBox.Instance.childDocs[PresBox.Instance.itemIndex]) : renderDoc;
- }
private _disposers: { [name: string]: IReactionDisposer } = {};
-
- constructor(props: any) {
- super(props);
- if ((Doc.ActivePresentation = this.rootDoc)) runInAction(() => (PresBox.Instance = this));
- this.props.Document.presentationFieldKey = this.fieldKey; // provide info to the presElement script so that it can look up rendering information about the presBox
- }
+ public selectedArray = new ObservableSet<Doc>();
@observable public static Instance: PresBox;
+ @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
@observable _isChildActive = false;
@observable _moveOnFromAudio: boolean = true;
@observable _presTimer!: NodeJS.Timeout;
- @observable _presKeyEventsActive: boolean = false;
@observable _eleArray: HTMLElement[] = [];
@observable _dragArray: HTMLElement[] = [];
@observable _pathBoolean: boolean = false;
@observable _expandBoolean: boolean = false;
-
- @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
- @observable private transitionTools: boolean = false;
- @observable private newDocumentTools: boolean = false;
- @observable private progressivizeTools: boolean = false;
- @observable private openMovementDropdown: boolean = false;
- @observable private openEffectDropdown: boolean = false;
- @observable private presentTools: boolean = false;
+ @observable _transitionTools: boolean = false;
+ @observable _newDocumentTools: boolean = false;
+ @observable _progressivizeTools: boolean = false;
+ @observable _openMovementDropdown: boolean = false;
+ @observable _openEffectDropdown: boolean = false;
+ @observable _presentTools: boolean = false;
+ @observable _treeViewMap: Map<Doc, number> = new Map();
+ @observable _presKeyEvents: boolean = false;
+ @observable _forceKeyEvents: boolean = false;
@computed get isTreeOrStack() {
return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any);
}
@@ -123,15 +115,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get childDocs() {
return DocListCast(this.rootDoc[this.presFieldKey]);
}
- @observable _treeViewMap: Map<Doc, number> = new Map();
-
@computed get tagDocs() {
- const tagDocs: Doc[] = [];
- for (const doc of this.childDocs) {
- const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
- tagDocs.push(tagDoc);
- }
- return tagDocs;
+ return this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null));
}
@computed get itemIndex() {
return NumCast(this.rootDoc._itemIndex);
@@ -142,41 +127,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get targetDoc() {
return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
}
- @computed get scrollable(): boolean {
+ @computed get scrollable() {
if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
- else return false;
+ return false;
}
- @computed get panable(): boolean {
+ @computed get panable() {
if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
- else return false;
+ return false;
}
@computed get selectedDocumentView() {
if (SelectionManager.Views().length) return SelectionManager.Views()[0];
- if (this._selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc);
+ if (this.selectedArray.size) return DocumentManager.Instance.getDocumentView(this.rootDoc);
}
- @computed get isPres(): boolean {
- document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
- if (this.selectedDoc?.type === DocumentType.PRES) {
- document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
- document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
- return true;
- }
- return false;
+ @computed get isPres() {
+ return this.selectedDoc === this.rootDoc;
}
@computed get selectedDoc() {
return this.selectedDocumentView?.rootDoc;
}
- _selectedArray = new ObservableSet<Doc>();
- clearSelectedArray = () => this._selectedArray.clear();
- addToSelectedArray = (doc: Doc) => this._selectedArray.add(doc);
- removeFromSelectedArray = (doc: Doc) => this._selectedArray.delete(doc);
+ isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentationTargetDoc === layoutDoc;
+ clearSelectedArray = () => this.selectedArray.clear();
+ addToSelectedArray = (doc: Doc) => this.selectedArray.add(doc);
+ removeFromSelectedArray = (doc: Doc) => this.selectedArray.delete(doc);
_unmounting = false;
@action
componentWillUnmount() {
this._unmounting = true;
document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
- this._presKeyEventsActive = false;
this.resetPresentation();
// Turn of progressivize editors
this.turnOffEdit(true);
@@ -185,6 +163,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
componentDidMount() {
+ this._disposers.keyboard = reaction(
+ () => this.selectedDoc,
+ selected => {
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
+ (this._presKeyEvents = selected === this.rootDoc) && document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.forcekeyboard = reaction(
+ () => this._forceKeyEvents,
+ force => {
+ if (force) {
+ document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
+ document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
+ this._presKeyEvents = true;
+ }
+ this._forceKeyEvents = false;
+ },
+ { fireImmediately: true }
+ );
this.props.setContentView?.(this);
this._unmounting = false;
this.rootDoc._forceRenderEngine = 'timeline';
@@ -192,7 +190,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc._gridGap = 0;
this.layoutDoc._yMargin = 0;
this.turnOffEdit(true);
- DocListCastAsync(Doc.MyTrails.data).then(pres => !pres?.includes(this.rootDoc) && Doc.AddDocToList(Doc.MyTrails, 'data', this.rootDoc));
this._disposers.selection = reaction(
() => SelectionManager.Views(),
views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()
@@ -201,11 +198,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
updateCurrentPresentation = (pres?: Doc) => {
- if (pres) Doc.ActivePresentation = pres;
- else Doc.ActivePresentation = this.rootDoc;
- document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
- document.addEventListener('keydown', PresBox.keyEventsWrapper, true);
- this._presKeyEventsActive = true;
+ Doc.ActivePresentation = pres ?? this.rootDoc;
PresBox.Instance = this;
};
@@ -241,17 +234,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
// TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
// No more frames in current doc and next slide is defined, therefore move to next slide
- nextSlide = (activeNext: Doc) => {
- const targetNext = Cast(activeNext.presentationTargetDoc, Doc, null);
- console.info('nextSlide', activeNext.title, targetNext?.title);
- let nextSelected = this.itemIndex + 1;
+ nextSlide = (slideNum?: number) => {
+ let nextSelected = slideNum ?? this.itemIndex + 1;
this.gotoDocument(nextSelected, this.activeItem);
for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
- if (!this.childDocs[nextSelected].groupWithUp) {
- break;
- } else {
- console.log('Title: ' + this.childDocs[nextSelected].title);
+ if (this.childDocs[nextSelected].groupWithUp) {
this.gotoDocument(nextSelected, this.activeItem, true);
+ } else {
+ break;
}
}
};
@@ -271,10 +261,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.nextInternalFrame(targetDoc, activeItem);
} else if (this.childDocs[this.itemIndex + 1] !== undefined) {
// Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
- this.nextSlide(activeNext);
+ const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]);
+ const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex;
+ this.nextSlide(curLast + 1);
} else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
// Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
- this.gotoDocument(0, this.activeItem);
+ this.nextSlide(0);
}
};
@@ -290,8 +282,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let prevSelected = this.itemIndex;
// Functionality for group with up
let didZoom = activeItem.presMovement;
- for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) {
- didZoom = this.childDocs[prevSelected].presMovement;
+ for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
+ didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
}
if (lastFrame !== undefined && curFrame >= 1) {
// Case 1: There are still other frames and should go through all frames before going to previous slide
@@ -299,7 +291,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
} else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
// Case 2: There are no other frames so it should go to the previous slide
prevSelected = Math.max(0, prevSelected - 1);
- this.gotoDocument(prevSelected, activeItem);
+ this.nextSlide(prevSelected);
+ this.rootDoc._itemIndex = prevSelected;
if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
} else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
// Case 3: Pres loop is on so it should go to the last slide
@@ -309,6 +302,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
+ @undoBatch
public gotoDocument = action((index: number, from?: Doc, group?: boolean) => {
Doc.UnBrushAllDocs();
@@ -317,8 +311,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
if (activeItem.presActiveFrame !== undefined) {
+ const transTime = NumCast(activeItem.presTransition, 500);
const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
- context && CollectionFreeFormViewChrome.gotoKeyFrame(context, NumCast(activeItem.presActiveFrame));
+ if (context) {
+ const contextView = DocumentManager.Instance.getFirstDocumentView(context);
+ if (contextView?.ComponentView) {
+ CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
+ context._currentFrame = NumCast(activeItem.presActiveFrame);
+ }
+ }
}
if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
@@ -332,95 +333,109 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (targetDoc) {
Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
- targetDoc &&
- runInAction(() => {
- if (activeItem.presMovement === PresMovement.Jump) targetDoc.focusSpeed = 0;
- else targetDoc.focusSpeed = activeItem.presTransition ? activeItem.presTransition : 500;
- });
- setTimeout(() => (targetDoc.focusSpeed = 500), this.activeItem.presTransition ? NumCast(this.activeItem.presTransition) + 10 : 510);
+ targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500)));
+ setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10);
}
if (targetDoc?.lastFrame !== undefined) {
targetDoc._currentFrame = 0;
}
if (!group) this.clearSelectedArray();
this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
- this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list
+ this.turnOffEdit();
+ this.navigateToActiveItem(); //Handles movement to element only when presTrail is list
this.onHideDocument(); //Handles hide after/before
}
});
-
- _navTimer!: NodeJS.Timeout;
- navigateToView = (targetDoc: Doc, activeItem: Doc) => {
- clearTimeout(this._navTimer);
- const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
- if (bestTarget) console.log(bestTarget.title, bestTarget.type);
- else console.log('no best target');
- if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false);
- };
-
static pinDataTypes(target: Doc) {
const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
const clippable = [DocumentType.COMPARISON].includes(target.type as any);
- return { scrollable, pannable, temporal, clippable };
+ const dataview = [DocumentType.INK].includes(target.type as any) && target.activeFrame === undefined;
+ const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
+ return { scrollable, pannable, temporal, clippable, dataview, textview };
}
- // navigates to the bestTarget document by making sure it is on screen,
- // then it applies the view specs stored in activeItem to
+
@action
- static navigateToDoc(bestTarget: Doc, activeItem: Doc, jumpToDoc: boolean) {
- bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s';
- const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(bestTarget);
+ static restoreTargetDocView(bestTarget: Doc, activeItem: Doc) {
+ const transTime = NumCast(activeItem.presTransition, 500);
+ const presTransitionTime = `all ${transTime}ms`;
+ const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(bestTarget);
+ bestTarget._viewTransition = presTransitionTime;
if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
- if (scrollable) bestTarget._scrollTop = activeItem.presPinViewScroll;
+ if (scrollable) {
+ bestTarget._scrollTop = activeItem.presPinViewScroll;
+ const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
+ if (contentBounds) {
+ const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView;
+ dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
+ }
+ }
+ if (dataview) Doc.GetProto(bestTarget).data = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+ if (textview) Doc.GetProto(bestTarget).text = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
if (pannable) {
- const contentBounds = Cast(activeItem.contentBounds, listSpec('number'));
+ const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
if (contentBounds) {
- bestTarget._panX = (contentBounds[0] + contentBounds[2]) / 2;
- bestTarget._panY = (contentBounds[1] + contentBounds[3]) / 2;
+ const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
+ bestTarget._panX = viewport.panX;
+ bestTarget._panY = viewport.panY;
const dv = DocumentManager.Instance.getDocumentView(bestTarget);
if (dv) {
- bestTarget._viewScale = Math.min(dv.props.PanelHeight() / (contentBounds[3] - contentBounds[1]), dv.props.PanelWidth() / (contentBounds[2] - contentBounds[0]));
+ const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
+ activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presZoom !== undefined ? computedScale : Math.min(computedScale, NumCast(bestTarget._viewScale)));
+ dv.ComponentView?.brushView?.(viewport);
}
} else {
bestTarget._panX = activeItem.presPinViewX;
bestTarget._panY = activeItem.presPinViewY;
- bestTarget._viewScale = activeItem.presPinViewScale;
+ activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presPinViewScale);
}
}
- return setTimeout(() => (bestTarget._viewTransition = undefined), activeItem.presTransition ? NumCast(activeItem.presTransition) + 10 : 510);
+ return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
}
/// copies values from the targetDoc (which is the prototype of the pinDoc) to
/// reserved fields on the pinDoc so that those values can be restored to the
/// target doc when navigating to it.
@action
- static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined) {
+ static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) {
if (pinProps?.pinWithView) {
// If pinWithView option set then update scale and x / y props of slide
const bounds = pinProps.pinWithView.bounds;
pinDoc.presPinView = true;
pinDoc.presPinViewX = bounds.left + bounds.width / 2;
pinDoc.presPinViewY = bounds.top + bounds.height / 2;
- pinDoc.presPinViewScale = pinProps.pinWithView.scale;
- pinDoc.contentBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
+ pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
}
if (pinProps?.pinDocView) {
- const { scrollable, pannable, temporal, clippable } = this.pinDataTypes(pinDoc);
- pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable;
-
- if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
- else if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
- else if (temporal) pinDoc.presEndTime = NumCast((pinDoc.presStartTime = pinDoc._currentTimecode)) + 0.1;
- else if (pannable) {
+ const { scrollable, pannable, temporal, clippable, dataview, textview } = this.pinDataTypes(pinDoc);
+ pinDoc.presPinView = (pinProps?.pinWithView ? true : false) || scrollable || temporal || pannable || clippable || dataview || textview || pinProps.activeFrame !== undefined;
+ pinDoc.presX = NumCast(targetDoc.x);
+ pinDoc.presY = NumCast(targetDoc.y);
+ pinDoc.presRot = NumCast(targetDoc.jitterRotation);
+ pinDoc.presWidth = NumCast(targetDoc.width);
+ pinDoc.presHeight = NumCast(targetDoc.height);
+
+ if (scrollable) {
+ pinDoc.presPinViewScroll = pinDoc._scrollTop;
+ }
+ if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
+ if (temporal) {
+ pinDoc.presStartTime = pinDoc._currentTimecode;
+ const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1);
+ pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration);
+ }
+ if (textview) pinDoc.presData = targetDoc.text instanceof ObjectField ? targetDoc.text[Copy]() : targetDoc.text;
+ if (dataview) pinDoc.presData = targetDoc.data instanceof ObjectField ? targetDoc.data[Copy]() : targetDoc.data;
+ if (pannable || scrollable) {
const panX = NumCast(pinDoc._panX);
const panY = NumCast(pinDoc._panY);
const pw = NumCast(pinProps.panelWidth);
const ph = NumCast(pinProps.panelHeight);
- const ps = NumCast(pinDoc._viewScale);
+ const ps = NumCast(pinDoc._viewScale, 1);
if (pw && ph && ps) {
- pinDoc.contentBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]);
+ pinDoc.presPinViewBounds = new List<number>([panX - pw / 2 / ps, panY - ph / 2 / ps, panX + pw / 2 / ps, panY + ph / 2 / ps]);
}
pinDoc.presPinViewX = panX;
pinDoc.presPinViewY = panY;
@@ -429,6 +444,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
+ static _navTimer: NodeJS.Timeout;
/**
* This method makes sure that cursor navigates to the element that
* has the option open and last in the group.
@@ -437,7 +453,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* a new tab. If presCollection is undefined it will open the document
* on the right.
*/
- navigateToElement = async (curDoc: Doc) => {
+ navigateToActiveItem = () => {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null);
@@ -445,7 +461,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc);
const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext);
- this.turnOffEdit();
// Handles the setting of presCollection
if (includesDoc) {
//Case 1: Pres collection should not change as it is already the same
@@ -454,7 +469,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc.presCollection = srcContext;
}
const presStatus = this.rootDoc.presStatus;
- const selViewCache = Array.from(this._selectedArray);
+ const selViewCache = Array.from(this.selectedArray);
const dragViewCache = Array.from(this._dragArray);
const eleViewCache = Array.from(this._eleArray);
const self = this;
@@ -468,7 +483,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
});
const openInTab = (doc: Doc, finished?: () => void) => {
- collectionDocView ? collectionDocView.props.addDocTab(doc, '') : this.props.addDocTab(doc, '');
+ (collectionDocView ?? this).props.addDocTab(doc, '');
this.layoutDoc.presCollection = targetDoc;
// this still needs some fixing
setTimeout(resetSelection, 500);
@@ -478,27 +493,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
finished?.();
}
};
+ PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection);
+ };
+
+ static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
+ if (activeItem.presPinView && DocCast(targetDoc.context)?._currentFrame === undefined) {
+ const transTime = NumCast(activeItem.presTransition, 500);
+ const presTransitionTime = `all ${transTime}ms`;
+ targetDoc._dataTransition = presTransitionTime;
+ targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
+ targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
+ targetDoc.jitterRotation = NumCast(activeItem.presRot, NumCast(targetDoc.jitterRotation));
+ targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width));
+ targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height));
+ setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10);
+ }
// If openDocument is selected then it should open the document for the user
if (activeItem.openDocument) {
- LightboxView.SetLightboxDoc(targetDoc);
- // openInTab(targetDoc);
- } else if (curDoc.presMovement === PresMovement.Pan && targetDoc) {
- LightboxView.SetLightboxDoc(undefined);
- await DocumentManager.Instance.jumpToDocument(targetDoc, false, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right
- } else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) {
+ LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
+ } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
LightboxView.SetLightboxDoc(undefined);
- //awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right
+ const zooming = activeItem.presMovement !== PresMovement.Pan;
+ DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom));
}
// After navigating to the document, if it is added as a presPinView then it will
// adjust the pan and scale to that of the pinView when it was added.
if (activeItem.presPinView) {
- console.log(targetDoc.title);
- console.log('presPinView in PresBox.tsx:420');
- // if targetDoc is not displayed but one of its aliases is, then we need to modify that alias, not the original target
- this.navigateToView(targetDoc, activeItem);
+ clearTimeout(PresBox._navTimer);
+ // targetDoc may or may not be displayed. this gets the first available document (or alias) view that matches targetDoc
+ const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
+ if (bestTarget) PresBox._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem);
}
- };
+ }
/**
* Uses the viewfinder to progressivize through the different views of a single collection.
@@ -705,8 +731,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc.presStatus = PresStatus.Edit;
clearTimeout(this._presTimer);
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- this.rootDoc.x = pt[0] + (this.props.PanelWidth() - 250);
- this.rootDoc.y = pt[1] + 10;
+ this.rootDoc.overlayX = pt[0] + (this.props.PanelWidth() - 250);
+ this.rootDoc.overlayY = pt[1] + 10;
this.rootDoc._height = 30;
this.rootDoc._width = 248;
Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc);
@@ -756,27 +782,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
});
- setMovementName = action((movement: any, activeItem: Doc): string => {
- let output: string = 'none';
- switch (movement) {
- case PresMovement.Zoom:
- output = 'Pan & Zoom';
- break; //Pan and zoom
- case PresMovement.Pan:
- output = 'Pan';
- break; //Pan
- case PresMovement.Jump:
- output = 'Jump cut';
- break; //Jump Cut
- case PresMovement.None:
- output = 'None';
- break; //None
- default:
- output = 'Zoom';
- activeItem.presMovement = 'zoom';
- break; //default set as zoom
+ movementName = action((activeItem: Doc) => {
+ if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
+ activeItem.presMovement = 'zoom';
}
- return output;
+ return StrCast(activeItem.presMovement);
});
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive)));
@@ -822,16 +832,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/**
* For sorting the array so that the order is maintained when it is dropped.
*/
- @action
- sortArray = (): Doc[] => {
- return this.childDocs.filter(doc => this._selectedArray.has(doc));
- };
+ sortArray = () => this.childDocs.filter(doc => this.selectedArray.has(doc));
/**
* Method to get the list of selected items in the order in which they have been selected
*/
@computed get listOfSelected() {
- return Array.from(this._selectedArray).map((doc: Doc, index: any) => {
+ return Array.from(this.selectedArray).map((doc: Doc, index: any) => {
const curDoc = Cast(doc, Doc, null);
const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
if (curDoc && curDoc === this.activeItem)
@@ -866,33 +873,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//Regular click
@action
selectElement = async (doc: Doc) => {
- const context = Cast(doc.context, Doc, null);
this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
- if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(context), 0);
- else this.updateCurrentPresentation(context);
+ if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
+ else this.updateCurrentPresentation(DocCast(doc.context));
};
//Command click
@action
multiSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => {
- if (!this._selectedArray.has(doc)) {
+ if (!this.selectedArray.has(doc)) {
this.addToSelectedArray(doc);
this._eleArray.push(ref);
this._dragArray.push(drag);
} else {
this.removeFromSelectedArray(doc);
- this.removeFromArray(this._eleArray, doc);
- this.removeFromArray(this._dragArray, doc);
+ this._eleArray.splice(this._eleArray.indexOf(ref));
+ this._dragArray.splice(this._dragArray.indexOf(drag));
}
this.selectPres();
};
- removeFromArray = (arr: any[], val: any) => {
- const index: number = arr.indexOf(val);
- const ret: any[] = arr.splice(index, 1);
- arr = ret;
- };
-
//Shift click
@action
shiftSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement) => {
@@ -925,9 +925,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
else this.regularSelect(doc, ref, drag, focus);
};
- static keyEventsWrapper = (e: KeyboardEvent) => {
- PresBox.Instance.keyEvents(e);
- };
+ static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance.keyEvents(e);
// Key for when the presentaiton is active
@action
@@ -941,7 +939,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (this.layoutDoc.presStatus === 'edit') {
undoBatch(
action(() => {
- for (const doc of this._selectedArray) {
+ for (const doc of this.selectedArray) {
this.removeDocument(doc);
}
this.clearSelectedArray();
@@ -1028,13 +1026,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- getAllIndexes = (arr: Doc[], val: Doc): number[] => {
- const indexes = [];
- for (let i = 0; i < arr.length; i++) {
- arr[i] === val && indexes.push(i);
- }
- return indexes;
- };
+ getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1);
// Adds the index in the pres path graphically
@computed get order() {
@@ -1153,7 +1145,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
if (timeInMS > 10000) timeInMS = 10000;
- this._selectedArray.forEach(doc => (doc.presTransition = timeInMS));
+ this.selectedArray.forEach(doc => (doc.presTransition = timeInMS));
};
// Converts seconds to ms and updates presTransition
@@ -1161,8 +1153,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let scale = Number(number) / 100;
if (change) scale += change;
if (scale < 0.01) scale = 0.01;
- if (scale > 1.5) scale = 1.5;
- this._selectedArray.forEach(doc => (doc.presZoom = scale));
+ if (scale > 1) scale = 1;
+ this.selectedArray.forEach(doc => (doc.presZoom = scale));
};
// Converts seconds to ms and updates presDuration
@@ -1171,100 +1163,43 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
if (timeInMS > 20000) timeInMS = 20000;
- this._selectedArray.forEach(doc => (doc.presDuration = timeInMS));
+ this.selectedArray.forEach(doc => (doc.presDuration = timeInMS));
};
/**
* When the movement dropdown is changes
*/
@undoBatch
- updateMovement = action((movement: any, all?: boolean) => {
- (all ? this.childDocs : this._selectedArray).forEach(doc => {
- switch (movement) {
- case PresMovement.Zoom: //Pan and zoom
- doc.presMovement = PresMovement.Zoom;
- break;
- case PresMovement.Pan: //Pan
- doc.presMovement = PresMovement.Pan;
- break;
- case PresMovement.Jump: //Jump Cut
- doc.presJump = true;
- doc.presMovement = PresMovement.Jump;
- break;
- case PresMovement.None:
- default:
- doc.presMovement = PresMovement.None;
- break;
- }
- });
- });
+ updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement)));
@undoBatch
@action
updateHideBefore = (activeItem: Doc) => {
activeItem.presHideBefore = !activeItem.presHideBefore;
- this._selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore));
+ this.selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore));
};
@undoBatch
@action
updateHideAfter = (activeItem: Doc) => {
activeItem.presHideAfter = !activeItem.presHideAfter;
- this._selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter));
+ this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter));
};
@undoBatch
@action
updateOpenDoc = (activeItem: Doc) => {
activeItem.openDocument = !activeItem.openDocument;
- this._selectedArray.forEach(doc => {
- doc.openDocument = activeItem.openDocument;
- });
+ this.selectedArray.forEach(doc => (doc.openDocument = activeItem.openDocument));
};
@undoBatch
@action
- updateEffectDirection = (effect: any, all?: boolean) => {
- (all ? this.childDocs : this._selectedArray).forEach(doc => {
- const tagDoc = doc; // Cast(doc.presentationTargetDoc, Doc, null);
- switch (effect) {
- case PresEffect.Left:
- tagDoc.presEffectDirection = PresEffect.Left;
- break;
- case PresEffect.Right:
- tagDoc.presEffectDirection = PresEffect.Right;
- break;
- case PresEffect.Top:
- tagDoc.presEffectDirection = PresEffect.Top;
- break;
- case PresEffect.Bottom:
- tagDoc.presEffectDirection = PresEffect.Bottom;
- break;
- case PresEffect.Center:
- default:
- tagDoc.presEffectDirection = PresEffect.Center;
- break;
- }
- });
- };
+ updateEffectDirection = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
@undoBatch
@action
- updateEffect = (effect: any, all?: boolean) => {
- (all ? this.childDocs : this._selectedArray).forEach(doc => {
- const tagDoc = doc; //Cast(doc.presentationTargetDoc, Doc, null);
- //prettier-ignore
- switch (effect) {
- default:
- case PresEffect.None: tagDoc.presEffect = PresEffect.None; break;
- case PresEffect.Bounce: tagDoc.presEffect = PresEffect.Bounce; break;
- case PresEffect.Fade: tagDoc.presEffect = PresEffect.Fade; break;
- case PresEffect.Flip: tagDoc.presEffect = PresEffect.Flip; break;
- case PresEffect.Roll: tagDoc.presEffect = PresEffect.Roll; break;
- case PresEffect.Rotate: tagDoc.presEffect = PresEffect.Rotate; break;
- }
- });
- };
+ updateEffect = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffect = effect));
_batch: UndoManager.Batch | undefined = undefined;
@@ -1273,6 +1208,49 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetDoc: Doc = this.targetDoc;
const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection;
const isPinWithView: boolean = BoolCast(activeItem.presPinView);
+ const presEffect = (effect: PresEffect) => (
+ <div className={`presBox-dropdownOption ${this.activeItem.presEffect === effect || (effect === PresEffect.None && !this.activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}>
+ {effect}
+ </div>
+ );
+ const presMovement = (movement: PresMovement) => (
+ <div className={`presBox-dropdownOption ${activeItem.presMovement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}>
+ {movement}
+ </div>
+ );
+ const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+ const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+ return (
+ <Tooltip title={<div className="dash-tooltip">{diretion}</div>}>
+ <div
+ style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+ onClick={() => this.updateEffectDirection(diretion)}>
+ {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
+ </div>
+ </Tooltip>
+ );
+ };
+ const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
+ return (
+ <input
+ type="range"
+ step={step}
+ min={min}
+ max={max}
+ value={value}
+ className={`toolbar-slider ${active ? '' : 'none'}`}
+ onPointerDown={e => {
+ this._batch = UndoManager.StartBatch('pres slider');
+ e.stopPropagation();
+ }}
+ onPointerUp={() => this._batch?.end()}
+ onChange={e => {
+ e.stopPropagation();
+ change(e.target.value);
+ }}
+ />
+ );
+ };
if (activeItem && targetDoc) {
const type = targetDoc.type;
const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
@@ -1283,46 +1261,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom';
return (
<div
- className={`presBox-ribbon ${this.transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
- onPointerDown={e => e.stopPropagation()}
- onPointerUp={e => e.stopPropagation()}
+ className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
+ onPointerDown={StopEvent}
+ onPointerUp={StopEvent}
onClick={action(e => {
e.stopPropagation();
- this.openMovementDropdown = false;
- this.openEffectDropdown = false;
+ this._openMovementDropdown = false;
+ this._openEffectDropdown = false;
})}>
<div className="ribbon-box">
Movement
- {isPresCollection || (isPresCollection && isPinWithView) ? (
- <div className="ribbon-property" style={{ marginLeft: 0, height: 25, textAlign: 'left', paddingLeft: 5, paddingRight: 5, fontSize: 10 }}>
- {this.scrollable ? 'Scroll to pinned view' : !isPinWithView ? 'No movement' : 'Pan & Zoom to pinned view'}
- </div>
- ) : (
- <div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this.openMovementDropdown = !this.openMovementDropdown;
- })}
- style={{ borderBottomLeftRadius: this.openMovementDropdown ? 0 : 5, border: this.openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
- {this.setMovementName(activeItem.presMovement, activeItem)}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
- <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={e => e.stopPropagation()} style={{ display: this.openMovementDropdown ? 'grid' : 'none' }}>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.None ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.None)}>
- None
- </div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Zoom ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Zoom)}>
- Pan {'&'} Zoom
- </div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Pan ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Pan)}>
- Pan
- </div>
- <div className={`presBox-dropdownOption ${activeItem.presMovement === PresMovement.Jump ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateMovement(PresMovement.Jump)}>
- Jump cut
- </div>
- </div>
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openMovementDropdown = !this._openMovementDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, border: this._openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ {this.movementName(activeItem)}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}>
+ {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.None)}
+ {presMovement(PresMovement.Zoom)}
+ {presMovement(PresMovement.Pan)}
+ {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.Jump)}
</div>
- )}
+ </div>
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Zoom (% screen filled)</div>
<div className="ribbon-property">
@@ -1337,21 +1301,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
</div>
- <input
- type="range"
- step="1"
- min="0"
- max="150"
- value={zoom}
- className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}
- id="toolbar-slider"
- onPointerDown={() => (this._batch = UndoManager.StartBatch('presZoom'))}
- onPointerUp={() => this._batch?.end()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- this.setZoom(e.target.value);
- }}
- />
+ {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Movement Speed</div>
<div className="ribbon-property">
@@ -1366,21 +1316,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
</div>
- <input
- type="range"
- step="0.1"
- min="0.1"
- max="10"
- value={transitionSpeed}
- className={`toolbar-slider ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}
- id="toolbar-slider"
- onPointerDown={() => (this._batch = UndoManager.StartBatch('presTransition'))}
- onPointerUp={() => this._batch?.end()}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- this.setTransitionTime(e.target.value);
- }}
- />
+ {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)}
<div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
<div className="slider-text">Fast</div>
<div className="slider-text">Medium</div>
@@ -1391,35 +1327,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
Visibility {'&'} Duration
<div className="ribbon-doubleButton">
{isPresCollection ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Hide before presented'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
<div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
Hide before
</div>
</Tooltip>
)}
{isPresCollection ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Hide after presented'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
<div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
Hide after
</div>
</Tooltip>
)}
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Open in lightbox view'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
<div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
Lightbox
</div>
@@ -1441,26 +1362,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
</div>
- <input
- type="range"
- step="0.1"
- min="0.1"
- max="20"
- value={duration}
- style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'block' }}
- className={'toolbar-slider'}
- id="duration-slider"
- onPointerDown={() => {
- this._batch = UndoManager.StartBatch('presDuration');
- }}
- onPointerUp={() => {
- if (this._batch) this._batch.end();
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- this.setDurationTime(e.target.value);
- }}
- />
+ {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
<div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
<div className="slider-text">Short</div>
<div className="slider-text">Medium</div>
@@ -1476,98 +1378,30 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="presBox-dropdown"
onClick={action(e => {
e.stopPropagation();
- this.openEffectDropdown = !this.openEffectDropdown;
+ this._openEffectDropdown = !this._openEffectDropdown;
})}
- style={{ borderBottomLeftRadius: this.openEffectDropdown ? 0 : 5, border: this.openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
{effect.toString()}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this.openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
- <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this.openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
- <div
- className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.None || !this.activeItem.presEffect ? 'active' : ''}`}
- onPointerDown={e => e.stopPropagation()}
- onClick={() => this.updateEffect(PresEffect.None)}>
- None
- </div>
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Fade ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Fade)}>
- Fade In
- </div>
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Flip ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Flip)}>
- Flip
- </div>
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Rotate ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Rotate)}>
- Rotate
- </div>
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Bounce ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Bounce)}>
- Bounce
- </div>
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === PresEffect.Roll ? 'active' : ''}`} onPointerDown={e => e.stopPropagation()} onClick={() => this.updateEffect(PresEffect.Roll)}>
- Roll
- </div>
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+ {presEffect(PresEffect.None)}
+ {presEffect(PresEffect.Fade)}
+ {presEffect(PresEffect.Flip)}
+ {presEffect(PresEffect.Rotate)}
+ {presEffect(PresEffect.Bounce)}
+ {presEffect(PresEffect.Roll)}
</div>
</div>
<div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}>
<div className="presBox-subheading">Effect direction</div>
- <div className="ribbon-property">{this.effectDirection}</div>
+ <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div>
</div>
<div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}>
- <Tooltip title={<div className="dash-tooltip">{'Enter from left'}</div>}>
- <div
- style={{ gridColumn: 1, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Left ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
- onClick={() => this.updateEffectDirection(PresEffect.Left)}>
- <FontAwesomeIcon icon={'angle-right'} />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Enter from right'}</div>}>
- <div
- style={{ gridColumn: 3, gridRow: 2, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Right ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
- onClick={() => this.updateEffectDirection(PresEffect.Right)}>
- <FontAwesomeIcon icon={'angle-left'} />
- </div>
- </Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Enter from top'}</div>
- </>
- }>
- <div
- style={{ gridColumn: 2, gridRow: 1, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Top ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
- onClick={() => this.updateEffectDirection(PresEffect.Top)}>
- <FontAwesomeIcon icon={'angle-down'} />
- </div>
- </Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Enter from bottom'}</div>
- </>
- }>
- <div
- style={{ gridColumn: 2, gridRow: 3, justifySelf: 'center', color: this.activeItem.presEffectDirection === PresEffect.Bottom ? Colors.LIGHT_BLUE : 'black', cursor: 'pointer' }}
- onClick={() => this.updateEffectDirection(PresEffect.Bottom)}>
- <FontAwesomeIcon icon={'angle-up'} />
- </div>
- </Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Enter from center'}</div>
- </>
- }>
- <div
- style={{
- gridColumn: 2,
- gridRow: 2,
- width: 10,
- height: 10,
- alignSelf: 'center',
- justifySelf: 'center',
- border: this.activeItem.presEffectDirection === PresEffect.Center || !this.activeItem.presEffectDirection ? `solid 2px ${Colors.LIGHT_BLUE}` : 'solid 2px black',
- borderRadius: '100%',
- cursor: 'pointer',
- }}
- onClick={() => this.updateEffectDirection(PresEffect.Center)}></div>
- </Tooltip>
+ {presDirection(PresEffect.Left, 'angle-right', 1, 2, {})}
+ {presDirection(PresEffect.Right, 'angle-left', 3, 2, {})}
+ {presDirection(PresEffect.Top, 'angle-down', 2, 1, {})}
+ {presDirection(PresEffect.Bottom, 'angle-up', 2, 3, {})}
+ {presDirection(PresEffect.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
</div>
</div>
)}
@@ -1581,59 +1415,27 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
- @computed get effectDirection() {
- // prettier-ignore
- switch (this.activeItem.presEffectDirection) {
- case 'left': return 'Enter from left';
- case 'right': return 'Enter from right';
- case 'top': return'Enter from top';
- case 'bottom': return 'Enter from bottom';
- }
- return 'Enter from center';
- }
-
@undoBatch
@action
applyTo = (array: Doc[]) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- this.updateMovement(activeItem.presMovement, true);
- this.updateEffect(activeItem.presEffect, true);
- this.updateEffectDirection(activeItem.presEffectDirection, true);
- array.forEach(doc => {
- const curDoc = Cast(doc, Doc, null);
- const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
- if (tagDoc && targetDoc) {
- curDoc.presTransition = activeItem.presTransition;
- curDoc.presDuration = activeItem.presDuration;
- curDoc.presHideBefore = activeItem.presHideBefore;
- curDoc.presHideAfter = activeItem.presHideAfter;
- }
+ this.updateMovement(this.activeItem.presMovement as PresMovement, true);
+ this.updateEffect(this.activeItem.presEffect as PresEffect, true);
+ this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffect, true);
+ const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
+ array.forEach(curDoc => {
+ curDoc.presTransition = presTransition;
+ curDoc.presDuration = presDuration;
+ curDoc.presHideBefore = presHideBefore;
+ curDoc.presHideAfter = presHideAfter;
});
};
- @computed get mediaStopSlides() {
- const activeItem: Doc = this.activeItem;
- const list = this.childDocs.map((doc, i) => {
- if (i > this.itemIndex) {
- return (
- <option>
- {i + 1}. {StrCast(doc.title)}
- </option>
- );
- }
- });
- return list;
- }
-
@computed get mediaOptionsDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
const clipStart: number = NumCast(activeItem.clipStart);
- const clipEnd: number = NumCast(activeItem.clipEnd);
- const duration = Math.round(NumCast(activeItem[`${Doc.LayoutFieldKey(activeItem)}-duration`]) * 10);
+ const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration']));
const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
- const mediaStopDocStr: string = mediaStopDocInd ? mediaStopDocInd + '. ' + this.childDocs[mediaStopDocInd - 1].title : '';
if (activeItem && targetDoc) {
return (
<div>
@@ -1649,9 +1451,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
<input
className="presBox-input"
- style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }}
+ style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
- value={NumCast(activeItem.presStartTime)}
+ value={NumCast(activeItem.presStartTime).toFixed(2)}
onKeyDown={e => e.stopPropagation()}
onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
activeItem.presStartTime = Number(e.target.value);
@@ -1675,9 +1477,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<input
className="presBox-input"
onKeyDown={e => e.stopPropagation()}
- style={{ textAlign: 'center', width: 30, height: 15, fontSize: 10 }}
+ style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
- value={NumCast(activeItem.presEndTime)}
+ value={NumCast(activeItem.presEndTime).toFixed(2)}
onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
activeItem.presEndTime = Number(e.target.value);
})}
@@ -1695,13 +1497,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{ gridColumn: 1, gridRow: 1 }}
className={`toolbar-slider ${'end'}`}
id="toolbar-slider"
- onPointerDown={() => {
+ onPointerDown={e => {
this._batch = UndoManager.StartBatch('presEndTime');
const endBlock = document.getElementById('endTime');
if (endBlock) {
endBlock.style.color = Colors.LIGHT_GRAY;
endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
}
+ e.stopPropagation();
}}
onPointerUp={() => {
this._batch?.end();
@@ -1725,13 +1528,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{ gridColumn: 1, gridRow: 1 }}
className={`toolbar-slider ${'start'}`}
id="toolbar-slider"
- onPointerDown={() => {
+ onPointerDown={e => {
this._batch = UndoManager.StartBatch('presStartTime');
const startBlock = document.getElementById('startTime');
if (startBlock) {
startBlock.style.color = Colors.LIGHT_GRAY;
startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
}
+ e.stopPropagation();
}}
onPointerUp={() => {
this._batch?.end();
@@ -1747,10 +1551,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}}
/>
</div>
- <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
- <div className="slider-text">{clipStart} s</div>
+ <div className="slider-headers">
+ <div className="slider-text">{clipStart.toFixed(2)} s</div>
<div className="slider-text"></div>
- <div className="slider-text">{clipEnd} s</div>
+ <div className="slider-text">{clipEnd.toFixed(2)} s</div>
</div>
</div>
<div className="ribbon-final-box">
@@ -1804,55 +1608,53 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get newDocumentToolbarDropdown() {
return (
- <div>
- <div
- className={'presBox-toolbar-dropdown'}
- style={{ display: this.newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }}
- onClick={e => e.stopPropagation()}
- onPointerUp={e => e.stopPropagation()}
- onPointerDown={e => e.stopPropagation()}>
- <div className="layout-container" style={{ height: 'max-content' }}>
- <div
- className="layout"
- style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
- onClick={action(() => {
- this.layout = 'blank';
- this.createNewSlide(this.layout);
- })}
- />
- <div
- className="layout"
- style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
- onClick={action(() => {
- this.layout = 'title';
- this.createNewSlide(this.layout);
- })}>
- <div className="title">Title</div>
- <div className="subtitle">Subtitle</div>
- </div>
- <div
- className="layout"
- style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
- onClick={action(() => {
- this.layout = 'header';
- this.createNewSlide(this.layout);
- })}>
- <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
- Section header
- </div>
+ <div
+ className={'presBox-toolbar-dropdown'}
+ style={{ display: this._newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }}
+ onClick={e => e.stopPropagation()}
+ onPointerUp={e => e.stopPropagation()}
+ onPointerDown={e => e.stopPropagation()}>
+ <div className="layout-container" style={{ height: 'max-content' }}>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'blank';
+ this.createNewSlide(this.layout);
+ })}
+ />
+ <div
+ className="layout"
+ style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'title';
+ this.createNewSlide(this.layout);
+ })}>
+ <div className="title">Title</div>
+ <div className="subtitle">Subtitle</div>
+ </div>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'header';
+ this.createNewSlide(this.layout);
+ })}>
+ <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
+ Section header
</div>
- <div
- className="layout"
- style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
- onClick={action(() => {
- this.layout = 'content';
- this.createNewSlide(this.layout);
- })}>
- <div className="title" style={{ alignSelf: 'center' }}>
- Title
- </div>
- <div className="content">Text goes here</div>
+ </div>
+ <div
+ className="layout"
+ style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }}
+ onClick={action(() => {
+ this.layout = 'content';
+ this.createNewSlide(this.layout);
+ })}>
+ <div className="title" style={{ alignSelf: 'center' }}>
+ Title
</div>
+ <div className="content">Text goes here</div>
</div>
</div>
</div>
@@ -1866,73 +1668,71 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get newDocumentDropdown() {
return (
- <div>
- <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div className="ribbon-box">
- Slide Title: <br></br>
- <input
- className="ribbon-textInput"
- placeholder="..."
- type="text"
- name="fname"
- onChange={e => {
- e.stopPropagation();
- e.preventDefault();
- runInAction(() => (this.title = e.target.value));
- }}></input>
+ <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div className="ribbon-box">
+ Slide Title: <br></br>
+ <input
+ className="ribbon-textInput"
+ placeholder="..."
+ type="text"
+ name="fname"
+ onChange={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ runInAction(() => (this.title = e.target.value));
+ }}></input>
+ </div>
+ <div className="ribbon-box">
+ Choose type:
+ <div className="ribbon-doubleButton">
+ <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+ Text
+ </div>
+ <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
+ Freeform
+ </div>
</div>
- <div className="ribbon-box">
- Choose type:
- <div className="ribbon-doubleButton">
- <div title="Text" className={'ribbon-toggle'} style={{ background: this.addFreeform ? '' : Colors.LIGHT_BLUE }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
- Text
- </div>
- <div title="Freeform" className={'ribbon-toggle'} style={{ background: this.addFreeform ? Colors.LIGHT_BLUE : '' }} onClick={action(() => (this.addFreeform = !this.addFreeform))}>
- Freeform
+ </div>
+ <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}>
+ Preset layouts:
+ <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}>
+ <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} />
+ <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}>
+ <div className="title">Title</div>
+ <div className="subtitle">Subtitle</div>
+ </div>
+ <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}>
+ <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
+ Section header
</div>
</div>
- </div>
- <div className="ribbon-box" style={{ display: this.addFreeform ? 'grid' : 'none' }}>
- Preset layouts:
- <div className="layout-container" style={{ height: this.openLayouts ? 'max-content' : '75px' }}>
- <div className="layout" style={{ border: this.layout === 'blank' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'blank'))} />
- <div className="layout" style={{ border: this.layout === 'title' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'title'))}>
- <div className="title">Title</div>
- <div className="subtitle">Subtitle</div>
+ <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}>
+ <div className="title" style={{ alignSelf: 'center' }}>
+ Title
</div>
- <div className="layout" style={{ border: this.layout === 'header' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'header'))}>
- <div className="title" style={{ alignSelf: 'center', fontSize: 10 }}>
- Section header
- </div>
+ <div className="content">Text goes here</div>
+ </div>
+ <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}>
+ <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>
+ Title
</div>
- <div className="layout" style={{ border: this.layout === 'content' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'content'))}>
- <div className="title" style={{ alignSelf: 'center' }}>
- Title
- </div>
- <div className="content">Text goes here</div>
+ <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>
+ Column one text
</div>
- <div className="layout" style={{ border: this.layout === 'twoColumns' ? `solid 2px ${Colors.MEDIUM_BLUE}` : '' }} onClick={action(() => (this.layout = 'twoColumns'))}>
- <div className="title" style={{ alignSelf: 'center', gridColumn: '1/3' }}>
- Title
- </div>
- <div className="content" style={{ gridColumn: 1, gridRow: 2 }}>
- Column one text
- </div>
- <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>
- Column two text
- </div>
+ <div className="content" style={{ gridColumn: 2, gridRow: 2 }}>
+ Column two text
</div>
</div>
- <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}>
- <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} />
- </div>
</div>
- <div className="ribbon-final-box">
- <div
- className={this.title !== '' && ((this.addFreeform && this.layout !== '') || !this.addFreeform) ? 'ribbon-final-button-hidden' : 'ribbon-final-button'}
- onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}>
- Create New Slide
- </div>
+ <div className="open-layout" onClick={action(() => (this.openLayouts = !this.openLayouts))}>
+ <FontAwesomeIcon style={{ transition: 'all 0.3s', transform: this.openLayouts ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'caret-down'} size={'lg'} />
+ </div>
+ </div>
+ <div className="ribbon-final-box">
+ <div
+ className={this.title !== '' && ((this.addFreeform && this.layout !== '') || !this.addFreeform) ? 'ribbon-final-button-hidden' : 'ribbon-final-button'}
+ onClick={() => this.createNewSlide(this.layout, this.title, this.addFreeform)}>
+ Create New Slide
</div>
</div>
</div>
@@ -1981,7 +1781,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Dropdown that appears when the user wants to begin presenting (either minimize or sidebar view)
@computed get presentDropdown() {
return (
- <div className={`dropdown-play ${this.presentTools ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div className={`dropdown-play ${this._presentTools ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
<div
className="dropdown-play-button"
onClick={undoBatch(
@@ -2067,115 +1867,109 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeFontColor = targetDoc['pres-text-color'] ? StrCast(targetDoc['pres-text-color']) : 'Black';
const viewedFontColor = targetDoc['pres-text-viewed-color'] ? StrCast(targetDoc['pres-text-viewed-color']) : 'Black';
return (
- <div>
- <div
- className={`presBox-ribbon ${this.progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`}
- onClick={e => e.stopPropagation()}
- onPointerUp={e => e.stopPropagation()}
- onPointerDown={e => e.stopPropagation()}>
- <div className="ribbon-box">
- {this.stringType} selected
- <div
- className="ribbon-doubleButton"
- style={{
- borderTop: 'solid 1px darkgrey',
- display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
- }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}>
- Contents
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}>
- Edit
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Active text color</div>
- <div
- className="ribbon-colorBox"
- style={{ backgroundColor: activeFontColor, height: 15, width: 15 }}
- onClick={action(() => {
- this.openActiveColorPicker = !this.openActiveColorPicker;
- })}></div>
+ <div className={`presBox-ribbon ${this._progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div className="ribbon-box">
+ {this.stringType} selected
+ <div
+ className="ribbon-doubleButton"
+ style={{
+ borderTop: 'solid 1px darkgrey',
+ display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
+ }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}>
+ Contents
</div>
- {this.activeColorPicker}
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Viewed font color</div>
- <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div>
+ <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}>
+ Edit
</div>
- {this.viewedColorPicker}
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
+ <div className="presBox-subheading">Active text color</div>
<div
- className="ribbon-doubleButton"
- style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}>
- Zoom
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}>
- Edit
- </div>
+ className="ribbon-colorBox"
+ style={{ backgroundColor: activeFontColor, height: 15, width: 15 }}
+ onClick={action(() => {
+ this.openActiveColorPicker = !this.openActiveColorPicker;
+ })}></div>
+ </div>
+ {this.activeColorPicker}
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
+ <div className="presBox-subheading">Viewed font color</div>
+ <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div>
+ </div>
+ {this.viewedColorPicker}
+ <div
+ className="ribbon-doubleButton"
+ style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}>
+ Zoom
</div>
- <div
- className="ribbon-doubleButton"
- style={{
- borderTop: 'solid 1px darkgrey',
- display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
- }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}>
- Scroll
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}>
- Edit
- </div>
+ <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}>
+ Edit
</div>
</div>
- <div className="ribbon-final-box">
- Frames
- <div className="ribbon-doubleButton">
- <div className="ribbon-frameSelector">
- <div
- key="back"
- title="back frame"
- className="backKeyframe"
- onClick={e => {
- e.stopPropagation();
- this.prevKeyframe(targetDoc, activeItem);
- }}>
- <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
- </div>
- <div
- key="num"
- title="toggle view all"
- className="numKeyframe"
- style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
- onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}>
- {NumCast(targetDoc._currentFrame)}
- </div>
- <div
- key="fwd"
- title="forward frame"
- className="fwdKeyframe"
- onClick={e => {
- e.stopPropagation();
- this.nextKeyframe(targetDoc, activeItem);
- }}>
- <FontAwesomeIcon icon={'caret-right'} size={'lg'} />
- </div>
- </div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Last frame'}</div>
- </>
- }>
- <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div>
- </Tooltip>
+ <div
+ className="ribbon-doubleButton"
+ style={{
+ borderTop: 'solid 1px darkgrey',
+ display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
+ }}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}>
+ Scroll
</div>
- <div className="ribbon-frameList">
- {this.frameListHeader}
- {this.frameList}
+ <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}>
+ Edit
</div>
- <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}>
- Play
+ </div>
+ </div>
+ <div className="ribbon-final-box">
+ Frames
+ <div className="ribbon-doubleButton">
+ <div className="ribbon-frameSelector">
+ <div
+ key="back"
+ title="back frame"
+ className="backKeyframe"
+ onClick={e => {
+ e.stopPropagation();
+ this.prevKeyframe(targetDoc, activeItem);
+ }}>
+ <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
+ </div>
+ <div
+ key="num"
+ title="toggle view all"
+ className="numKeyframe"
+ style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
+ onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}>
+ {NumCast(targetDoc._currentFrame)}
+ </div>
+ <div
+ key="fwd"
+ title="forward frame"
+ className="fwdKeyframe"
+ onClick={e => {
+ e.stopPropagation();
+ this.nextKeyframe(targetDoc, activeItem);
+ }}>
+ <FontAwesomeIcon icon={'caret-right'} size={'lg'} />
+ </div>
</div>
+ <Tooltip
+ title={
+ <>
+ <div className="dash-tooltip">{'Last frame'}</div>
+ </>
+ }>
+ <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div>
+ </Tooltip>
+ </div>
+ <div className="ribbon-frameList">
+ {this.frameListHeader}
+ {this.frameList}
+ </div>
+ <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}>
+ Play
</div>
</div>
</div>
@@ -2186,42 +1980,32 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
switchActive = (color: ColorState) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const val = String(color.hex);
- targetDoc['pres-text-color'] = val;
+ this.targetDoc['pres-text-color'] = String(color.hex);
return true;
};
@undoBatch
@action
switchPresented = (color: ColorState) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const val = String(color.hex);
- targetDoc['pres-text-viewed-color'] = val;
+ this.targetDoc['pres-text-viewed-color'] = String(color.hex);
return true;
};
@computed get activeColorPicker() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
return !this.openActiveColorPicker ? null : (
<SketchPicker
onChange={this.switchActive}
presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(targetDoc['pres-text-color'])}
+ color={StrCast(this.targetDoc['pres-text-color'])}
/>
);
}
@computed get viewedColorPicker() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
return !this.openViewedColorPicker ? null : (
<SketchPicker
onChange={this.switchPresented}
presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(targetDoc['pres-text-viewed-color'])}
+ color={StrCast(this.targetDoc['pres-text-viewed-color'])}
/>
);
}
@@ -2264,12 +2048,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//Toggle whether the user edits or not
@action
editScrollProgressivize = (e: React.MouseEvent) => {
- const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
if (!targetDoc.editScrollProgressivize) {
if (!targetDoc.scrollProgressivize) {
targetDoc.scrollProgressivize = true;
- activeItem.scrollProgressivize = true;
+ this.activeItem.scrollProgressivize = true;
}
targetDoc.editScrollProgressivize = true;
} else {
@@ -2281,8 +2064,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
progressivizeScroll = (e: React.MouseEvent) => {
e.stopPropagation();
- const activeItem: Doc = this.activeItem;
- activeItem.scrollProgressivize = !activeItem.scrollProgressivize;
+ this.activeItem.scrollProgressivize = !this.activeItem.scrollProgressivize;
const targetDoc: Doc = this.targetDoc;
targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize;
// CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame));
@@ -2384,21 +2166,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
@observable
- toggleDisplayMovement = (doc: Doc) => {
- if (doc.displayMovement) doc.displayMovement = false;
- else doc.displayMovement = true;
- };
+ toggleDisplayMovement = (doc: Doc) => (doc.displayMovement = !doc.displayMovement);
@action
checkList = (doc: Doc, list: any): number => {
const x: List<number> = list;
- if (x && x.length >= NumCast(doc._currentFrame) + 1) {
+ if (x?.length >= NumCast(doc._currentFrame) + 1) {
return x[NumCast(doc._currentFrame)];
} else if (x) {
x.length = NumCast(doc._currentFrame) + 1;
x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1];
return x[NumCast(doc._currentFrame)];
- } else return 100;
+ }
+ return 100;
};
@computed get progressivizeChildDocs() {
@@ -2452,26 +2232,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@action
- nextAppearFrame = (doc: Doc, i: number): void => {
- // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
- // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null);
- const appearFrame = Cast(doc.appearFrame, 'number', null);
- if (appearFrame === undefined) {
- doc.appearFrame = 0;
- }
- doc.appearFrame = appearFrame + 1;
+ nextAppearFrame = (doc: Doc, i: number) => {
+ doc.appearFrame = (Cast(doc.appearFrame, 'number', null) ?? 0) + 1;
this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
};
@action
- prevAppearFrame = (doc: Doc, i: number): void => {
- // const activeItem = Cast(this.childDocs[this.itemIndex], Doc, null);
- // const targetDoc = Cast(activeItem?.presentationTargetDoc, Doc, null);
- const appearFrame = Cast(doc.appearFrame, 'number', null);
- if (appearFrame === undefined) {
- doc.appearFrame = 0;
- }
- doc.appearFrame = Math.max(0, appearFrame - 1);
+ prevAppearFrame = (doc: Doc, i: number) => {
+ doc.appearFrame = Math.max(0, (Cast(doc.appearFrame, 'number', null) ?? 0) - 1);
this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
};
@@ -2500,31 +2268,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @computed get moreInfoDropdown() {
- return <div></div>;
- }
-
@computed
get toolbarWidth(): number {
- const width = this.props.PanelWidth();
- return width;
+ return this.props.PanelWidth();
}
@action
- toggleProperties = () => {
- if (SettingsManager.propertiesWidth > 0) {
- SettingsManager.propertiesWidth = 0;
- } else {
- SettingsManager.propertiesWidth = 250;
- }
- };
+ toggleProperties = () => (SettingsManager.propertiesWidth = SettingsManager.propertiesWidth > 0 ? 0 : 250);
@computed get toolbar() {
const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
- const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation;
const activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
return mode === CollectionViewType.Carousel3D ? null : (
@@ -2533,12 +2289,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<FontAwesomeIcon icon={"plus"} />
<FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
</div></Tooltip> */}
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'View paths'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'View paths'}</div>}>
<div
style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
className={'toolbar-button'}
@@ -2557,22 +2308,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
<div className="toolbar-divider" /> */}
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>
- </>
- }>
- <div className="toolbar-button" style={{ cursor: presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
- <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: presKeyEvents ? activeColor : inactiveColor }} />
+ <Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}>
+ <div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
+ <FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
</div>
</Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{propTitle}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{propTitle}</div>}>
<div className="toolbar-button" style={{ position: 'absolute', right: 4, fontSize: 16 }} onClick={this.toggleProperties}>
<FontAwesomeIcon className={'toolbar-thumbtack'} icon={propIcon} style={{ color: SettingsManager.propertiesWidth > 0 ? activeColor : inactiveColor }} />
</div>
@@ -2592,17 +2333,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
return (
- <div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+ <div className="presBox-buttons" style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
{isMini ? null : (
<select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>
+ <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
List
</option>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Tree}>
+ <option onPointerDown={StopEvent} value={CollectionViewType.Tree}>
Tree
</option>
{Doc.noviceMode ? null : (
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>
+ <option onPointerDown={StopEvent} value={CollectionViewType.Carousel3D}>
3D Carousel
</option>
)}
@@ -2623,11 +2364,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
{mode === CollectionViewType.Carousel3D || isMini ? null : (
<div
- className={`presBox-button-right ${this.presentTools ? 'active' : ''}`}
+ className={`presBox-button-right ${this._presentTools ? 'active' : ''}`}
onClick={action(() => {
- if (this.childDocs.length) this.presentTools = !this.presentTools;
+ if (this.childDocs.length) this._presentTools = !this._presentTools;
})}>
- <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this.presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} />
+ <FontAwesomeIcon className="dropdown" style={{ margin: 0, transform: this._presentTools ? 'rotate(180deg)' : 'rotate(0deg)' }} icon={'angle-down'} />
{this.presentDropdown}
</div>
)}
@@ -2639,16 +2380,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@action
- getList = (list: any): List<number> => {
- const x: List<number> = list;
- return x;
- };
+ getList = (list: any): List<number> => list;
@action
updateList = (list: any): List<number> => {
const targetDoc: Doc = this.targetDoc;
const x: List<number> = list;
- x.length + 1;
x[x.length - 1] = NumCast(targetDoc._scrollY);
return x;
};
@@ -2656,8 +2393,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
newFrame = () => {
const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const type: string = StrCast(targetDoc.type);
+ const type: string = StrCast(this.targetDoc.type);
if (!activeItem.frameList) activeItem.frameList = new List<number>();
switch (type) {
case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
@@ -2671,12 +2407,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="frameList-header">
&nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
<div className={'frameList-headerButtons'}>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Add frame by example'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}>
<div
className={'headerButton'}
onClick={e => {
@@ -2686,12 +2417,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Edit in collection'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
<div
className={'headerButton'}
onClick={e => {
@@ -2707,27 +2433,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get frameList() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const frameList: List<number> = this.getList(activeItem.frameList);
- if (frameList) {
- const frameItems = frameList.map(value => <div className="framList-item"></div>);
- return <div className="frameList-container">{frameItems}</div>;
- } else return null;
+ const frameList: List<number> = this.getList(this.activeItem.frameList);
+ return !frameList ? null : (
+ <div className="frameList-container">
+ {frameList.map(value => (
+ <div className="framList-item" />
+ ))}
+ </div>
+ );
}
@computed get playButtonFrames() {
- const targetDoc: Doc = this.targetDoc;
- return (
- <>
- {this.targetDoc ? (
- <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
- <div>{NumCast(targetDoc._currentFrame)}</div>
- <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
- <div>{NumCast(targetDoc.lastFrame)}</div>
- </div>
- ) : null}
- </>
+ const targetDoc = this.targetDoc;
+ return !this.targetDoc ? null : (
+ <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
+ <div>{NumCast(targetDoc._currentFrame)}</div>
+ <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
+ <div>{NumCast(targetDoc.lastFrame)}</div>
+ </div>
);
}
@@ -2737,35 +2460,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Case 1: There are still other frames and should go through all frames before going to next slide
return (
<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Loop'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
<div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}>
<FontAwesomeIcon icon={'redo-alt'} />
</div>
</Tooltip>
- <div className="presPanel-divider"></div>
+ <div className="presPanel-divider" />
<div
className="presPanel-button"
style={{ opacity: presStart ? 0.4 : 1 }}
- onClick={() => {
+ onClick={e => {
this.back();
if (this._presTimer) {
clearTimeout(this._presTimer);
this.layoutDoc.presStatus = PresStatus.Manual;
}
+ e.stopPropagation();
}}>
<FontAwesomeIcon icon={'arrow-left'} />
</div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
<div className="presPanel-button" onClick={this.startOrPause}>
<FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
</div>
@@ -2773,23 +2487,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
className="presPanel-button"
style={{ opacity: presEnd ? 0.4 : 1 }}
- onClick={() => {
+ onClick={e => {
this.next();
if (this._presTimer) {
clearTimeout(this._presTimer);
this.layoutDoc.presStatus = PresStatus.Manual;
}
+ e.stopPropagation();
}}>
<FontAwesomeIcon icon={'arrow-right'} />
</div>
<div className="presPanel-divider"></div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Click to return to 1st slide'}</div>
- </>
- }>
- <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.gotoDocument(0, this.activeItem)}>
+ <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
+ <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.nextSlide(0)}>
<b>1</b>
</div>
</Tooltip>
@@ -2850,9 +2560,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
@action
- startMarqueeCreateSlide = () => {
- PresBox.startMarquee = true;
- };
+ startMarqueeCreateSlide = () => (PresBox.startMarquee = true);
AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
var indexNum = 0;
@@ -2882,23 +2590,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
render() {
- // calling this method for keyEvents
- this.isPres;
// needed to ensure that the childDocs are loaded for looking up fields
this.childDocs.slice();
const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
- const presKeyEvents: boolean = this.isPres && this._presKeyEventsActive && this.rootDoc === Doc.ActivePresentation;
const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
- return DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc) ? (
- <div className="miniPres" onClick={e => e.stopPropagation()}>
- <div className="presPanelOverlay" style={{ display: 'inline-flex', height: 30, background: '#323232', top: 0, zIndex: 3000000, boxShadow: presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Loop'}</div>
- </>
- }>
+ return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
+ <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
+ <div
+ className="presPanelOverlay"
+ style={{ display: 'inline-flex', height: 30, background: Doc.ActivePresentation === this.rootDoc ? 'green' : '#323232', top: 0, zIndex: 3000000, boxShadow: this._presKeyEvents ? '0 0 0px 3px ' + Colors.MEDIUM_BLUE : undefined }}>
+ <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
<div
className="presPanel-button"
style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : undefined }}
@@ -2910,12 +2612,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}>
<FontAwesomeIcon icon={'arrow-left'} />
</div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
<div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.startOrPause, false, false)}>
<FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} />
</div>
@@ -2924,12 +2621,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<FontAwesomeIcon icon={'arrow-right'} />
</div>
<div className="presPanel-divider"></div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Click to return to 1st slide'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
<div className="presPanel-button" style={{ border: 'solid 1px white' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.gotoDocument(0, this.activeItem), false, false)}>
<b>1</b>
</div>
@@ -2950,7 +2642,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{this.toolbar}
{this.newDocumentToolbarDropdown}
<div className="presBox-listCont">
- <div className="Slide" style={{ height: `calc(100% - 30px)` }}>
+ <div className="Slide">
{mode !== CollectionViewType.Invalid ? (
<CollectionView
{...this.props}
@@ -2975,25 +2667,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
) : null}
</div>
- {
+ {/* {
// if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack
<Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}>
<button className="add-slide-button" onPointerDown={this.startMarqueeCreateSlide}>
+ NEW SLIDE
</button>
</Tooltip>
- }
+ } */}
</div>
</div>
);
}
+ static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
+ const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
+ const openInTab = (doc: Doc, finished?: () => void) => {
+ CollectionDockingView.AddSplit(doc, 'right');
+ finished?.();
+ };
+ PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext);
+ }
}
ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
- const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
- const openInTab = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, 'right');
- finished?.();
- };
- DocumentManager.Instance.jumpToDocument(bestTarget, true, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, () => PresBox.navigateToDoc(bestTarget, activeItem, true), undefined, true, NumCast(activeItem.presZoom));
+ PresBox.NavigateToDoc(bestTarget, activeItem);
});
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 969f034a8..8a6c2a6dc 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -1,161 +1,159 @@
-$light-blue: #AEDDF8;
-$dark-blue: #5B9FDD;
+$light-blue: #aeddf8;
+$dark-blue: #5b9fdd;
$light-background: #ececec;
$slide-background: #d5dce2;
-$slide-active: #5B9FDD;
+$slide-active: #5b9fdd;
.presItem-container {
- cursor: grab;
- display: flex;
- grid-template-columns: 20px auto;
- font-family: Roboto;
- letter-spacing: normal;
- position: relative;
- pointer-events: all;
- width: 100%;
- height: 100%;
- font-weight: 400;
- -webkit-touch-callout: none;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -ms-user-select: none;
- user-select: none;
- align-items: center;
-
- // .presItem-number {
- // margin-top: 3.5px;
- // font-size: 12px;
- // font-weight: 700;
- // text-align: center;
- // justify-self: center;
- // align-self: flex-start;
- // position: relative;
- // display: inline-block;
- // overflow: hidden;
- // }
+ cursor: grab;
+ display: flex;
+ grid-template-columns: 20px auto;
+ font-family: Roboto;
+ letter-spacing: normal;
+ position: relative;
+ pointer-events: all;
+ width: 100%;
+ height: 100%;
+ font-weight: 400;
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ align-items: center;
+ // .presItem-number {
+ // margin-top: 3.5px;
+ // font-size: 12px;
+ // font-weight: 700;
+ // text-align: center;
+ // justify-self: center;
+ // align-self: flex-start;
+ // position: relative;
+ // display: inline-block;
+ // overflow: hidden;
+ // }
}
.presItem-slide {
- position: relative;
- height: 100%;
- width: 100%;
- border-bottom: .5px solid grey;
- display: flex;
- justify-content: space-between;
- align-items: center;
- grid-template-rows: 16px 10px auto;
- grid-template-columns: max-content max-content max-content max-content auto;
+ position: relative;
+ height: 100%;
+ width: 100%;
+ border-bottom: 0.5px solid grey;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ grid-template-rows: 16px 10px auto;
+ grid-template-columns: max-content max-content max-content max-content auto;
- .presItem-name {
- display: flex;
- min-width: 20px;
- z-index: 300;
- top: 2px;
- align-self: center;
- font-size: 11px;
- font-family: Roboto;
- font-weight: 500;
- position: relative;
- padding-left: 10px;
- padding-right: 10px;
- letter-spacing: normal;
- width: max-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: pre;
- }
+ .presItem-name {
+ display: flex;
+ min-width: 20px;
+ z-index: 300;
+ top: 2px;
+ align-self: center;
+ font-size: 11px;
+ font-family: Roboto;
+ font-weight: 500;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ }
- .presItem-docName {
- min-width: 20px;
- z-index: 300;
- align-self: center;
- font-size: 9px;
- font-family: Roboto;
- font-weight: 300;
- position: relative;
- padding-left: 10px;
- padding-right: 10px;
- letter-spacing: normal;
- width: max-content;
- text-overflow: ellipsis;
- overflow: hidden;
- white-space: pre;
- grid-row: 2;
- grid-column: 1/6;
- }
+ .presItem-docName {
+ min-width: 20px;
+ z-index: 300;
+ align-self: center;
+ font-size: 9px;
+ font-family: Roboto;
+ font-weight: 300;
+ position: relative;
+ padding-left: 10px;
+ padding-right: 10px;
+ letter-spacing: normal;
+ width: max-content;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ grid-row: 2;
+ grid-column: 1/6;
+ }
- .presItem-time {
- align-self: center;
- position: relative;
- padding-right: 10px;
- top: 1px;
- font-size: 10;
- font-weight: 300;
- font-family: Roboto;
- z-index: 300;
- letter-spacing: normal;
- }
+ .presItem-time {
+ align-self: center;
+ position: relative;
+ padding-right: 10px;
+ top: 1px;
+ font-size: 10;
+ font-weight: 300;
+ font-family: Roboto;
+ z-index: 300;
+ letter-spacing: normal;
+ }
- .presItem-embedded {
- overflow: hidden;
- grid-row: 3;
- grid-column: 1/8;
- position: relative;
- display: flex;
- width: auto;
- justify-content: center;
- margin: auto;
- margin-bottom: 2px;
- border-bottom-left-radius: 5px;
- border-bottom-right-radius: 5px;
- }
+ .presItem-embedded {
+ overflow: hidden;
+ grid-row: 3;
+ grid-column: 1/8;
+ position: relative;
+ display: flex;
+ width: auto;
+ justify-content: center;
+ margin: auto;
+ margin-bottom: 2px;
+ border-bottom-left-radius: 5px;
+ border-bottom-right-radius: 5px;
+ }
- .presItem-embeddedMask {
- width: 100%;
- height: 100%;
- position: absolute;
- border-radius: 3px;
- top: 0;
- left: 0;
- z-index: 1;
- overflow: hidden;
- }
+ .presItem-embeddedMask {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ border-radius: 3px;
+ top: 0;
+ left: 0;
+ z-index: 1;
+ overflow: hidden;
+ }
+ .presItem-slideButtons {
+ display: flex;
+ grid-column: 7;
+ grid-row: 1/3;
+ width: max-content;
+ justify-self: right;
+ justify-content: flex-end;
- .presItem-slideButtons {
+ .slideButton {
+ cursor: pointer;
+ position: relative;
+ border-radius: 100px;
+ z-index: 300;
+ width: 18px;
+ height: 18px;
display: flex;
- grid-column: 7;
- grid-row: 1/3;
- width: max-content;
- justify-self: right;
- justify-content: flex-end;
-
- .slideButton {
- cursor: pointer;
- position: relative;
- border-radius: 100px;
- z-index: 300;
- width: 18px;
- height: 18px;
- display: flex;
- font-size: 12px;
- justify-self: center;
- align-self: center;
- background-color: rgba(0, 0, 0, 0.5);
- color: white;
- justify-content: center;
- align-items: center;
- transition: 0.2s;
- margin-right: 3px;
- }
+ font-size: 12px;
+ justify-self: center;
+ align-self: center;
+ background-color: rgba(0, 0, 0, 0.5);
+ color: white;
+ justify-content: center;
+ align-items: center;
+ transition: 0.2s;
+ margin-right: 3px;
+ }
- .slideButton:hover {
- background-color: rgba(0, 0, 0, 1);
- transform: scale(1.2);
- }
- }
+ .slideButton:hover {
+ background-color: rgba(0, 0, 0, 1);
+ transform: scale(1.2);
+ }
+ }
}
// .presItem-slide:hover {
@@ -194,7 +192,8 @@ $slide-active: #5B9FDD;
// }
.presItem-slide.active {
- box-shadow: 0 0 0px 2.5px $dark-blue;
+ //box-shadow: 0 0 0px 2.5px $dark-blue;
+ border: $dark-blue solid 2.5px;
}
.presItem-slide.group {
@@ -239,38 +238,38 @@ $slide-active: #5B9FDD;
}
.presItem-multiDrag {
- font-family: Roboto;
- font-weight: 600;
- color: white;
- text-align: center;
- justify-content: center;
- align-content: center;
- width: 100px;
- height: 30px;
- position: absolute;
- background-color: $dark-blue;
- z-index: 4000;
- border-radius: 10px;
- box-shadow: black 0.4vw 0.4vw 0.8vw;
- line-height: 30px;
+ font-family: Roboto;
+ font-weight: 600;
+ color: white;
+ text-align: center;
+ justify-content: center;
+ align-content: center;
+ width: 100px;
+ height: 30px;
+ position: absolute;
+ background-color: $dark-blue;
+ z-index: 4000;
+ border-radius: 10px;
+ box-shadow: black 0.4vw 0.4vw 0.8vw;
+ line-height: 30px;
}
.presItem-miniSlide {
- font-weight: 700;
- font-size: 12;
- grid-column: 1/8;
- align-self: center;
- justify-self: center;
- background-color: #d5dce2;
- width: 26px;
- text-align: center;
- height: 26px;
- line-height: 28px;
- border-radius: 100%;
+ font-weight: 700;
+ font-size: 12;
+ grid-column: 1/8;
+ align-self: center;
+ justify-self: center;
+ background-color: #d5dce2;
+ width: 26px;
+ text-align: center;
+ height: 26px;
+ line-height: 28px;
+ border-radius: 100%;
}
.presItem-miniSlide.active {
- box-shadow: 0 0 0px 1.5px $dark-blue;
+ box-shadow: 0 0 0px 1.5px $dark-blue;
}
.expandButton {
@@ -306,4 +305,4 @@ $slide-active: #5B9FDD;
top: 1;
font-weight: 600;
font-size: 12;
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 91196ca21..e6d08cd53 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
@@ -24,6 +24,8 @@ import { PresBox } from './PresBox';
import './PresElementBox.scss';
import { PresMovement } from './PresEnums';
import React = require('react');
+import { InkField } from '../../../../fields/InkField';
+import { RichTextField } from '../../../../fields/RichTextField';
/**
* This class models the view a document added to presentation will have in the presentation.
* It involves some functionality for its buttons and options.
@@ -48,7 +50,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return this.presBox.presStatus;
}
@computed get selectedArray() {
- return this.presBoxView?._selectedArray;
+ return this.presBoxView?.selectedArray;
}
@computed get presBoxView() {
const vpath = this.props.docViewPath();
@@ -79,9 +81,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
@action
- presExpandDocumentClick = () => {
- this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
- };
+ presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton);
embedHeight = (): number => 97;
// embedWidth = () => this.props.PanelWidth();
@@ -160,15 +160,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
));
return groupSlides;
}
- @computed get duration() {
- let durationInS: number;
- if (this.rootDoc.type === DocumentType.AUDIO || this.rootDoc.type === DocumentType.VID) {
- durationInS = NumCast(this.rootDoc.presEndTime) - NumCast(this.rootDoc.presStartTime);
- durationInS = Math.round(durationInS * 10) / 10;
- } else if (this.rootDoc.presDuration) durationInS = NumCast(this.rootDoc.presDuration) / 1000;
- else durationInS = 2;
- return 'D: ' + durationInS + 's';
- }
@computed get transition() {
let transitionInS: number;
@@ -320,6 +311,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
case DocumentType.RTF:
const scroll = targetDoc._scrollTop;
activeItem.presPinViewScroll = scroll;
+ if (targetDoc.type === DocumentType.RTF) {
+ activeItem.presData = targetDoc.text instanceof RichTextField ? targetDoc.text[Copy]() : targetDoc.text;
+ }
+ break;
+ case DocumentType.INK:
+ activeItem.presData = targetDoc.data instanceof InkField ? targetDoc.data[Copy]() : targetDoc.data;
break;
case DocumentType.VID:
case DocumentType.AUDIO:
@@ -337,6 +334,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
activeItem.presPinViewY = y;
activeItem.presPinViewScale = scale;
}
+
+ activeItem.presX = NumCast(targetDoc.x);
+ activeItem.presY = NumCast(targetDoc.y);
+ activeItem.presRot = NumCast(targetDoc.jitterRotation);
+ activeItem.presWidth = NumCast(targetDoc.width);
+ activeItem.presHeight = NumCast(targetDoc.height);
};
@computed get recordingIsInOverlay() {
@@ -381,7 +384,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
hideRecording = (e: React.MouseEvent, iconClick: boolean = false) => {
e.stopPropagation();
this.removeAllRecordingInOverlay();
-
// if (iconClick) PresElementBox.showVideo = false;
};
@@ -401,8 +403,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
- startRecording = (activeItem: Doc) => {
- console.log('start recording', 'activeItem', activeItem);
+ startRecording = (e: React.MouseEvent, activeItem: Doc) => {
+ e.stopPropagation();
if (PresElementBox.videoIsRecorded(activeItem)) {
// if we already have an existing recording
this.showRecording(activeItem, true);
@@ -429,8 +431,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
activeItem.recording = recording;
// make recording box appear in the bottom right corner of the screen
- recording.x = window.innerWidth - recording[WidthSym]() - 20;
- recording.y = window.innerHeight - recording[HeightSym]() - 20;
+ recording.overlayX = window.innerWidth - recording[WidthSym]() - 20;
+ recording.overlayY = window.innerHeight - recording[HeightSym]() - 20;
Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording);
}
};
@@ -446,6 +448,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get mainItem() {
const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false;
+ const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres;
const toolbarWidth: number = this.toolbarWidth;
const showMore: boolean = this.toolbarWidth >= 300;
const miniView: boolean = this.toolbarWidth <= 110;
@@ -477,26 +480,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerOver={this.onPointerOver}
onPointerLeave={this.onPointerLeave}
onPointerDown={this.headerDown}>
- {/* {miniView ?
- // when width is LESS than 110 px
- <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
- {`${this.indexInPres + 1}.`}
- </div>
- :
- // when width is MORE than 110 px
- <div className="presItem-number">
- {`${this.indexInPres + 1}.`}
- </div>} */}
- {/* <div className="presItem-number">
- {`${this.indexInPres + 1}.`}
- </div> */}
- {miniView ? null : (
+ {miniView ? (
+ <div className={`presItem-miniSlide ${isSelected ? 'active' : ''}`} ref={this._dragRef}>
+ {`${this.indexInPres + 1}.`}
+ </div>
+ ) : (
<div
- ref={miniView ? null : this._dragRef}
- className={`presItem-slide ${isSelected ? 'active' : ''}`}
+ ref={this._dragRef}
+ className={`presItem-slide ${isCurrent ? 'active' : ''}`}
style={{
backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isSelected ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
+ //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
+ border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined,
}}>
<div
className="presItem-name"
@@ -510,79 +505,37 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
{/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
{/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}
- <div className={'presItem-slideButtons'}>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Update view'}</div>
- </>
- }>
+ <div className="presItem-slideButtons">
+ <Tooltip title={<div className="dash-tooltip">Update view</div>}>
<div className="slideButton" onClick={() => this.updateView(targetDoc, activeItem)} style={{ fontWeight: 700, display: activeItem.presPinView ? 'flex' : 'none' }}>
V
</div>
</Tooltip>
-
- {this.recordingIsInOverlay ? (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Hide Recording'}</div>
- </>
- }>
- <div className="slideButton" onClick={e => this.hideRecording(e, true)} style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={'video-slash'} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- ) : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{`${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>
- </>
- }>
- <div
- className="slideButton"
- onClick={e => {
- e.stopPropagation();
- this.startRecording(activeItem);
- }}
- style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={'video'} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- )}
-
- {this.indexInPres === 0 ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>
- </>
- }>
- <div
- className="slideButton"
- onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
- style={{
- zIndex: 1000 - this.indexInPres,
- fontWeight: 700,
- backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
- height: activeItem.groupWithUp ? 53 : 18,
- transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
- }}>
- <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
- <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
- </div>
+ {!Doc.noviceMode && <Tooltip title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
+ <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>}
+ <Tooltip title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
+ <div
+ className="slideButton"
+ onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
+ style={{
+ display: this.indexInPres === 0 ? 'none' : '',
+ zIndex: 1000 - this.indexInPres,
+ fontWeight: 700,
+ backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+ height: activeItem.groupWithUp ? 53 : 18,
+ transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
+ }}>
+ <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
+ <FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
</div>
- </Tooltip>
- )}
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>
- </>
- }>
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}>
<div
- className={'slideButton'}
+ className="slideButton"
onClick={e => {
e.stopPropagation();
this.presExpandDocumentClick();
@@ -590,18 +543,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
<FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Remove from presentation'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">Remove from presentation</div>}>
<div className={'slideButton'} onClick={this.removeItem}>
<FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
</div>
- {/* <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div> */}
{this.renderEmbeddedInline}
</div>
)}
diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index 93ab323fb..c6a222c3a 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -1,28 +1,28 @@
export enum PresMovement {
- Zoom = "zoom",
- Pan = "pan",
- Jump = "jump",
- None = "none",
+ Zoom = 'zoom',
+ Pan = 'pan',
+ Jump = 'jump',
+ None = 'none',
}
export enum PresEffect {
- Zoom = "Zoom",
- Lightspeed = "Lightspeed",
- Fade = "Fade in",
- Flip = "Flip",
- Rotate = "Rotate",
- Bounce = "Bounce",
- Roll = "Roll",
- None = "None",
- Left = "left",
- Right = "right",
- Center = "center",
- Top = "top",
- Bottom = "bottom"
+ Zoom = 'Zoom',
+ Lightspeed = 'Lightspeed',
+ Fade = 'Fade in',
+ Flip = 'Flip',
+ Rotate = 'Rotate',
+ Bounce = 'Bounce',
+ Roll = 'Roll',
+ None = 'None',
+ Left = 'Enter from left',
+ Right = 'Enter from right',
+ Center = 'Enter from center',
+ Top = 'Enter from Top',
+ Bottom = 'Enter from bottom',
}
export enum PresStatus {
- Autoplay = "auto",
- Manual = "manual",
- Edit = "edit"
-} \ No newline at end of file
+ Autoplay = 'auto',
+ Manual = 'manual',
+ Edit = 'edit',
+}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 44f815336..ee418a02f 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -107,7 +107,8 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
pointerEvents: this.props.pointerEvents?.() as any,
outline: brushed === Doc.DocBrushStatus.linkHighlighted ? 'solid 1px lightBlue' : undefined,
backgroundColor: brushed === Doc.DocBrushStatus.highlighted ? 'orange' : StrCast(this.props.document.backgroundColor),
- }}></div>
+ }}
+ />
);
}
}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index a45edfbca..5c10c7cef 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -64,6 +64,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _styleRule: any; // stylesheet rule for making hyperlinks clickable
private _retries = 0; // number of times tried to create the PDF viewer
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -165,10 +166,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
- focusSpeed = 500;
-
if (!this._pdfViewer) this._initialScroll = scrollTo;
- else if (smooth) smoothScroll(focusSpeed, mainCont, scrollTo);
+ else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
}
} else {
@@ -176,9 +175,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
return focusSpeed;
};
- crop = (region: Doc | undefined, addCrop?: boolean) => {
- return this.props.crop(region, addCrop);
- };
+ crop = (region: Doc | undefined, addCrop?: boolean) => this.props.crop(region, addCrop);
+ brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
@action
setupPdfJsViewer = async () => {
@@ -327,13 +325,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
query: searchString,
};
if (clear) {
- this._pdfViewer?.findController.executeCommand('reset', { query: '' });
+ this._pdfViewer?.eventBus.dispatch('reset', {});
} else if (!searchString) {
bwd ? this.prevAnnotation() : this.nextAnnotation();
} else if (this._pdfViewer?.pageViewsReady) {
- this._pdfViewer.findController.executeCommand('findagain', findOpts);
+ this._pdfViewer?.eventBus.dispatch('find', { ...findOpts, type: 'again' });
} else if (this._mainCont.current) {
- const executeFind = () => this._pdfViewer.findController.executeCommand('find', findOpts);
+ const executeFind = () => this._pdfViewer?.eventBus.dispatch('find', findOpts);
this._mainCont.current.addEventListener('pagesloaded', executeFind);
this._mainCont.current.addEventListener('pagerendered', executeFind);
}
@@ -448,6 +446,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
};
setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
+ setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
@action
onZoomWheel = (e: React.WheelEvent) => {
@@ -505,6 +504,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
mixBlendMode: mixBlendMode,
display: display,
transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
+ pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
{...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
@@ -513,6 +513,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
fieldKey={this.props.fieldKey + '-annotations'}
CollectionView={undefined}
setPreviewCursor={this.setPreviewCursor}
+ setBrushViewer={this.setBrushViewer}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
ScreenToLocalTransform={this.overlayTransform}
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index d415e9367..a1131b92e 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -13,7 +13,12 @@
align-items: center;
height: $topbar-height;
background-color: $dark-gray;
+ border-bottom: $standard-border;
+ padding: 0px 10px;
cursor: default;
+ display: flex;
+ justify-content: center;
+ width: 100%;
.topbar-inner-container {
display: flex;
@@ -21,6 +26,7 @@
position: relative;
display: grid;
grid-auto-columns: 33.3% 33.3% 33.3%;
+ width: 100%;
align-items: center;
// &:first-child {
@@ -70,10 +76,8 @@
align-items: center;
gap: 5px;
- .topbar-dashboards {
- display: flex;
- flex-direction: row;
- gap: 5px;
+ .topbar-dashboard-header {
+ font-weight: 600;
}
}
@@ -96,6 +100,27 @@
width: fit-content;
gap: 5px;
+ .logo-container {
+ font-size: 15;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+
+ .logo {
+ background-color: transparent;
+ width: 25px;
+ height: 25px;
+ margin-right: 5px;
+
+ }
+ }
+
.topBar-icon:hover {
background-color: $close-red;
}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index cbcfed06f..7e728306c 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -1,15 +1,14 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { MdBugReport } from 'react-icons/md';
-import { action } from 'mobx';
+import { Button, FontSize, IconButton, Size } from 'browndash-components';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { AclAdmin, Doc } from '../../../fields/Doc';
+import { FaBug, FaCamera } from 'react-icons/fa';
+import { AclAdmin, Doc, DocListCast } from '../../../fields/Doc';
import { StrCast } from '../../../fields/Types';
import { GetEffectiveAcl } from '../../../fields/util';
-import { Utils } from '../../../Utils';
import { DocumentManager } from '../../util/DocumentManager';
-import { SelectionManager } from '../../util/SelectionManager';
+import { ReportManager } from '../../util/ReportManager';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { UndoManager } from '../../util/UndoManager';
@@ -19,7 +18,6 @@ import { DashboardView } from '../DashboardView';
import { Borders, Colors } from '../global/globalEnums';
import { MainView } from '../MainView';
import './TopBar.scss';
-import { ReportManager } from '../../util/ReportManager';
/**
* ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user
@@ -28,83 +26,174 @@ import { ReportManager } from '../../util/ReportManager';
@observer
export class TopBar extends React.Component {
navigateToHome = () => {
- CollectionDockingView.Instance?.CaptureThumbnail()?.then(() => {
+ (CollectionDockingView.Instance?.CaptureThumbnail() ?? new Promise<void>(res => res())).then(() => {
Doc.ActivePage = 'home';
DashboardView.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use
});
};
-
+
+ @observable textColor: string = Colors.LIGHT_GRAY;
+ @observable backgroundColor: string = Colors.DARK_GRAY;
+
+ /**
+ * Returns the left hand side of the topbar.
+ * This side of the topbar contains the different modes.
+ * The modes include:
+ * - Explore mode
+ * - Tracking mode
+ */
+ @computed get topbarLeft() {
+ return (
+ <div className="topbar-left">
+ {Doc.ActiveDashboard ? (
+ <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} isCircle={true} hoverStyle="gray" color={this.textColor} />
+ ) : (
+ <div className="logo-container">
+ <img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img>
+ <span style={{ color: Colors.LIGHT_GRAY, fontWeight: 200 }}>brown</span>
+ <span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span>
+ </div>
+ )}
+ {Doc.ActiveDashboard && !Doc.noviceMode && (
+ <Button
+ text="Explore"
+ tooltip="Browsing mode for directly navigating to documents"
+ fontSize={FontSize.SECONDARY}
+ isActive={MainView.Instance._exploreMode}
+ size={Size.SMALL}
+ color={this.textColor}
+ borderRadius={5}
+ hoverStyle="gray"
+ iconPosition="right"
+ onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))}
+ />
+ )}
+ </div>
+ );
+ }
+
+ /**
+ * Returns the center of the topbar
+ * This part of the topbar contains everything related to the current dashboard including:
+ * - Selection of dashboards
+ * - Creating a new dashboard
+ * - Taking a snapshot of a dashboard
+ */
+ @computed get topbarCenter() {
+ // const dashboardItems = myDashboards.map(board => {
+ // const boardTitle = StrCast(board.title);
+ // console.log(boardTitle);
+ // return {
+ // text: boardTitle,
+ // onClick: () => DashboardView.openDashboard(board),
+ // val: board,
+ // };
+ // });
+ return Doc.ActiveDashboard ? (
+ <div className="topbar-center">
+ <Button
+ text={StrCast(Doc.ActiveDashboard.title)}
+ tooltip="Browsing mode for directly navigating to documents"
+ fontSize={FontSize.SECONDARY}
+ size={Size.SMALL}
+ color={'white'}
+ type="outline"
+ backgroundColor={Colors.MEDIUM_BLUE}
+ borderRadius={5}
+ hoverStyle="none"
+ onClick={(e: React.MouseEvent) => {
+ const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard);
+ ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' });
+ ContextMenu.Instance.addItem({
+ description: 'Snapshot Dashboard',
+ event: async () => {
+ const batch = UndoManager.StartBatch('snapshot');
+ await DashboardView.snapshotDashboard();
+ batch.end();
+ },
+ icon: 'edit',
+ });
+ dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
+ }}
+ />
+ <Button
+ text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'}
+ onClick={() => {
+ SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
+ }}
+ type="outline"
+ fontSize={FontSize.SECONDARY}
+ size={Size.SMALL}
+ borderRadius={5}
+ hoverStyle="gray"
+ />
+ {!Doc.noviceMode && (
+ <IconButton
+ fontSize={FontSize.SECONDARY}
+ isCircle={true}
+ tooltip="Work on a copy of the dashboard layout"
+ size={Size.SMALL}
+ color={this.textColor}
+ borderRadius={Borders.STANDARD}
+ hoverStyle="gray"
+ onClick={async () => {
+ const batch = UndoManager.StartBatch('snapshot');
+ await DashboardView.snapshotDashboard();
+ batch.end();
+ }}
+ icon={<FaCamera />}
+ />
+ )}
+ </div>
+ ) : null;
+ }
+
+ /**
+ * Returns the right hand side of the topbar.
+ * This part of the topbar includes information about the current user,
+ * and allows the user to access their account settings etc.
+ */
+ @computed get topbarRight() {
+ return (
+ <div className="topbar-right">
+ <IconButton size={Size.SMALL} isCircle={true} color={Colors.LIGHT_GRAY} backgroundColor={Colors.DARK_GRAY} hoverStyle="gray" onClick={() => ReportManager.Instance.open()} icon={<FaBug />} />
+ <IconButton
+ size={Size.SMALL}
+ isCircle={true}
+ color={Colors.LIGHT_GRAY}
+ backgroundColor={Colors.DARK_GRAY}
+ hoverStyle="gray"
+ onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}
+ icon={<FontAwesomeIcon icon="question-circle" />}
+ />
+ <IconButton
+ size={Size.SMALL}
+ isCircle={true}
+ color={Colors.LIGHT_GRAY}
+ backgroundColor={Colors.DARK_GRAY}
+ hoverStyle="gray"
+ isActive={SettingsManager.Instance.isOpen}
+ onClick={() => SettingsManager.Instance.open()}
+ icon={<FontAwesomeIcon icon="cog" />}
+ />
+ {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.textColor} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */}
+ </div>
+ );
+ }
+
render() {
- const activeDashboard = Doc.ActiveDashboard;
return (
//TODO:glr Add support for light / dark mode
- <div style={{ pointerEvents: 'all', background: Colors.DARK_GRAY, borderBottom: Borders.STANDARD }} className="topbar-container">
- <div className="topbar-inner-container">
- <div className="topbar-left">
- {activeDashboard ? (
- <>
- <div
- className="topbar-button-icon"
- onClick={e => {
- ContextMenu.Instance.addItem({ description: 'Logout', event: () => window.location.assign(Utils.prepend('/logout')), icon: 'edit' });
- ContextMenu.Instance.displayMenu(e.clientX + 5, e.clientY + 10);
- }}>
- {Doc.CurrentUserEmail}
- </div>
- <div className="topbar-button-icon" onClick={this.navigateToHome}>
- <FontAwesomeIcon icon="home" />
- </div>
- </>
- ) : null}
- </div>
- <div className="topbar-center">
- <div className="topbar-title" onClick={() => activeDashboard && SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(activeDashboard)!, false)}>
- {activeDashboard ? StrCast(activeDashboard.title) : 'Dash'}
- </div>
- <div
- className="topbar-button-icon"
- onClick={e => {
- const dashView = activeDashboard && DocumentManager.Instance.getDocumentView(activeDashboard);
- ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' });
- ContextMenu.Instance.addItem({
- description: 'Snapshot Dashboard',
- event: async () => {
- const batch = UndoManager.StartBatch('snapshot');
- await DashboardView.snapshotDashboard();
- batch.end();
- },
- icon: 'edit',
- });
- dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
- }}>
- <FontAwesomeIcon color="white" size="lg" icon="bars" />
- </div>
- <Tooltip title={<div className="dash-tooltip">Browsing mode for directly navigating to documents</div>} placement="bottom">
- <div className="topbar-button-icon" style={{ background: MainView.Instance._exploreMode ? Colors.LIGHT_BLUE : undefined }} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))}>
- <FontAwesomeIcon color={MainView.Instance._exploreMode ? 'red' : 'white'} icon="eye" size="lg" />
- </div>
- </Tooltip>
- </div>
- <div className="topbar-right">
- {Doc.ActiveDashboard ? (
- <div
- className="topbar-button-icon"
- onClick={() => {
- SharingManager.Instance.open(undefined, activeDashboard);
- }}>
- {GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'view original'}
- </div>
- ) : null}
- <div className="topbar-button-icon" onClick={() => ReportManager.Instance.open()}>
- <MdBugReport />
- </div>
- <div className="topbar-button-icon" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}>
- <FontAwesomeIcon icon="question-circle" />
- </div>
- <div className="topbar-button-icon" onClick={() => SettingsManager.Instance.open()}>
- <FontAwesomeIcon icon="cog" />
- </div>
- </div>
+ <div style={{ pointerEvents: 'all' }} className="topbar-container">
+ <div
+ className="topbar-inner-container"
+ style={{
+ color: this.textColor,
+ background: this.backgroundColor,
+ }}>
+ {this.topbarLeft}
+ {this.topbarCenter}
+ {this.topbarRight}
</div>
</div>
);
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index ee7dd1fc1..02038c426 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -1,5 +1,4 @@
import { action, configure, observable, ObservableMap, Lambda } from 'mobx';
-import "normalize.css";
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { observer } from 'mobx-react';
@@ -21,7 +20,6 @@ URLField;
ScriptField;
CursorField;
-
function applyToDoc(doc: { [index: string]: FieldResult }, key: string, scriptString: string): boolean;
function applyToDoc(doc: { [index: number]: FieldResult }, key: number, scriptString: string): boolean;
function applyToDoc(doc: any, key: string | number, scriptString: string): boolean {
@@ -37,11 +35,11 @@ function applyToDoc(doc: any, key: string | number, scriptString: string): boole
}
configure({
- enforceActions: "observed"
+ enforceActions: 'observed',
});
@observer
-class ListViewer extends React.Component<{ field: List<Field> }>{
+class ListViewer extends React.Component<{ field: List<Field> }> {
@observable
expanded = false;
@@ -49,14 +47,16 @@ class ListViewer extends React.Component<{ field: List<Field> }>{
onClick = (e: React.MouseEvent) => {
this.expanded = !this.expanded;
e.stopPropagation();
- }
+ };
render() {
let content;
if (this.expanded) {
content = (
<div>
- {this.props.field.map((field, index) => <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} />)}
+ {this.props.field.map((field, index) => (
+ <DebugViewer field={field} key={index} setValue={value => applyToDoc(this.props.field, index, value)} />
+ ))}
</div>
);
} else {
@@ -66,7 +66,7 @@ class ListViewer extends React.Component<{ field: List<Field> }>{
<div>
<button onClick={this.onClick}>Toggle</button>
{content}
- </div >
+ </div>
);
}
}
@@ -80,7 +80,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
onClick = (e: React.MouseEvent) => {
this.expanded = !this.expanded;
e.stopPropagation();
- }
+ };
render() {
let content;
@@ -96,10 +96,7 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
});
content = (
<div>
- Document ({this.props.field[Id]})
- <div style={{ paddingLeft: "25px" }}>
- {fields}
- </div>
+ Document ({this.props.field[Id]})<div style={{ paddingLeft: '25px' }}>{fields}</div>
</div>
);
} else {
@@ -109,24 +106,23 @@ class DocumentViewer extends React.Component<{ field: Doc }> {
<div>
<button onClick={this.onClick}>Toggle</button>
{content}
- </div >
+ </div>
);
}
}
@observer
-class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: string): boolean }> {
-
+class DebugViewer extends React.Component<{ field: FieldResult; setValue(value: string): boolean }> {
render() {
let content;
const field = this.props.field;
if (field instanceof List) {
- content = (<ListViewer field={field} />);
+ content = <ListViewer field={field} />;
} else if (field instanceof Doc) {
- content = (<DocumentViewer field={field} />);
- } else if (typeof field === "string") {
+ content = <DocumentViewer field={field} />;
+ } else if (typeof field === 'string') {
content = <p>"{field}"</p>;
- } else if (typeof field === "number" || typeof field === "boolean") {
+ } else if (typeof field === 'number' || typeof field === 'boolean') {
content = <p>{field}</p>;
} else if (field instanceof RichTextField) {
content = <p>RTF: {field.Data}</p>;
@@ -153,28 +149,30 @@ class Viewer extends React.Component {
@action
inputOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.idToAdd = e.target.value;
- }
+ };
@action
onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
- if (e.key === "Enter") {
- DocServer.GetRefField(this.idToAdd).then(action((field: any) => {
- if (field !== undefined) {
- this.fields.push(field);
- }
- }));
- this.idToAdd = "";
+ if (e.key === 'Enter') {
+ DocServer.GetRefField(this.idToAdd).then(
+ action((field: any) => {
+ if (field !== undefined) {
+ this.fields.push(field);
+ }
+ })
+ );
+ this.idToAdd = '';
}
- }
+ };
render() {
return (
<>
- <input value={this.idToAdd}
- onChange={this.inputOnChange}
- onKeyDown={this.onKeyPress} />
+ <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyPress} />
<div>
- {this.fields.map((field, index) => <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>)}
+ {this.fields.map((field, index) => (
+ <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>
+ ))}
</div>
</>
);
@@ -182,11 +180,11 @@ class Viewer extends React.Component {
}
(async function () {
- await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "viewer");
- ReactDOM.render((
- <div style={{ position: "absolute", width: "100%", height: "100%" }}>
+ await DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'viewer');
+ ReactDOM.render(
+ <div style={{ position: 'absolute', width: '100%', height: '100%' }}>
<Viewer />
- </div>),
+ </div>,
document.getElementById('root')
);
-})(); \ No newline at end of file
+})();
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 612fc7fb8..0ceaff968 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -33,7 +33,10 @@ export namespace Field {
return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
}
export function toScriptString(field: Field): string {
- if (typeof field === 'string') return `"${field}"`;
+ if (typeof field === 'string') {
+ if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
+ return `"${field}"`;
+ }
if (typeof field === 'number' || typeof field === 'boolean') return String(field);
if (field === undefined || field === null) return 'null';
return field[ToScriptString]();
@@ -149,6 +152,28 @@ export class Doc extends RefField {
public static set MainDocId(id: string | undefined) {
this.mainDocId = id;
}
+
+ @observable public static CurrentlyLoading: Doc[];
+ // removes from currently loading display
+ @action
+ public static removeCurrentlyLoading(doc: Doc) {
+ if (Doc.CurrentlyLoading) {
+ const index = Doc.CurrentlyLoading.indexOf(doc);
+ index !== -1 && Doc.CurrentlyLoading.splice(index, 1);
+ }
+ }
+
+ // adds doc to currently loading display
+ @action
+ public static addCurrentlyLoading(doc: Doc) {
+ if (!Doc.CurrentlyLoading) {
+ Doc.CurrentlyLoading = [];
+ }
+ if (Doc.CurrentlyLoading.indexOf(doc) === -1) {
+ Doc.CurrentlyLoading.push(doc);
+ }
+ }
+
@observable public static GuestDashboard: Doc | undefined;
@observable public static GuestTarget: Doc | undefined;
@observable public static GuestMobile: Doc | undefined;
@@ -180,7 +205,7 @@ export class Doc extends RefField {
return DocCast(Doc.UserDoc().myRecentlyClosed);
}
public static get MyTrails() {
- return DocCast(Doc.UserDoc().myTrails);
+ return DocCast(Doc.ActiveDashboard?.myTrails);
}
public static get MyOverlayDocs() {
return DocCast(Doc.UserDoc().myOverlayDocs);
@@ -217,6 +242,8 @@ export class Doc extends RefField {
return DocCast(Doc.UserDoc().activeDashboard);
}
public static set ActiveDashboard(val: Doc | undefined) {
+ const overlays = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), null);
+ overlays && (overlays.length = 0);
Doc.UserDoc().activeDashboard = val;
}
public static set ActiveTool(tool: InkTool) {
@@ -225,11 +252,13 @@ export class Doc extends RefField {
public static get ActiveTool(): InkTool {
return StrCast(Doc.UserDoc().activeTool, InkTool.None) as InkTool;
}
- public static get ActivePresentation() {
- return DocCast(Doc.UserDoc().activePresentation);
+ public static get ActivePresentation(): Opt<Doc> {
+ return DocCast(Doc.ActiveDashboard?.activePresentation);
}
public static set ActivePresentation(val) {
- Doc.UserDoc().activePresentation = new PrefetchProxy(val);
+ if (Doc.ActiveDashboard) {
+ Doc.ActiveDashboard.activePresentation = val;
+ }
}
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
@@ -1176,7 +1205,7 @@ export namespace Doc {
}
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
- if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return DocBrushStatus.unbrushed;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return DocBrushStatus.unbrushed;
const status = brushManager.BrushedDoc.has(doc) ? DocBrushStatus.selfBrushed : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? DocBrushStatus.protoBrushed : DocBrushStatus.unbrushed;
if (status === DocBrushStatus.unbrushed) {
const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement();
@@ -1238,7 +1267,7 @@ export namespace Doc {
}
const highlightManager = new HighlightBrush();
export function IsHighlighted(doc: Doc) {
- if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate) return false;
+ if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
@@ -1373,11 +1402,17 @@ export namespace Doc {
layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
layoutDoc._nativeWidth = undefined;
layoutDoc._nativeHeight = undefined;
+ layoutDoc._forceReflow = undefined;
+ layoutDoc._nativeHeightUnfrozen = undefined;
+ layoutDoc._nativeDimModifiable = undefined;
} else {
layoutDoc._autoHeight = false;
if (!Doc.NativeWidth(layoutDoc)) {
layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
+ layoutDoc._forceReflow = true;
+ layoutDoc._nativeHeightUnfrozen = true;
+ layoutDoc._nativeDimModifiable = true;
}
}
});
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 1321bc327..0b51db70b 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -115,7 +115,7 @@ export class SchemaHeaderField extends ObjectField {
}
[ToScriptString]() {
- return `header(${this.heading},${this.type}})`;
+ return `header(${this.heading},${this.type},${this.width}})`;
}
[ToString]() {
return `SchemaHeaderField`;
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 48d5c5563..4896c027d 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -9,7 +9,7 @@ import { Doc, Field, Opt } from './Doc';
import { Copy, Id, ToScriptString, ToString } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
-import { Cast, NumCast, StrCast } from './Types';
+import { Cast, StrCast } from './Types';
import { Plugins } from './util';
function optional(propSchema: PropSchema) {
@@ -43,12 +43,11 @@ const scriptSchema = createSimpleSchema({
originalScript: true,
});
-function finalizeScript(script: ScriptField, captures: boolean) {
+function finalizeScript(script: ScriptField) {
const comp = CompileScript(script.script.originalScript, script.script.options);
if (!comp.compiled) {
throw new Error("Couldn't compile loaded script");
}
- !captures && ScriptField._scriptFieldCache.set(script.script.originalScript, comp);
if (script.setterscript) {
const compset = CompileScript(script.setterscript?.originalScript, script.setterscript.options);
if (!compset.compiled) {
@@ -72,9 +71,9 @@ async function deserializeScript(script: ScriptField) {
else if (!isNaN(Number(val))) captured[key] = Number(val);
else captured[key] = val;
})
- ).then(() => ((script as any).script = finalizeScript(script, true)));
+ ).then(() => ((script as any).script = finalizeScript(script)));
} else {
- (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script, false);
+ (script as any).script = ScriptField.GetScriptFieldCache(script.script.originalScript) ?? finalizeScript(script);
}
}
@@ -105,7 +104,7 @@ export class ScriptField extends ObjectField {
}
this.rawscript = rawscript;
this.setterscript = setterscript;
- this.script = script ?? (CompileScript('false') as CompiledScript);
+ this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
}
// init(callback: (res: Field) => any) {
@@ -143,7 +142,7 @@ export class ScriptField extends ObjectField {
return this.script.originalScript;
}
public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
- const compiled = CompileScript(script, {
+ return CompileScript(script, {
params: {
this: Doc?.name || 'Doc', // this is the doc that executes the script
self: Doc?.name || 'Doc', // self is the root doc of the doc that executes the script
@@ -156,7 +155,6 @@ export class ScriptField extends ObjectField {
addReturn: addReturn,
capturedVariables,
});
- return compiled;
}
public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
@@ -189,13 +187,13 @@ export class ComputedField extends ScriptField {
const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ComputedField(compiled) : undefined;
}
- public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+ public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
if (!doc[`${fieldKey}-indexed`]) {
const flist = new List<number>(numberRange(curTimecode + 1).map(i => undefined) as any as number[]);
- flist[curTimecode] = NumCast(doc[fieldKey]);
+ flist[curTimecode] = Cast(doc[fieldKey], 'number', null);
doc[`${fieldKey}-indexed`] = flist;
}
- const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
+ const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, ${defaultVal})`, {}, true, {});
const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
}
@@ -260,8 +258,8 @@ ScriptingGlobals.add(
);
ScriptingGlobals.add(
- function getIndexVal(list: any[], index: number) {
- return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), undefined as any);
+ function getIndexVal(list: any[], index: number, defaultVal: Opt<number> = undefined) {
+ return list?.reduce((p, x, i) => ((i <= index && x !== undefined) || p === undefined ? x : p), defaultVal);
},
'returns the value at a given index of a list',
'(list: any[], index: number)'
diff --git a/src/fields/util.ts b/src/fields/util.ts
index b3cbbe241..4a62a6a1f 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,8 +1,10 @@
import { action, observable, runInAction, trace } from 'mobx';
import { computedFn } from 'mobx-utils';
import { DocServer } from '../client/DocServer';
+import { CollectionViewType } from '../client/documents/DocumentTypes';
import { SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
+import { CollectionDockingView } from '../client/views/collections/CollectionDockingView';
import { returnZero } from '../Utils';
import CursorField from './CursorField';
import {
@@ -278,6 +280,15 @@ function getEffectiveAcl(target: any, user?: string): symbol {
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
if (!visited) visited = [] as Doc[];
if (visited.includes(target)) return;
+
+ if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
+ target[key] = acl;
+ if (target !== Doc.GetProto(target)) {
+ //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???)
+ updateCachedAcls(target);
+ }
+ return;
+ }
visited.push(target);
const HierarchyMapping = new Map<string, number>([
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index f19496d25..8265de445 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -832,7 +832,7 @@ export class MobileInterface extends React.Component {
</div>
{this.switchMenuView}
{this.inkMenu}
- <GestureOverlay>
+ <GestureOverlay isActive={true}>
<div style={{ display: 'none' }}>
<RichTextMenu key="rich" />
</div>
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 787e331c5..fe4c475c9 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -60,6 +60,18 @@ export default class UploadManager extends ApiManager {
return new Promise<void>(resolve => {
form.parse(req, async (_err, _fields, files) => {
const results: Upload.FileResponse[] = [];
+ if (_err?.message) {
+ results.push({
+ source: {
+ size: 0,
+ path: 'none',
+ name: 'none',
+ type: 'none',
+ toJSON: () => ({ name: 'none', path: '' }),
+ },
+ result: { name: 'failed upload', message: `${_err.message}` },
+ });
+ }
for (const key in files) {
const f = files[key];
if (!Array.isArray(f)) {
@@ -86,7 +98,7 @@ export default class UploadManager extends ApiManager {
const videoId = JSON.parse(payload).videoId;
const results: Upload.FileResponse[] = [];
const result = await DashUploadUtils.uploadYoutube(videoId);
- result && !(result.result instanceof Error) && results.push(result);
+ result && results.push(result);
_success(res, results);
resolve();
});
@@ -96,6 +108,21 @@ export default class UploadManager extends ApiManager {
register({
method: Method.POST,
+ subscription: '/queryYoutubeProgress',
+ secureHandler: async ({ req, res }) => {
+ return new Promise<void>(async resolve => {
+ req.addListener('data', args => {
+ const payload = String.fromCharCode.apply(String, args);
+ const videoId = JSON.parse(payload).videoId;
+ _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId) });
+ resolve();
+ });
+ });
+ },
+ });
+
+ register({
+ method: Method.POST,
subscription: new RouteSubscriber('youtubeScreenshot'),
secureHandler: async ({ req, res }) => {
const { id, timecode } = req.body;
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index cae35da60..4870d218b 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -2,35 +2,35 @@ import { green, red } from 'colors';
import { ExifImage } from 'exif';
import * as exifr from 'exifr';
import { File } from 'formidable';
-import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
+import { createReadStream, createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
import * as path from 'path';
-import { basename } from "path";
+import { basename } from 'path';
import * as sharp from 'sharp';
import { Stream } from 'stream';
import { filesDirectory, publicDirectory } from '.';
import { Opt } from '../fields/Doc';
-import { ParsedPDF } from "../server/PdfTypes";
+import { ParsedPDF } from '../server/PdfTypes';
import { Utils } from '../Utils';
import { createIfNotExists } from './ActionUtilities';
import { clientPathToFile, Directory, pathToDirectory, serverPathToFile } from './ApiManagers/UploadManager';
-import { resolvedServerUrl } from "./server_Initialization";
+import { resolvedServerUrl } from './server_Initialization';
import { AcceptableMedia, Upload } from './SharedMediaTypes';
import request = require('request-promise');
import formidable = require('formidable');
-import { file } from 'jszip';
-import { csvParser } from './DataVizUtils';
-const { exec } = require("child_process");
+const spawn = require('child_process').spawn;
+const { exec } = require('child_process');
const parse = require('pdf-parse');
-const ffmpeg = require("fluent-ffmpeg");
-const fs = require("fs");
-const requestImageSize = require("../client/util/request-image-size");
+const ffmpeg = require('fluent-ffmpeg');
+const fs = require('fs');
+const requestImageSize = require('../client/util/request-image-size');
+const md5File = require('md5-file');
export enum SizeSuffix {
- Small = "_s",
- Medium = "_m",
- Large = "_l",
- Original = "_o",
- None = ""
+ Small = '_s',
+ Medium = '_m',
+ Large = '_l',
+ Original = '_o',
+ None = '',
}
export function InjectSize(filename: string, size: SizeSuffix) {
@@ -43,7 +43,6 @@ function isLocal() {
}
export namespace DashUploadUtils {
-
export interface Size {
width: number;
suffix: SizeSuffix;
@@ -59,19 +58,19 @@ export namespace DashUploadUtils {
return AcceptableMedia.imageFormats.includes(path.extname(url).toLowerCase());
}
- const size = "content-length";
- const type = "content-type";
+ const size = 'content-length';
+ const type = 'content-type';
+
+ const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
- const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
-
export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
// make a list of paths to create the ordered text file for ffmpeg
const inputListName = 'concat.txt';
const textFilePath = path.join(filesDirectory, inputListName);
// make a list of paths to create the ordered text file for ffmpeg
- const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
+ const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
// write the text file to the file system
- writeFile(textFilePath, filePathsText, (err) => console.log(err));
+ writeFile(textFilePath, filePathsText, err => console.log(err));
// make output file name based on timestamp
const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
@@ -81,117 +80,214 @@ export namespace DashUploadUtils {
// concatenate the videos
await new Promise((resolve, reject) => {
var merge = ffmpeg();
- merge.input(textFilePath)
- .inputOptions(['-f concat', '-safe 0'])
+ merge
+ .input(textFilePath)
+ .inputOptions(['-f concat', '-safe 0'])
.outputOptions('-c copy')
//.videoCodec("copy")
.save(outputFilePath)
- .on("error", reject)
- .on("end", resolve);
- })
-
- // delete concat.txt from the file system
- unlinkSync(textFilePath);
- // delete the old segment videos from the server
- filePaths.forEach(filePath => unlinkSync(filePath));
-
- // return the path(s) to the output file
- return {
- accessPaths: getAccessPaths(Directory.videos, outputFileName)
- }
+ .on('error', reject)
+ .on('end', resolve);
+ });
+
+ // delete concat.txt from the file system
+ unlinkSync(textFilePath);
+ // delete the old segment videos from the server
+ filePaths.forEach(filePath => unlinkSync(filePath));
+
+ // return the path(s) to the output file
+ return {
+ accessPaths: getAccessPaths(Directory.videos, outputFileName),
+ };
+ }
+
+ function resolveExistingFile(name: string, pat: string, directory: Directory, type?: string, duration?: number, rawText?: string) {
+ const data = { size: 0, path: path.basename(pat), name, type: type ?? '' };
+ const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration?.toString(), mime: '', toJson: () => undefined as any }) };
+ return {
+ source: file,
+ result: {
+ accessPaths: {
+ agnostic: getAccessPaths(directory, data.path),
+ },
+ rawText,
+ duration,
+ },
+ };
}
+ export function QueryYoutubeProgress(videoId: string) {
+ return uploadProgress.get(videoId) ?? 'failed';
+ }
+
+ let uploadProgress = new Map<string, string>();
+
export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
- console.log("UPLOAD " + videoId);
return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
- exec('youtube-dl -o ' + (videoId + ".mp4") + ' https://www.youtube.com/watch?v=' + videoId + ' -f "best[filesize<50M]"',
- (error: any, stdout: any, stderr: any) => {
- if (error) console.log(`error: ${error.message}`);
- else if (stderr) console.log(`stderr: ${stderr}`);
- else {
- console.log(`stdout: ${stdout}`);
- const data = { size: 0, path: videoId + ".mp4", name: videoId, type: "video/mp4" };
- const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ""), mtime: null, length: 0, mime: "", toJson: () => undefined as any }) };
- res(MoveParsedFile(file, Directory.videos));
+ console.log('Uploading YouTube video: ' + videoId);
+ const name = videoId;
+ const path = name.replace(/^-/, '__') + '.mp4';
+ const finalPath = serverPathToFile(Directory.videos, path);
+ if (existsSync(finalPath)) {
+ uploadProgress.set(videoId, 'computing duration');
+ exec(`yt-dlp -o ${finalPath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
+ const time = Array.from(stdout.trim().split(':')).reverse();
+ const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+ res(resolveExistingFile(name, finalPath, Directory.videos, 'video/mp4', duration, undefined));
+ });
+ } else {
+ uploadProgress.set(videoId, 'starting download');
+ const ytdlp = spawn(`yt-dlp`, ['-o', path, `https://www.youtube.com/watch?v=${videoId}`, '--max-filesize', '100M', '-f', 'mp4']);
+
+ ytdlp.stdout.on('data', (data: any) => !uploadProgress.get(videoId)?.includes('Aborting.') && uploadProgress.set(videoId, data.toString()));
+
+ let errors = '';
+ ytdlp.stderr.on('data', (data: any) => (errors = data.toString()));
+
+ ytdlp.on('exit', function (code: any) {
+ if (code || uploadProgress.get(videoId)?.includes('Aborting.')) {
+ res({
+ source: {
+ size: 0,
+ path,
+ name,
+ type: '',
+ toJSON: () => ({ name, path }),
+ },
+ result: { name: 'failed youtube query', message: `Could not archive video. ${code ? errors : uploadProgress.get(videoId)}` },
+ });
+ } else {
+ uploadProgress.set(videoId, 'computing duration');
+ exec(`yt-dlp-o ${path} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
+ const time = Array.from(stdout.trim().split(':')).reverse();
+ const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
+ const data = { size: 0, path, name, type: 'video/mp4' };
+ const file = { ...data, toJSON: () => ({ ...data, filename: data.path.replace(/.*\//, ''), mtime: duration.toString(), mime: '', toJson: () => undefined as any }) };
+ res(MoveParsedFile(file, Directory.videos));
+ });
}
});
+ }
});
}
export async function upload(file: File): Promise<Upload.FileResponse> {
const { type, path, name } = file;
- const types = type?.split("/") ?? [];
+ const types = type?.split('/') ?? [];
const category = types[0];
let format = `.${types[1]}`;
console.log(green(`Processing upload of file (${name}) and format (${format}) with upload type (${type}) in category (${category}).`));
-
+
switch (category) {
- case "image":
+ case 'image':
if (imageFormats.includes(format)) {
const result = await UploadImage(path, basename(path));
return { source: file, result };
}
- case "video":
- if (format.includes("x-matroska")) {
- console.log("case video");
- await new Promise(res => ffmpeg(file.path)
- .videoCodec("copy") // this will copy the data instead of reencode it
- .save(file.path.replace(".mkv", ".mp4"))
- .on('end', res));
- file.path = file.path.replace(".mkv", ".mp4");
- format = ".mp4";
+ fs.unlink(path, () => {});
+ return { source: file, result: { name: 'Unsupported image format', message: `Could not upload unsupported file (${name}). Please convert to an .jpg` } };
+ case 'video':
+ if (format.includes('x-matroska')) {
+ console.log('case video');
+ await new Promise(res =>
+ ffmpeg(file.path)
+ .videoCodec('copy') // this will copy the data instead of reencode it
+ .save(file.path.replace('.mkv', '.mp4'))
+ .on('end', res)
+ );
+ file.path = file.path.replace('.mkv', '.mp4');
+ format = '.mp4';
+ }
+ if (format.includes('quicktime')) {
+ let abort = false;
+ await new Promise<void>(res =>
+ ffmpeg.ffprobe(file.path, (err: any, metadata: any) => {
+ if (metadata.streams.some((stream: any) => stream.codec_name === 'hevc')) {
+ abort = true;
+ }
+ res();
+ })
+ );
+ if (abort) {
+ // bcz: instead of aborting, we could convert the file using the code below to an mp4. Problem is that this takes a long time and will clog up the server.
+ // await new Promise(res =>
+ // ffmpeg(file.path)
+ // .videoCodec('libx264') // this will copy the data instead of reencode it
+ // .audioCodec('mp2')
+ // .save(file.path.replace('.MOV', '.mp4').replace('.mov', '.mp4'))
+ // .on('end', res)
+ // );
+ // file.path = file.path.replace('.mov', '.mp4').replace('.MOV', '.mp4');
+ // format = '.mp4';
+ fs.unlink(path, () => {});
+ return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
+ }
}
if (videoFormats.includes(format)) {
return MoveParsedFile(file, Directory.videos);
}
- case "application":
+ fs.unlink(path, () => {});
+ return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
+ case 'application':
if (applicationFormats.includes(format)) {
- return UploadPdf(file);
+ const val = UploadPdf(file);
+ if (val) return val;
}
- case "audio":
- const components = format.split(";");
+ case 'audio':
+ const components = format.split(';');
if (components.length > 1) {
format = components[0];
}
if (audioFormats.includes(format)) {
return UploadAudio(file, format);
}
- case "text":
- if (types[1] == "csv") {
+ fs.unlink(path, () => {});
+ return { source: file, result: { name: 'Unsupported audio format', message: `Could not upload unsupported file (${name}). Please convert to an .mp3` } };
+ case 'text':
+ if (types[1] == 'csv') {
return UploadCsv(file);
}
-
}
console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
+ fs.unlink(path, () => {});
return { source: file, result: new Error(`Could not upload unsupported file (${name}) with upload type (${type}).`) };
}
async function UploadPdf(file: File) {
- const { path: sourcePath } = file;
- const dataBuffer = readFileSync(sourcePath);
- const result: ParsedPDF = await parse(dataBuffer);
- await new Promise<void>((resolve, reject) => {
- const name = path.basename(sourcePath);
- const textFilename = `${name.substring(0, name.length - 4)}.txt`;
- const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
- writeStream.write(result.text, error => error ? reject(error) : resolve());
- });
- return MoveParsedFile(file, Directory.pdfs, undefined, result.text);
+ const fileKey = (await md5File(file.path)) + '.pdf';
+ const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
+ if (fExists(fileKey, Directory.pdfs) && fExists(textFilename, Directory.text)) {
+ return new Promise<Upload.FileResponse>(res => {
+ const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
+ const readStream = createReadStream(serverPathToFile(Directory.text, textFilename));
+ var rawText = '';
+ readStream.on('data', chunk => (rawText += chunk.toString())).on('end', () => res(resolveExistingFile(file.name, fileKey, Directory.pdfs, file.type, undefined, rawText)));
+ });
+ }
+ const dataBuffer = readFileSync(file.path);
+ const result: ParsedPDF | any = await parse(dataBuffer).catch((e: any) => e);
+ if (!result.code) {
+ await new Promise<void>((resolve, reject) => {
+ const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
+ writeStream.write(result?.text, error => (error ? reject(error) : resolve()));
+ });
+ return MoveParsedFile(file, Directory.pdfs, undefined, result?.text, undefined, fileKey);
+ }
+ return { source: file, result: { name: 'faile pdf pupload', message: `Could not upload (${file.name}).${result.message}` } };
}
async function UploadCsv(file: File) {
- const { path: sourcePath } = file;
- // read the file as a string
+ const { path: sourcePath } = file;
+ // read the file as a string
const data = readFileSync(sourcePath, 'utf8');
// split the string into an array of lines
return MoveParsedFile(file, Directory.csv, undefined, data);
// console.log(csvParser(data));
-
}
- const manualSuffixes = [".webm"];
+ const manualSuffixes = ['.webm'];
async function UploadAudio(file: File, format: string) {
const suffix = manualSuffixes.includes(format) ? format : undefined;
@@ -200,37 +296,37 @@ export namespace DashUploadUtils {
/**
* Uploads an image specified by the @param source to Dash's /public/files/
- * directory, and returns information generated during that upload
- *
+ * directory, and returns information generated during that upload
+ *
* @param {string} source is either the absolute path of an already uploaded image or
* the url of a remote image
* @param {string} filename dictates what to call the image. If not specified,
* the name {@param prefix}_upload_{GUID}
* @param {string} prefix is a string prepended to the generated image name in the
* event that @param filename is not specified
- *
+ *
* @returns {ImageUploadInformation | Error} This method returns
* 1) the paths to the uploaded images (plural due to resizing)
* 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
* 3) the size of the image, in bytes (4432130)
* 4) the content type of the image, i.e. image/(jpeg | png | ...)
*/
- export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => {
+ export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => {
const metadata = await InspectImage(source);
if (metadata instanceof Error) {
- return metadata;
+ return { name: metadata.name, message: metadata.message };
}
return UploadInspectedImage(metadata, filename || metadata.filename, prefix);
};
export async function buildFileDirectories() {
if (!existsSync(publicDirectory)) {
- console.error("\nPlease ensure that the following directory exists...\n");
+ console.error('\nPlease ensure that the following directory exists...\n');
console.log(publicDirectory);
process.exit(0);
}
if (!existsSync(filesDirectory)) {
- console.error("\nPlease ensure that the following directory exists...\n");
+ console.error('\nPlease ensure that the following directory exists...\n');
console.log(filesDirectory);
process.exit(0);
}
@@ -252,7 +348,7 @@ export namespace DashUploadUtils {
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
- *
+ *
* @param source is the path or url to the image in question
*/
export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => {
@@ -265,9 +361,9 @@ export namespace DashUploadUtils {
*/
if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) {
const [ext, data] = rawMatches.slice(1, 3);
- const resolved = filename = `upload_${Utils.GenerateGuid()}.${ext}`;
+ const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`);
const error = await new Promise<Error | null>(resolve => {
- writeFile(serverPathToFile(Directory.images, resolved), data, "base64", resolve);
+ writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve);
});
if (error !== null) {
return error;
@@ -276,12 +372,12 @@ export namespace DashUploadUtils {
}
let resolvedUrl: string;
/**
- *
+ *
* At this point, we want to take whatever url we have and make sure it's requestable.
* Anything that's hosted by some other website already is, but if the url is a local file url
* (locates the file on this server machine), we have to resolve the client side url by cutting out the
* basename subtree (i.e. /images/<some_guid>.<ext>) and put it on the end of the server's url.
- *
+ *
* This can always be localhost, regardless of whether this is on the server or not, since we (the server, not the client)
* will be the ones making the request, and from the perspective of dash-release or dash-web, localhost:<port> refers to the same thing
* as the full dash-release.eastus.cloudapp.azure.com:<port>.
@@ -290,18 +386,20 @@ export namespace DashUploadUtils {
if (matches === null) {
resolvedUrl = source;
} else {
- resolvedUrl = `${resolvedServerUrl}/${matches[1].split("\\").join("/")}`;
+ resolvedUrl = `${resolvedServerUrl}/${matches[1].split('\\').join('/')}`;
}
// See header comments: not all image files have exif data (I believe only JPG is the only format that can have it)
const exifData = await parseExifData(resolvedUrl);
const results = {
exifData,
- requestable: resolvedUrl
+ requestable: resolvedUrl,
};
+
// Use the request library to parse out file level image information in the headers
- const { headers } = (await new Promise<any>((resolve, reject) => {
- request.head(resolvedUrl, (error, res) => error ? reject(error) : resolve(res));
- }).catch(console.error));
+ const { headers } = await new Promise<any>((resolve, reject) => {
+ return request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res)));
+ }).catch(e => console.log(e));
+
try {
// Compute the native width and height ofthe image with an npm module
const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl);
@@ -313,7 +411,7 @@ export namespace DashUploadUtils {
nativeWidth,
nativeHeight,
filename,
- ...results
+ ...results,
};
} catch (e: any) {
console.log(e);
@@ -331,42 +429,50 @@ export namespace DashUploadUtils {
* @param suffix If the file doesn't have a suffix and you want to provide it one
* to appear in the new location
*/
- export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string): Promise<Upload.FileResponse> {
+ export async function MoveParsedFile(file: formidable.File, destination: Directory, suffix: string | undefined = undefined, text?: string, duration?: number, targetName?: string): Promise<Upload.FileResponse> {
const { path: sourcePath } = file;
- let name = path.basename(sourcePath);
+ let name = targetName ?? path.basename(sourcePath);
suffix && (name += suffix);
return new Promise(resolve => {
const destinationPath = serverPathToFile(destination, name);
rename(sourcePath, destinationPath, error => {
resolve({
source: file,
- result: error ? error : {
- accessPaths: {
- agnostic: getAccessPaths(destination, name)
- },
- rawText: text
- }
+ result: error
+ ? error
+ : {
+ accessPaths: {
+ agnostic: getAccessPaths(destination, name),
+ },
+ rawText: text,
+ duration,
+ },
});
});
});
}
+ export function fExists(name: string, destination: Directory) {
+ const destinationPath = serverPathToFile(destination, name);
+ return existsSync(destinationPath);
+ }
+
export function getAccessPaths(directory: Directory, fileName: string) {
return {
client: clientPathToFile(directory, fileName),
- server: serverPathToFile(directory, fileName)
+ server: serverPathToFile(directory, fileName),
};
}
- export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => {
+ export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
- const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`;
+ const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
const { images } = Directory;
const information: Upload.ImageInformation = {
accessPaths: {
- agnostic: getAccessPaths(images, resolved)
+ agnostic: getAccessPaths(images, resolved),
},
- ...metadata
+ ...metadata,
};
const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
for (const suffix of Object.keys(writtenFiles)) {
@@ -383,9 +489,9 @@ export namespace DashUploadUtils {
const val: any = layer[key];
if (val instanceof Buffer) {
layer[key] = val.toString();
- } else if (Array.isArray(val) && typeof val[0] === "number") {
+ } else if (Array.isArray(val) && typeof val[0] === 'number') {
layer[key] = Buffer.from(val).toString();
- } else if (typeof val === "object") {
+ } else if (typeof val === 'object') {
bufferConverterRec(val);
}
}
@@ -403,27 +509,27 @@ export namespace DashUploadUtils {
});
});
//data && bufferConverterRec(data);
- return { data: await exifr.parse(image), error };
+ return error ? { data: undefined, error } : { data: await exifr.parse(image), error };
};
const { pngs, jpgs, webps, tiffs } = AcceptableMedia;
const pngOptions = {
compressionLevel: 9,
adaptiveFiltering: true,
- force: true
+ force: true,
};
export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
const writtenFiles: { [suffix: string]: string } = {};
for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
- const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix));
+ const outputPath = path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix)));
await new Promise<void>(async (resolve, reject) => {
const source = streamProvider();
let readStream: Stream = source instanceof Promise ? await source : source;
if (resizer) {
readStream = readStream.pipe(resizer.withMetadata());
}
- readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject);
+ readStream.pipe(createWriteStream(outputPath)).on('close', resolve).on('error', reject);
});
}
return writtenFiles;
@@ -442,15 +548,14 @@ export namespace DashUploadUtils {
initial = initial.webp();
} else if (tiffs.includes(ext)) {
initial = initial.tiff();
- } else if (ext === ".gif") {
+ } else if (ext === '.gif') {
initial = undefined;
}
return {
resizer: initial,
- suffix
+ suffix,
};
- })
+ }),
];
}
-
-} \ No newline at end of file
+}
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index cde95526f..7db1c2dae 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -2,36 +2,45 @@ import { ExifData } from 'exif';
import { File } from 'formidable';
export namespace AcceptableMedia {
- export const gifs = [".gif"];
- export const pngs = [".png"];
- export const jpgs = [".jpg", ".jpeg"];
- export const webps = [".webp"];
- export const tiffs = [".tiff"];
+ export const gifs = ['.gif'];
+ export const pngs = ['.png'];
+ export const jpgs = ['.jpg', '.jpeg'];
+ export const webps = ['.webp'];
+ export const tiffs = ['.tiff'];
export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs];
- export const videoFormats = [".mov", ".mp4", ".quicktime", ".mkv", ".x-matroska;codecs=avc1"];
- export const applicationFormats = [".pdf"];
- export const audioFormats = [".wav", ".mp3", ".mpeg", ".flac", ".au", ".aiff", ".m4a", ".webm"];
+ export const videoFormats = ['.mov', '.mp4', '.quicktime', '.mkv', '.x-matroska;codecs=avc1'];
+ export const applicationFormats = ['.pdf'];
+ export const audioFormats = ['.wav', '.mp3', '.mpeg', '.flac', '.au', '.aiff', '.m4a', '.webm'];
}
export namespace Upload {
-
export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
- return "nativeWidth" in uploadResponse;
+ return 'nativeWidth' in uploadResponse;
+ }
+
+ export function isVideoInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.VideoInformation {
+ return 'duration' in uploadResponse;
}
export interface FileInformation {
accessPaths: AccessPathInfo;
rawText?: string;
+ duration?: number;
}
- export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error };
+ export type FileResponse<T extends FileInformation = FileInformation> = { source: File; result: T | Error };
export type ImageInformation = FileInformation & InspectionResults;
+ export type VideoInformation = FileInformation & VideoResults;
+
export interface AccessPathInfo {
- [suffix: string]: { client: string, server: string };
+ [suffix: string]: { client: string; server: string };
}
+ export interface VideoResults {
+ duration: number;
+ }
export interface InspectionResults {
source: string;
requestable: string;
@@ -44,8 +53,7 @@ export namespace Upload {
}
export interface EnrichedExifData {
- data: ExifData & ExifData["gps"];
+ data: ExifData & ExifData['gps'];
error?: string;
}
-
-} \ No newline at end of file
+}