aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts14
-rw-r--r--src/client/Network.ts75
-rw-r--r--src/client/documents/Documents.ts83
-rw-r--r--src/client/util/CurrentUserUtils.ts42
-rw-r--r--src/client/util/DocumentManager.ts51
-rw-r--r--src/client/util/DragManager.ts6
-rw-r--r--src/client/util/LinkManager.ts21
-rw-r--r--src/client/views/DocComponent.tsx4
-rw-r--r--src/client/views/DocumentButtonBar.tsx46
-rw-r--r--src/client/views/DocumentDecorations.scss498
-rw-r--r--src/client/views/DocumentDecorations.tsx65
-rw-r--r--src/client/views/GlobalKeyHandler.ts17
-rw-r--r--src/client/views/LightboxView.tsx17
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/MarqueeAnnotator.tsx27
-rw-r--r--src/client/views/OverlayView.scss70
-rw-r--r--src/client/views/OverlayView.tsx387
-rw-r--r--src/client/views/PropertiesDocBacklinksSelector.tsx4
-rw-r--r--src/client/views/PropertiesDocContextSelector.tsx1
-rw-r--r--src/client/views/PropertiesView.tsx6
-rw-r--r--src/client/views/SidebarAnnos.tsx4
-rw-r--r--src/client/views/StyleProvider.scss2
-rw-r--r--src/client/views/StyleProvider.tsx51
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx11
-rw-r--r--src/client/views/collections/CollectionPileView.tsx18
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx6
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx9
-rw-r--r--src/client/views/collections/CollectionTreeView.scss11
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx5
-rw-r--r--src/client/views/collections/TabDocView.tsx10
-rw-r--r--src/client/views/collections/TreeView.scss36
-rw-r--r--src/client/views/collections/TreeView.tsx159
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx30
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx95
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx13
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss4
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx5
-rw-r--r--src/client/views/linking/LinkMenu.tsx18
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx6
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx10
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx1
-rw-r--r--src/client/views/nodes/DocumentLinksButton.scss2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx27
-rw-r--r--src/client/views/nodes/DocumentView.scss3
-rw-r--r--src/client/views/nodes/DocumentView.tsx2480
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/ImageBox.tsx63
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx2
-rw-r--r--src/client/views/nodes/LabelBigText.js34
-rw-r--r--src/client/views/nodes/LabelBox.scss2
-rw-r--r--src/client/views/nodes/LabelBox.tsx27
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx14
-rw-r--r--src/client/views/nodes/PDFBox.tsx67
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx52
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.scss5
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx155
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx429
-rw-r--r--src/client/views/nodes/VideoBox.scss1
-rw-r--r--src/client/views/nodes/VideoBox.tsx25
-rw-r--r--src/client/views/nodes/WebBox.tsx10
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx44
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts2
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx21
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss2
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx707
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx16
-rw-r--r--src/client/views/pdf/PDFViewer.tsx14
-rw-r--r--src/client/views/search/SearchBox.tsx2
-rw-r--r--src/client/views/topbar/TopBar.tsx26
-rw-r--r--src/fields/Doc.ts11
-rw-r--r--src/server/ApiManagers/UploadManager.ts546
-rw-r--r--src/server/DashSession/Session/agents/applied_session_agent.ts4
-rw-r--r--src/server/database.ts8
78 files changed, 3624 insertions, 3138 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index b280badd7..205f9379e 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -204,11 +204,11 @@ export namespace Utils {
return { h: h, s: s, l: l };
}
- export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number) {
- if (scrollTop + contextHgt < targetY + minSpacing + targetHgt) {
+ export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
+ if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
}
- if (scrollTop > targetY - minSpacing - targetHgt) {
+ if (scrollTop > Math.max(0, targetY - minSpacing - targetHgt)) {
return Math.max(0, Math.floor(targetY - minSpacing - targetHgt));
}
}
@@ -399,6 +399,12 @@ export function timenow() {
return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
}
+export function incrementTitleCopy(title: string) {
+ const numstr = title.match(/.*(\{([0-9]*)\})+/);
+ const copyNumStr = `{${1 + (numstr ? (+numstr[2]) : 0)}}`;
+ return (numstr ? title.replace(numstr[1], "") : title) + copyNumStr;
+}
+
export function formatTime(time: number) {
time = Math.round(time);
const hours = Math.floor(time / 60 / 60);
@@ -603,6 +609,8 @@ export function DashColor(color: string) {
}
export function lightOrDark(color: any) {
+ if (color === "transparent") return "gray";
+ if (color.startsWith?.("linear")) return "black";
const nonAlphaColor = color.startsWith("#") ? (color as string).substring(0, 7) :
color.startsWith("rgba") ? color.replace(/,.[^,]*\)/, ")").replace("rgba", "rgb") : color;
const col = DashColor(nonAlphaColor).rgb();
diff --git a/src/client/Network.ts b/src/client/Network.ts
index bf2918734..1255e5ce0 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -4,46 +4,47 @@ import { Upload } from "../server/SharedMediaTypes";
export namespace Networking {
- export async function FetchFromServer(relativeRoute: string) {
- return (await fetch(relativeRoute)).text();
- }
+ export async function FetchFromServer(relativeRoute: string) {
+ return (await fetch(relativeRoute)).text();
+ }
- export async function PostToServer(relativeRoute: string, body?: any) {
- const options = {
- uri: Utils.prepend(relativeRoute),
- method: "POST",
- body,
- json: true
- };
- return requestPromise.post(options);
- }
+ export async function PostToServer(relativeRoute: string, body?: any) {
+ const options = {
+ uri: Utils.prepend(relativeRoute),
+ method: "POST",
+ body,
+ json: true
+ };
+ return requestPromise.post(options);
+ }
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
- const formData = new FormData();
- if (Array.isArray(files)) {
- if (!files.length) {
- return [];
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+ console.log(files)
+ const formData = new FormData();
+ if (Array.isArray(files)) {
+ if (!files.length) {
+ return [];
+ }
+ files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+ } else {
+ formData.append(Utils.GenerateGuid(), files);
}
- files.forEach(file => formData.append(Utils.GenerateGuid(), file));
- } else {
- formData.append(Utils.GenerateGuid(), files);
- }
- const parameters = {
- method: 'POST',
- body: formData
- };
- const response = await fetch("/uploadFormData", parameters);
- return response.json();
- }
+ const parameters = {
+ method: 'POST',
+ body: formData
+ };
+ const response = await fetch("/uploadFormData", parameters);
+ return response.json();
+ }
- export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
- const parameters = {
- method: 'POST',
- body: JSON.stringify({ videoId }),
- json: true
- };
- const response = await fetch("/uploadYoutubeVideo", parameters);
- return response.json();
- }
+ export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+ const parameters = {
+ method: 'POST',
+ body: JSON.stringify({ videoId }),
+ json: true
+ };
+ const response = await fetch("/uploadYoutubeVideo", parameters);
+ return response.json();
+ }
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e50172af5..8f92e9313 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -14,7 +14,7 @@ import { Cast, NumCast, StrCast } from "../../fields/Types";
import { AudioField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Upload } from "../../server/SharedMediaTypes";
-import { OmitKeys, Utils } from "../../Utils";
+import { OmitKeys, Utils, aggregateBounds } from "../../Utils";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { DocServer } from "../DocServer";
import { Networking } from "../Network";
@@ -118,6 +118,7 @@ export class DocumentOptions {
_lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
_lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
_freeformLOD?: boolean; // whether to use LOD to render a freeform document
+ _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target
_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
_scrollTop?: number; // scroll location for pdfs
@@ -140,6 +141,8 @@ export class DocumentOptions {
_itemIndex?: number; // which item index the carousel viewer is showing
_showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
_singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document)
+ _minFontSize?: number; // minimum font size for labelBoxes
+ _maxFontSize?: number; // maximum font size for labelBoxes
_columnWidth?: number;
_columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden
_fontSize?: string;
@@ -222,6 +225,7 @@ export class DocumentOptions {
dockingConfig?: string;
annotationOn?: Doc;
isPushpin?: boolean;
+ isGroup?: boolean; // whether a collection should use a grouping UI behavior
_removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
// BACKGROUND GRID
@@ -279,8 +283,10 @@ export class DocumentOptions {
strokeWidth?: number;
freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection
treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view
+ treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
+ treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically
// Action Button
buttonMenu?: boolean; // whether a action button should be displayed
@@ -362,7 +368,7 @@ export namespace Docs {
[DocumentType.RTF, {
layout: { view: FormattedTextBox, dataField: "text" },
options: {
- _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true,
+ _height: 150, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true,
links: "@links(self)"
}
}],
@@ -446,7 +452,7 @@ export namespace Docs {
}],
[DocumentType.LABEL, {
layout: { view: LabelBox, dataField: defaultDataKey },
- options: { links: "@links(self)" }
+ options: { links: "@links(self)", _singleLine: true }
}],
[DocumentType.EQUATION, {
layout: { view: EquationBox, dataField: defaultDataKey },
@@ -461,8 +467,8 @@ export namespace Docs {
options: { links: "@links(self)" }
}],
[DocumentType.SLIDER, {
- layout: { view: SliderBox, dataField: defaultDataKey },
- options: { links: "@links(self)" }
+ layout: { view: SliderBox, dataField: defaultDataKey, },
+ options: { links: "@links(self)", treeViewGrowsHorizontally: true }
}],
[DocumentType.PRES, {
layout: { view: PresBox, dataField: defaultDataKey },
@@ -774,7 +780,7 @@ export namespace Docs {
I.data = new InkField(points);
I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
I["acl-Override"] = "None";
- I.links = "@links(self)";
+ I.links = ComputedField.MakeFunction("links(self)");
I[Initializing] = false;
return I;
}
@@ -814,7 +820,7 @@ export namespace Docs {
}
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Freeform }, id);
+ const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id);
documents.map(d => d.context = inst);
return inst;
}
@@ -1098,10 +1104,11 @@ export namespace DocUtils {
export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) {
broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1);
return DocUtils.ActiveRecordings.map(audio =>
- DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording link", "recording timeline"));
+ DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording annotation:linked recording", "recording timeline"));
}
export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
+ if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? "Commentary:Comments On" : "link";
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return;
if (target.doc === Doc.UserDoc()) return undefined;
@@ -1327,22 +1334,25 @@ export namespace DocUtils {
const _height = NumCast(doc._height);
const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
- let fieldTemplate: Opt<Doc>;
- if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
- fieldTemplate = Docs.Create.TextDocument("", options);
- } else if (doc.data instanceof PdfField) {
- fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
- } else if (doc.data instanceof VideoField) {
- fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
- } else if (doc.data instanceof AudioField) {
- fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
- } else if (doc.data instanceof ImageField) {
- fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
- }
- const docTemplate = docLayoutTemplate || creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
-
- fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
- docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+ if (docLayoutTemplate) {
+ Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined);
+ } else {
+ let fieldTemplate: Opt<Doc>;
+ if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
+ fieldTemplate = Docs.Create.TextDocument("", options);
+ } else if (doc.data instanceof PdfField) {
+ fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+ } else if (doc.data instanceof VideoField) {
+ fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+ } else if (doc.data instanceof AudioField) {
+ fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+ } else if (doc.data instanceof ImageField) {
+ fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+ }
+ const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+ fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate);
+ docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
+ }
}
export function makeCustomView(doc: Doc, custom: boolean, layout: string) {
Doc.setNativeView(doc);
@@ -1356,26 +1366,31 @@ export namespace DocUtils {
if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", "");
}
- export function pileup(docList: Doc[], x?: number, y?: number, create: boolean = true) {
+ export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
let w = 0, h = 0;
runInAction(() => {
docList.forEach(d => {
DocUtils.iconify(d);
- w = Math.max(d[WidthSym](), w);
- h = Math.max(d[HeightSym](), h);
+ w = Math.max(NumCast(d._width), w);
+ h = Math.max(NumCast(d._height), h);
+ });
+ docList.forEach((d, i) => {
+ d.x = Math.cos(Math.PI * 2 * i / docList.length) * size;
+ d.y = Math.sin(Math.PI * 2 * i / docList.length) * size;
+ d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
});
- h = Math.max(h, w * 4 / 3); // converting to an icon does not update the height right away. so this is a fallback hack to try to do something reasonable
+ const aggBounds = aggregateBounds(docList.map(d => ({ x: NumCast(d.x), y: NumCast(d.y), width: NumCast(d._width), height: NumCast(d._height) })), 0, 0);
docList.forEach((d, i) => {
- d.x = Math.cos(Math.PI * 2 * i / docList.length) * 10 - w / 2;
- d.y = Math.sin(Math.PI * 2 * i / docList.length) * 10 - h / 2;
+ d.x = NumCast(d.x) - ((aggBounds.r + aggBounds.x) / 2);
+ d.y = NumCast(d.y) - ((aggBounds.b + aggBounds.y) / 2);
d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
});
});
if (create) {
- const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: (x || 0) - 55, y: (y || 0) - 55, _width: 110, _height: 100, });
- newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - 55;
- newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - 55;
- newCollection._width = newCollection._height = 110;
+ const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, });
+ newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
+ newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
+ newCollection._width = newCollection._height = size * 2;
newCollection._jitterRotation = 10;
return newCollection;
}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index edb362a37..3165e38a6 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -9,7 +9,7 @@ import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { ComputedField, ScriptField } from "../../fields/ScriptField";
import { BoolCast, Cast, DateCast, NumCast, PromiseValue, StrCast } from "../../fields/Types";
-import { nullAudio } from "../../fields/URLField";
+import { ImageField, nullAudio } from "../../fields/URLField";
import { SharingPermissions } from "../../fields/util";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
@@ -292,7 +292,7 @@ export class CurrentUserUtils {
if (doc["template-icon-view"] === undefined) {
const iconView = Docs.Create.LabelDocument({
title: "icon", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("title"), _backgroundColor: "dimgray",
- _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
+ _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
});
// Docs.Create.TextDocument("", {
// title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" })
@@ -303,8 +303,9 @@ export class CurrentUserUtils {
}
if (doc["template-icon-view-rtf"] === undefined) {
const iconRtfView = Docs.Create.LabelDocument({
- title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"),
- _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
+ title: "icon_" + DocumentType.RTF, _showTitle: "creationDate", textTransform: "unset", letterSpacing: "unset", layout: LabelBox.LayoutString("text"),
+ _singleLine: false, _minFontSize: 18, _maxFontSize: 24,
+ _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
});
iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF);
doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView);
@@ -319,19 +320,41 @@ export class CurrentUserUtils {
}
if (doc["template-icon-view-img"] === undefined) {
const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", {
- title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
+ title: "data", _width: 150, isTemplateDoc: true, _showTitle: "title", onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true
});
iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG);
doc["template-icon-view-img"] = new PrefetchProxy(iconImageView);
}
if (doc["template-icon-view-col"] === undefined) {
- const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true });
+ const iconColView = Docs.Create.ImageDocument("", { title: "icon", _showTitle: "title", _width: 360 / 4, _height: 270 / 4, onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true });
iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL);
+ const proto = iconColView.proto as Doc;
+ proto["icon-nativeWidth"] = 360 / 4;
+ proto["icon-nativeHeight"] = 270 / 4;
+ proto.icon = new ImageField("http://www.cs.brown.edu/~bcz/noImage.png");
doc["template-icon-view-col"] = new PrefetchProxy(iconColView);
}
+ if (doc["template-icon-view-video"] === undefined) {
+ const iconVidView = Docs.Create.ImageDocument("", { title: "icon", _width: 360 / 4, _height: 270 / 4, _showTitle: "title", onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true });
+ iconVidView.isTemplateDoc = makeTemplate(iconVidView, true, "icon_" + DocumentType.VID);
+ const proto = iconVidView.proto as Doc;
+ proto["icon-nativeWidth"] = 360 / 4;
+ proto["icon-nativeHeight"] = 270 / 4;
+ proto.icon = new ImageField("http://www.cs.brown.edu/~bcz/noImage.png");
+ doc["template-icon-view-video"] = new PrefetchProxy(iconVidView);
+ }
+ if (doc["template-icon-view-pdf"] === undefined) {
+ const iconPdfView = Docs.Create.ImageDocument("", { title: "icon", _width: 360 / 4, _height: 270 / 4, _showTitle: "title", onClick: ScriptField.MakeScript("deiconifyView(documentView)", { documentView: "any" }), system: true });
+ iconPdfView.isTemplateDoc = makeTemplate(iconPdfView, true, "icon_" + DocumentType.PDF);
+ const proto = iconPdfView.proto as Doc;
+ proto["icon-nativeWidth"] = 360 / 4;
+ proto["icon-nativeHeight"] = 270 / 4;
+ proto.icon = new ImageField("http://www.cs.brown.edu/~bcz/noImage.png");
+ doc["template-icon-view-pdf"] = new PrefetchProxy(iconPdfView);
+ }
if (doc["template-icons"] === undefined) {
doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
- doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75, system: true }));
+ doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc, doc["template-icon-view-video"] as Doc, doc["template-icon-view-pdf"] as Doc], { title: "icon templates", _height: 75, system: true }));
} else {
const templateIconsDoc = Cast(doc["template-icons"], Doc, null);
const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, doc["template-icon-view-button"] as Doc,
@@ -366,7 +389,7 @@ export class CurrentUserUtils {
const textDoc = Docs.Create.TreeDocument([], {
title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true,
- backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"])
+ backgroundColor: "white", system: true, cloneFieldFilter: new List<string>(["system"])
});
Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text');
FormattedTextBox.SelectOnLoad = textDoc[Id];
@@ -1321,9 +1344,6 @@ export class CurrentUserUtils {
// undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme;
}
- if (doc.activeCollectionBackground === "white") { // temporary to avoid having to rebuild the databse for old accounts that have this set by default.
- doc.activeCollectionBackground = undefined;
- }
return doc;
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ad6d90bc3..b6c28d2fe 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -168,30 +168,43 @@ export class DocumentManager {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
const docView = getFirstDocView(targetDoc, originatingDoc);
- const wasHidden = targetDoc.hidden; //
- if (wasHidden) runInAction(() => targetDoc.hidden = false); // if the target is hidden, un-hide it here.
+ 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
+ var wasHidden = resolvedTarget.hidden;
+ if (wasHidden) {
+ runInAction(() => {
+ resolvedTarget.hidden = false; // if the target is hidden, un-hide it here.
+ docView?.props.bringToFront(resolvedTarget);
+ });
+ }
const focusAndFinish = (didFocus: boolean) => {
+ const finalTargetDoc = resolvedTarget;
if (originatingDoc?.isPushpin) {
if (!didFocus && !wasHidden) { // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
- targetDoc.hidden = !targetDoc.hidden;
+ finalTargetDoc.hidden = !finalTargetDoc.hidden;
}
} else {
- targetDoc.hidden && (targetDoc.hidden = undefined);
+ finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
!noSelect && docView?.select(false);
}
finished?.();
- return false;
};
- const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
- const annoContainerView = annotatedDoc && getFirstDocView(annotatedDoc);
+ const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : 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
const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (!docView && annoContainerView) {
- 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 ((!docView && targetDoc.type !== DocumentType.MARKER) && annoContainerView) {
+ if (annoContainerView.props.Document.layoutKey === "layout_icon") {
+ annoContainerView.iconify(() => this.jumpToDocument(
+ targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc,
+ finished, originalTarget, noSelect, presZoom));
+ return;
+ } else {
+ 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) {
!noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
@@ -207,10 +220,17 @@ export class DocumentManager {
createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
} else { // otherwise try to get a view of the context of the target
if (targetDocContextView) { // we found a context view and aren't forced to create a new one ... focus on the context first..
+ wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
+ targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
targetDocContext._viewTransition = "transform 500ms";
targetDocContextView.props.focus(targetDocContextView.rootDoc, {
willZoom, afterFocus: async () => {
targetDocContext._viewTransition = undefined;
+ if (targetDocContext.layoutKey === "layout_icon") {
+ targetDocContextView.iconify(() => this.jumpToDocument(
+ targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc,
+ finished, originalTarget, noSelect, presZoom));
+ }
return ViewAdjustment.doNothing;
}
});
@@ -221,27 +241,27 @@ export class DocumentManager {
finished?.();
} else { // no timecode means we need to find the context view and focus on our target
const findView = (delay: number) => {
- const retryDocView = getFirstDocView(targetDoc); // test again for the target view snce we presumably created the context above by focusing on it
- if (retryDocView) { // we found the target in the context
+ const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
+ if (retryDocView) { // we found the target in the context.
Doc.linkFollowHighlight(retryDocView.rootDoc);
- retryDocView.props.focus(targetDoc, {
+ retryDocView.focus(targetDoc, {
willZoom, afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
!noSelect && focusAndFinish(didFocus);
res(ViewAdjustment.doNothing);
})
}); // focus on the target in the context
- } else if (delay > 1500) {
+ } else if (delay > 1000) {
// we didn't find the target, so it must have moved out of the context. Go back to just creating it.
if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc);
if (targetDoc.layout) { // there will no layout for a TEXTANCHOR type document
createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target
}
} else {
- setTimeout(() => findView(delay + 250), 250);
+ setTimeout(() => findView(delay + 200), 200);
}
};
- findView(0);
+ setTimeout(() => findView(0), 0);
}
} else { // there's no context view so we need to create one first and try again when that finishes
const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget);
@@ -251,7 +271,6 @@ export class DocumentManager {
}
}
}
-
}
export function DocFocusOrOpen(doc: any, collectionDoc?: Doc) {
const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 47b7ce0e7..8ac28bc89 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -384,8 +384,8 @@ export namespace DragManager {
}
}
const rect = ele.getBoundingClientRect();
- const scaleX = rect.width / ele.offsetWidth;
- const scaleY = ele.offsetHeight ? rect.height / ele.offsetHeight : scaleX;
+ const scaleX = rect.width / (ele.offsetWidth || rect.width);
+ const scaleY = ele.offsetHeight ? rect.height / (ele.offsetHeight || rect.height) : scaleX;
elesCont.left = Math.min(rect.left, elesCont.left);
elesCont.top = Math.min(rect.top, elesCont.top);
@@ -409,7 +409,7 @@ export namespace DragManager {
Array.from(pdfBox).filter(pb => pb.width && pb.height).map((pb, i) => pb.getContext('2d')!.drawImage(pdfBoxSrc[i], 0, 0));
}
[dragElement, ...Array.from(dragElement.getElementsByTagName('*'))].forEach(ele =>
- ele.hasAttribute("style") && ((ele as any).style.pointerEvents = "none"));
+ (ele as any).style && ((ele as any).style.pointerEvents = "none"));
dragDiv.appendChild(dragElement);
if (dragElement !== ele) {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index df2c02a8d..9445533dc 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -29,6 +29,7 @@ export class LinkManager {
public static currentLink: Opt<Doc>;
public static get Instance() { return LinkManager._instance; }
public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+ public static AutoKeywords = "keywords:Usages";
static links: Doc[] = [];
constructor() {
LinkManager._instance = this;
@@ -149,9 +150,11 @@ export class LinkManager {
public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
const anchorGroups = new Map<string, Array<Doc>>();
this.relatedLinker(anchor).forEach(link => {
- if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") {
- const group = anchorGroups.get(StrCast(link.linkRelationship));
- anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]);
+ if (link.linkRelationship && link.linkRelationship !== "-ungrouped-") {
+ const relation = StrCast(link.linkRelationship);
+ const anchorRelation = relation.indexOf(":") !== -1 ? relation.split(":")[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation;
+ const group = anchorGroups.get(anchorRelation);
+ anchorGroups.set(anchorRelation, group ? [...group, link] : [link]);
} else {
// if link is in no groups then put it in default group
const group = anchorGroups.get("*");
@@ -221,7 +224,9 @@ export class LinkManager {
const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : (traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs);
- const followLinks = linkDocList.length ? (sourceDoc.isPushpin ? linkDocList : [linkDocList[0]]) : [];
+ const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1);
+ var count = 0;
+ const allFinished = () => ++count === followLinks.length && finished?.();
followLinks.forEach(async linkDoc => {
if (linkDoc) {
const target = (sourceDoc === linkDoc.anchor1 ? linkDoc.anchor2 : sourceDoc === linkDoc.anchor2 ? linkDoc.anchor1 :
@@ -232,20 +237,20 @@ export class LinkManager {
const tour = DocListCast(target[fieldKey]).reverse();
LightboxView.SetLightboxDoc(currentContext, undefined, tour);
setTimeout(LightboxView.Next);
- finished?.();
+ allFinished();
} else {
const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
const containerDoc = containerAnnoDoc || target;
const containerDocContext = Cast(containerDoc?.context, Doc, null);
const targetContext = LightboxView.LightboxDoc ? containerAnnoDoc || containerDocContext : containerDocContext;
const targetNavContext = !Doc.AreProtosEqual(targetContext, currentContext) ? targetContext : undefined;
- DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, finished);
+ DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, "lightbox"), finished), targetNavContext, linkDoc, undefined, sourceDoc, allFinished);
}
} else {
- finished?.();
+ allFinished();
}
} else {
- finished?.();
+ allFinished();
}
});
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 73a261c40..79aaf2158 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -11,6 +11,7 @@ import { DocUtils } from '../documents/Documents';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
import { InteractionUtils } from '../util/InteractionUtils';
import { UndoManager } from '../util/UndoManager';
+import { DocumentView } from './nodes/DocumentView';
import { Touchable } from './Touchable';
@@ -41,6 +42,7 @@ interface ViewBoxBaseProps {
Document: Doc;
DataDoc?: Doc;
ContainingCollectionDoc: Opt<Doc>;
+ DocumentView?: () => DocumentView;
fieldKey: string;
layerProvider?: (doc: Doc, assign?: boolean) => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
@@ -63,7 +65,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
// key where data is stored
@computed get fieldKey() { return this.props.fieldKey; }
- lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
+ lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.DocumentView?.().props.treeViewDoc ?? this.props.ContainingCollectionDoc }).result;
isContentActive = (outsideReaction?: boolean) => (
this.props.isContentActive?.() === false ? false :
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index aa9318310..9b95f1525 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -215,28 +215,30 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
pinWithView = (targetDoc: Doc) => {
if (targetDoc) {
TabDocView.PinDoc(targetDoc);
- const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetDoc.type as any) || targetDoc._viewType === CollectionViewType.Stacking;
- const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG);
- if (scrollable) {
- const scroll = targetDoc._scrollTop;
- activeDoc.presPinView = true;
- activeDoc.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeDoc.presPinTimecode = targetDoc._currentTimecode;
- } else if (pannable) {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeDoc.presPinView = true;
- activeDoc.presPinViewX = x;
- activeDoc.presPinViewY = y;
- activeDoc.presPinViewScale = scale;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const width = targetDoc._clipWidth;
- activeDoc.presPinClipWidth = width;
- activeDoc.presPinView = true;
- }
+ setTimeout(() => {
+ const activeDoc = PresBox.Instance.childDocs[PresBox.Instance.childDocs.length - 1];
+ const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetDoc.type as any) || targetDoc._viewType === CollectionViewType.Stacking;
+ const pannable: boolean = ((targetDoc.type === DocumentType.COL && targetDoc._viewType === CollectionViewType.Freeform) || targetDoc.type === DocumentType.IMG);
+ if (scrollable) {
+ const scroll = targetDoc._scrollTop;
+ activeDoc.presPinView = true;
+ activeDoc.presPinViewScroll = scroll;
+ } else if (targetDoc.type === DocumentType.VID) {
+ activeDoc.presPinTimecode = targetDoc._currentTimecode;
+ } else if (pannable) {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeDoc.presPinView = true;
+ activeDoc.presPinViewX = x;
+ activeDoc.presPinViewY = y;
+ activeDoc.presPinViewScale = scale;
+ } else if (targetDoc.type === DocumentType.COMPARISON) {
+ const width = targetDoc._clipWidth;
+ activeDoc.presPinClipWidth = width;
+ activeDoc.presPinView = true;
+ }
+ });
}
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 82dca1287..481b90249 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -4,8 +4,8 @@ $linkGap: 3px;
.documentDecorations-Dark,
.documentDecorations {
- position: absolute;
- z-index: 2000;
+ position: absolute;
+ z-index: 2000;
}
.documentDecorations-Dark {
background: dimgray;
@@ -17,11 +17,11 @@ $linkGap: 3px;
left: 0;
display: grid;
grid-template-rows: 20px 8px 1fr 8px;
- grid-template-columns: 8px 16px 1fr 8px 8px;
+ grid-template-columns: 8px 1fr 8px;
pointer-events: none;
.documentDecorations-centerCont {
- grid-column: 3;
+ grid-column: 2;
background: none;
}
@@ -41,6 +41,7 @@ $linkGap: 3px;
opacity: 1;
transform: translate(10px, 10px);
grid-row: 4;
+ grid-column: 3
}
.documentDecorations-topLeftResizer,
@@ -73,20 +74,18 @@ $linkGap: 3px;
.documentDecorations-topResizer,
.documentDecorations-bottomResizer {
- grid-column-start: 2;
- grid-column-end: 5;
+ grid-column: 2;
}
.documentDecorations-bottomRightResizer,
.documentDecorations-topRightResizer,
.documentDecorations-rightResizer {
- grid-column-start: 5;
- grid-column-end: 7;
+ grid-column: 3;
}
.documentDecorations-rotation,
.documentDecorations-borderRadius {
- grid-column: 5;
+ grid-column: 3;
grid-row: 4;
border-radius: 100%;
background: black;
@@ -132,114 +131,114 @@ $linkGap: 3px;
opacity: 1;
}
- .documentDecorations-topLeftResizer,
- .documentDecorations-leftResizer,
- .documentDecorations-bottomLeftResizer {
- grid-column: 1;
- }
-
- .documentDecorations-topResizer,
- .documentDecorations-bottomResizer {
- grid-column-start: 2;
- grid-column-end: 5;
- }
-
- .documentDecorations-bottomRightResizer,
- .documentDecorations-topRightResizer,
- .documentDecorations-rightResizer {
- grid-column-start: 5;
- grid-column-end: 7;
- }
-
- .documentDecorations-rotation,
- .documentDecorations-borderRadius {
- grid-column: 5;
- grid-row: 4;
- border-radius: 100%;
- background: black;
- height: 8;
- right: -12;
- top: 12;
- position: relative;
- pointer-events: all;
- cursor: nwse-resize;
-
- .borderRadiusTooltip {
- width: 10px;
- height: 10px;
- position: absolute;
- }
- }
- .documentDecorations-rotation {
- background: transparent;
- right: -15;
- }
-
- .documentDecorations-topLeftResizer,
- .documentDecorations-bottomRightResizer {
- cursor: nwse-resize;
- background: unset;
- opacity: 1;
- }
+ .documentDecorations-topLeftResizer,
+ .documentDecorations-leftResizer,
+ .documentDecorations-bottomLeftResizer {
+ grid-column: 1;
+ }
- .documentDecorations-topLeftResizer {
- border-left: 2px solid;
- border-top: solid 2px;
- }
+ .documentDecorations-topResizer,
+ .documentDecorations-bottomResizer {
+ grid-column: 2;
+ }
- .documentDecorations-bottomRightResizer {
- border-right: 2px solid;
- border-bottom: solid 2px;
- }
+ .documentDecorations-bottomRightResizer,
+ .documentDecorations-topRightResizer,
+ .documentDecorations-rightResizer {
+ grid-column: 3
+ }
- .documentDecorations-topLeftResizer:hover,
- .documentDecorations-bottomRightResizer:hover {
- opacity: 1;
- }
+ .documentDecorations-rotation,
+ .documentDecorations-borderRadius {
+ grid-column: 3;
+ grid-row: 4;
+ border-radius: 100%;
+ background: black;
+ height: 8;
+ right: -12;
+ top: 12;
+ position: relative;
+ pointer-events: all;
+ cursor: nwse-resize;
- .documentDecorations-bottomRightResizer {
- grid-row: 4;
- }
+ .borderRadiusTooltip {
+ width: 10px;
+ height: 10px;
+ position: absolute;
+ }
+ }
+ .documentDecorations-rotation {
+ background: transparent;
+ right: -15;
+ }
- .documentDecorations-topRightResizer,
- .documentDecorations-bottomLeftResizer {
- cursor: nesw-resize;
- background: unset;
- opacity: 1;
- }
+ .documentDecorations-topLeftResizer,
+ .documentDecorations-bottomRightResizer {
+ cursor: nwse-resize;
+ background: unset;
+ opacity: 1;
+ }
+
+ .documentDecorations-topLeftResizer {
+ border-left: 2px solid;
+ border-top: solid 2px;
+ }
+
+ .documentDecorations-bottomRightResizer {
+ border-right: 2px solid;
+ border-bottom: solid 2px;
+ }
+
+ .documentDecorations-topLeftResizer:hover,
+ .documentDecorations-bottomRightResizer:hover {
+ opacity: 1;
+ }
+
+ .documentDecorations-bottomRightResizer {
+ grid-row: 4;
+ }
+
+ .documentDecorations-topRightResizer,
+ .documentDecorations-bottomLeftResizer {
+ cursor: nesw-resize;
+ background: unset;
+ opacity: 1;
+ }
- .documentDecorations-topRightResizer {
- border-right: 2px solid;
- border-top: 2px solid;
- }
+ .documentDecorations-topRightResizer {
+ border-right: 2px solid;
+ border-top: 2px solid;
+ }
- .documentDecorations-bottomLeftResizer {
- border-left: 2px solid;
- border-bottom: 2px solid;
- }
+ .documentDecorations-bottomLeftResizer {
+ border-left: 2px solid;
+ border-bottom: 2px solid;
+ }
- .documentDecorations-topRightResizer:hover,
- .documentDecorations-bottomLeftResizer:hover {
+.documentDecorations-topRightResizer:hover,
+.documentDecorations-bottomLeftResizer:hover {
cursor: nesw-resize;
background: black;
opacity: 1;
- }
+}
- .documentDecorations-topResizer,
- .documentDecorations-bottomResizer {
+.documentDecorations-topResizer,
+.documentDecorations-bottomResizer {
cursor: ns-resize;
- }
+}
- .documentDecorations-title-Dark,
- .documentDecorations-title {
+.documentDecorations-title-Dark,
+.documentDecorations-title {
opacity: 1;
- grid-column-start: 2;
- grid-column-end: 4;
+ 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: 5px;
+ margin-left: 6px; // closeButton width (14) - leftColumn width (8)
+ margin-right: 2px;
height: 20px;
position: absolute;
border-radius: 8px;
@@ -247,7 +246,7 @@ $linkGap: 3px;
.documentDecorations-titleSpan,
.documentDecorations-titleSpan-Dark {
- width: 100%;
+ width: 100% ;
border-radius: 8px;
background: #ffffffa0;
position: absolute;
@@ -263,105 +262,62 @@ $linkGap: 3px;
background: black;
}
- .documentDecorations-contextMenu {
- width: 25px;
- height: calc(100% + 8px); // 8px for the height of the top resizer bar
- grid-column-start: 2;
- grid-column-end: 2;
- pointer-events: all;
- padding-left: 5px;
- cursor: pointer;
- }
-
- .documentDecorations-titleBackground {
- background: #ffffffcf;
- border-radius: 8px;
- width: 100%;
- height: 100%;
- position: absolute;
- }
-
- .documentDecorations-title {
- opacity: 1;
- grid-column-start: 2;
- grid-column-end: 4;
- pointer-events: auto;
- overflow: hidden;
- text-align: center;
- display: flex;
- margin-left: 5px;
- height: 20px;
- position: absolute;
- .documentDecorations-titleSpan {
- width: 100%;
- border-radius: 8px;
- background: #ffffffcf;
- position: absolute;
- display: inline-block;
- cursor: move;
- }
- }
-
- .focus-visible {
- margin-left: 0px;
- }
-}
+ .documentDecorations-titleBackground {
+ background: #ffffffcf;
+ border-radius: 8px;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ }
-.documentDecorations-iconifyButton {
- opacity: 1;
- grid-column-start: 4;
- grid-column-end: 4;
- pointer-events: all;
- right: 0;
- cursor: pointer;
- position: absolute;
- width: 20px;
+ .focus-visible {
+ margin-left: 0px;
+ }
}
.documentDecorations-openButton {
- display: flex;
- align-items: center;
- opacity: 1;
- grid-column-start: 5;
- grid-column-end: 5;
- pointer-events: all;
- cursor: pointer;
+ 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-start: 1;
- grid-column-end: 3;
- pointer-events: all;
- cursor: pointer;
-
- > svg {
- margin: 0;
- }
+ display: flex;
+ align-items: center;
+ opacity: 1;
+ grid-column: 1;
+ pointer-events: all;
+ width: 14px;
+ cursor: pointer;
+
+ > svg {
+ margin: 0;
+ }
}
.documentDecorations-background {
- background: lightblue;
- position: absolute;
- opacity: 0.1;
+ background: lightblue;
+ position: absolute;
+ opacity: 0.1;
}
.linkFlyout {
- grid-column: 2/4;
+ grid-column: 2/4;
}
.linkButton-empty:hover {
- background: $medium-gray;
- transform: scale(1.05);
- cursor: pointer;
+ background: $medium-gray;
+ transform: scale(1.05);
+ cursor: pointer;
}
.linkButton-nonempty:hover {
- background: $medium-gray;
- transform: scale(1.05);
- cursor: pointer;
+ background: $medium-gray;
+ transform: scale(1.05);
+ cursor: pointer;
}
.link-button-container {
@@ -379,132 +335,132 @@ $linkGap: 3px;
}
.linkButtonWrapper {
- pointer-events: auto;
- padding-right: 5px;
- width: 25px;
+ pointer-events: auto;
+ padding-right: 5px;
+ width: 25px;
}
.linkButton-linker {
- height: 20px;
- width: 20px;
- text-align: center;
- border-radius: 50%;
- pointer-events: auto;
- color: $dark-gray;
- border: $dark-gray 1px solid;
+ height: 20px;
+ width: 20px;
+ text-align: center;
+ border-radius: 50%;
+ pointer-events: auto;
+ color: $dark-gray;
+ border: $dark-gray 1px solid;
}
.linkButton-linker:hover {
- cursor: pointer;
- transform: scale(1.05);
+ cursor: pointer;
+ transform: scale(1.05);
}
.linkButton-empty,
.linkButton-nonempty {
- height: 20px;
- width: 20px;
- border-radius: 50%;
- opacity: 0.9;
- pointer-events: auto;
- background-color: $dark-gray;
- color: $white;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- transition: transform 0.2s;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
-
- &:hover {
- background: $medium-gray;
- transform: scale(1.05);
- cursor: pointer;
- }
+ height: 20px;
+ width: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ pointer-events: auto;
+ background-color: $dark-gray;
+ color: $white;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &:hover {
+ background: $medium-gray;
+ transform: scale(1.05);
+ cursor: pointer;
+ }
}
.templating-menu {
- position: absolute;
- pointer-events: auto;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- transition: transform 0.2s;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
+ position: absolute;
+ pointer-events: auto;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 75%;
+ transition: transform 0.2s;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ align-items: center;
}
.documentdecorations-icon {
- margin: 0px;
+ margin: 0px;
}
.templating-button,
.docDecs-tagButton {
- width: 20px;
- height: 20px;
- border-radius: 50%;
- opacity: 0.9;
- font-size: 14;
- background-color: $dark-gray;
- color: $white;
- text-align: center;
- cursor: pointer;
-
- &:hover {
- background: $medium-gray;
- transform: scale(1.05);
- }
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ opacity: 0.9;
+ font-size: 14;
+ background-color: $dark-gray;
+ color: $white;
+ text-align: center;
+ cursor: pointer;
+
+ &:hover {
+ background: $medium-gray;
+ transform: scale(1.05);
+ }
}
#template-list {
- position: absolute;
- top: 25px;
- left: 0px;
- width: max-content;
- font-family: $sans-serif;
- font-size: 12px;
- background-color: $light-gray;
- padding: 2px 12px;
- list-style: none;
-
- .templateToggle,
- .chromeToggle {
- text-align: left;
- }
-
- input {
- margin-right: 10px;
- }
+ position: absolute;
+ top: 25px;
+ left: 0px;
+ width: max-content;
+ font-family: $sans-serif;
+ font-size: 12px;
+ background-color: $light-gray;
+ padding: 2px 12px;
+ list-style: none;
+
+ .templateToggle,
+ .chromeToggle {
+ text-align: left;
+ }
+
+ input {
+ margin-right: 10px;
+ }
}
@-moz-keyframes spin {
- 100% {
- -moz-transform: rotate(360deg);
- }
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
}
@-webkit-keyframes spin {
- 100% {
- -webkit-transform: rotate(360deg);
- }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
}
@keyframes spin {
- 100% {
- -webkit-transform: rotate(360deg);
- transform: rotate(360deg);
- }
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
}
@keyframes shadow-pulse {
- 0% {
- box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8);
- }
+ 0% {
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8);
+ }
- 100% {
- box-shadow: 0 0 0 10px rgba(0, 255, 0, 0);
- }
+ 100% {
+ box-shadow: 0 0 0 10px rgba(0, 255, 0, 0);
+ }
}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d7ead713a..1feb643df 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -144,10 +144,31 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
return true;
}
- onCloseClick = () => {
- const selected = SelectionManager.Views().slice();
- SelectionManager.DeselectAll();
- selected.map(dv => dv.props.removeDocument?.(dv.props.Document));
+ _deleteAfterIconify = false;
+ _iconifyBatch: UndoManager.Batch | undefined;
+ onCloseClick = (force = false) => {
+ if (this.canDelete) {
+ const views = SelectionManager.Views().slice().filter(v => v);
+ this._deleteAfterIconify = force || this._iconifyBatch ? true : false;
+ if (!this._iconifyBatch) {
+ this._iconifyBatch = UndoManager.StartBatch("iconifying");
+ } else {
+ force = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
+ }
+ var iconifyingCount = views.length;
+ const finished = action((force?: boolean) => {
+ if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
+ if (this._deleteAfterIconify) {
+ views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document));
+ SelectionManager.DeselectAll();
+ }
+ this._iconifyBatch?.end();
+ this._iconifyBatch = undefined;
+ }
+ });
+ if (force) finished(force);
+ else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
+ }
}
onMaximizeDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, () => {
@@ -171,7 +192,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
} else if (e.altKey) { // open same document in new tab
CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right");
} else {
- LightboxView.SetLightboxDoc(selectedDocs[0].props.Document, undefined, selectedDocs.slice(1).map(view => view.props.Document));
+ var openDoc = selectedDocs[0].props.Document;
+ if (openDoc.layoutKey === "layout_icon") {
+ openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
+ Doc.deiconifyView(openDoc);
+ }
+ LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document));
}
}
SelectionManager.DeselectAll();
@@ -431,20 +457,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-";
}
+ @computed get canDelete() {
+ return SelectionManager.Views().some(docView => {
+ if (docView.rootDoc.stayInCollection) return false;
+ const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
+ //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
+ return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin);
+ });
+ }
+ @computed get hasIcons() {
+ return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon");
+ }
+
render() {
const bounds = this.Bounds;
const seldoc = SelectionManager.Views().slice(-1)[0];
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
- const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles;
+ const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup;
const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle;
const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton);
- const canDelete = SelectionManager.Views().some(docView => {
- const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
- //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) &&
- return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin);
- });
+ const canDelete = this.canDelete;
const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
<Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
<div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()}
@@ -454,16 +488,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
</Tooltip>);
const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
- const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> :
+ const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} key="title" /> :
this._edtingTitle ?
<input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
- style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }}
type="text" name="dynbox" autoComplete="on"
value={this._accumulatedTitle}
onBlur={e => this.titleBlur()}
onChange={action(e => this._accumulatedTitle = e.target.value)}
onKeyPress={this.titleEntered} /> :
- <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
+ <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} >
<span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
</div>;
@@ -498,13 +531,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
}}>
- {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
+ {!canDelete ? <div /> : topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons), "Close")}
{titleArea}
{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
{hideResizers ? (null) :
<>
- {SelectionManager.Views().length !== 1 || hideTitle ? (null) :
- topBtn("iconify", `window-${seldoc.finalLayoutKey.includes("icon") ? "restore" : "minimize"}`, undefined, this.onIconifyClick, `${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`)}
<div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
<div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
<div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} />
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 4a327a842..6120c4c9e 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -142,11 +142,18 @@ export class KeyManager {
case "delete":
case "backspace":
if (document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") {
- const selected = SelectionManager.Views().filter(dv => !dv.topMost);
UndoManager.RunInBatch(() => {
- SelectionManager.DeselectAll();
- selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document));
- }, "delete");
+ if (LightboxView.LightboxDoc) {
+ LightboxView.SetLightboxDoc(undefined);
+ SelectionManager.DeselectAll();
+ }
+ else DocumentDecorations.Instance.onCloseClick(true);
+ }, "backspace");
+ // const selected = SelectionManager.Views().filter(dv => !dv.topMost);
+ // UndoManager.RunInBatch(() => {
+ // SelectionManager.DeselectAll();
+ // selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document));
+ // }, "delete");
return { stopPropagation: true, preventDefault: true };
}
break;
@@ -268,7 +275,7 @@ export class KeyManager {
const pt = SelectionManager.Views()[0].props.ScreenToLocalTransform().transformPoint(bds.x + (bds.r - bds.x) / 2, bds.y + (bds.b - bds.y) / 2);
const text = `__DashDocId(${pt?.[0] || 0},${pt?.[1] || 0}):` + SelectionManager.Views().map(dv => dv.Document[Id]).join(":");
SelectionManager.Views().length && navigator.clipboard.writeText(text);
- DocumentDecorations.Instance.onCloseClick();
+ DocumentDecorations.Instance.onCloseClick(true);
stopPropagation = false;
preventDefault = false;
}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index ec3bf6c18..59ed0dc92 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -36,7 +36,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
@observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target
private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, scrollTop: Opt<number> }>;
private static _history: Opt<{ doc: Doc, target?: Doc }[]> = [];
- private static _future: Opt<Doc[]> = [];
+ @observable private static _future: Opt<Doc[]> = [];
private static _docView: Opt<DocumentView>;
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) {
@@ -63,7 +63,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
}
}
if (future) {
- this._future = future.slice().sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)).sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length);
+ this._future = [...(this._future ?? []), ...(this.LightboxDoc ? [this.LightboxDoc] : []), ...future.slice().sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)).sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length),];
}
this._doc = doc;
this._layoutTemplate = layoutTemplate;
@@ -118,7 +118,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
addDocTab = LightboxView.AddDocTab;
@action public static Next() {
const doc = LightboxView._doc!;
- const target = LightboxView._docTarget = LightboxView._future?.pop();
+ const target = LightboxView._docTarget = this._future?.pop();
const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
if (targetDocView && target) {
const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
@@ -163,11 +163,8 @@ export class LightboxView extends React.Component<LightboxViewProps> {
const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
if (docView) {
LightboxView._docTarget = target;
- const focusSpeed = 1000;
- doc._viewTransition = `transform ${focusSpeed}ms`;
if (!target) docView.ComponentView?.shrinkWrap?.();
else docView.focus(target, { willZoom: true, scale: 0.9 });
- setTimeout(() => doc._viewTransition = undefined, focusSpeed);
}
else {
LightboxView.SetLightboxDoc(doc, target);
@@ -195,7 +192,9 @@ export class LightboxView extends React.Component<LightboxViewProps> {
const coll = LightboxView._docTarget;
if (coll) {
const fieldKey = Doc.LayoutFieldKey(coll);
- LightboxView.SetLightboxDoc(coll, undefined, [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + "-annotations"])]);
+ const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + "-annotations"])];
+ const links = DocListCast(coll.links).map(link => LinkManager.getOppositeAnchor(link, coll)).filter(doc => doc).map(doc => doc!);
+ LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
TabDocView.PinDoc(coll, { hidePresBox: true });
}
}
@@ -230,7 +229,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
const doc = LightboxView._doc;
const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
- else target && targetView?.focus(target, { willZoom: true, scale: 0.9, instant: true });
+ //else target && targetView?.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}
@@ -270,7 +269,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
() => LightboxView.LightboxDoc && LightboxView._future?.length ? "" : "none", e => {
e.stopPropagation();
LightboxView.Next();
- })}
+ }, this.future()?.length.toString())}
<LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} />
<div className="lightboxView-tabBtn" title={"open in tab"}
onClick={e => {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 609af568e..8e9be1eeb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -297,6 +297,7 @@ export class MainView extends React.Component {
searchFilterDocs={returnEmptyDoclist}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
+ suppressSetHeight={true}
renderDepth={-1}
/>;
}
@@ -361,7 +362,7 @@ export class MainView extends React.Component {
pinToPres={emptyFunction}
docViewPath={returnEmptyDoclist}
layerProvider={undefined}
- styleProvider={this._sidebarContent.proto === Doc.UserDoc().myDashboards ? DashboardStyleProvider : DefaultStyleProvider}
+ styleProvider={this._sidebarContent.proto === Doc.UserDoc().myDashboards || this._sidebarContent.proto === Doc.UserDoc().myFilesystem ? DashboardStyleProvider : DefaultStyleProvider}
rootSelected={returnTrue}
removeDocument={returnFalse}
ScreenToLocalTransform={this.mainContainerXf}
@@ -632,7 +633,7 @@ export class MainView extends React.Component {
<ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} />
{this.topbar}
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
- {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} /> : (null)}
+ {DocumentLinksButton.LinkEditorDocView ? <LinkMenu clearLinkEditor={action(() => DocumentLinksButton.LinkEditorDocView = undefined)} docView={DocumentLinksButton.LinkEditorDocView} /> : (null)}
{LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : (null)}
<div style={{ position: "relative", display: LightboxView.LightboxDoc ? "none" : undefined, zIndex: 2001 }} >
<CollectionMenu panelWidth={this.topMenuWidth} panelHeight={this.topMenuHeight} />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 563261dec..a6b012bd6 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -5,7 +5,7 @@ import { Id } from "../../fields/FieldSymbols";
import { List } from "../../fields/List";
import { NumCast } from "../../fields/Types";
import { GetEffectiveAcl } from "../../fields/util";
-import { Utils } from "../../Utils";
+import { unimplementedFunction, Utils } from "../../Utils";
import { Docs } from "../documents/Documents";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { DragManager } from "../util/DragManager";
@@ -33,6 +33,7 @@ export interface MarqueeAnnotatorProps {
getPageFromScroll?: (top: number) => number;
finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
}
@observer
export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@@ -63,6 +64,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
doc.addEventListener("pointermove", this.onSelectMove);
doc.addEventListener("pointerup", this.onSelectEnd);
+ AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight("rgba(173, 216, 230, 0.75)", true), true);
AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight("rgba(173, 216, 230, 0.75)", true));
AnchorMenu.Instance.Highlight = this.highlight;
AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight("rgba(173, 216, 230, 0.75)", true, savedAnnotations);
@@ -93,6 +95,29 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
}
});
});
+ /**
+ * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
+ * It also initiates a Drag/Drop interaction to place the text annotation.
+ */
+ AnchorMenu.Instance.StartCropDrag = !this.props.anchorMenuCrop ? unimplementedFunction : action((e: PointerEvent, ele: HTMLElement) => {
+ e.preventDefault();
+ e.stopPropagation();
+ var cropRegion: Doc | undefined;
+ const sourceAnchorCreator = () => {
+ cropRegion = this.highlight("rgba(173, 216, 230, 0.75)", true); // hyperlink color
+ cropRegion && this.props.addDocument(cropRegion);
+ return cropRegion;
+ };
+ const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
+ DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
+ dragComplete: e => {
+ if (!e.aborted && e.linkDocument) {
+ Doc.GetProto(e.linkDocument).linkRelationship = "cropped image";
+ Doc.GetProto(e.linkDocument).title = "crop: " + this.props.docView.rootDoc.title;
+ }
+ }
+ });
+ });
}
componentWillUnmount() {
const doc = (this.props.iframe?.()?.contentDocument ?? document);
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 02ea5e53b..d6a6e4301 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -1,59 +1,59 @@
.overlayView {
- position: absolute;
- pointer-events: none;
- top: 0;
- width: 100vw;
- height: 100vh;
- /* background-color: pink; */
- z-index: 100000;
+ position: absolute;
+ pointer-events: none;
+ top: 0;
+ width: 100vw;
+ height: 100vh;
+ /* background-color: pink; */
+ z-index: 100000;
}
.overlayWindow-outerDiv {
- border-radius: 5px;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- top: 0;
- left: 0;
+ border-radius: 5px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ top: 0;
+ left: 0;
}
.overlayWindow-outerDiv,
.overlayView-wrapperDiv {
- position: absolute;
- z-index: 1;
+ position: absolute;
+ z-index: 1;
}
.overlayWindow-titleBar {
- flex: 0 1 30px;
- background: darkslategray;
- color: whitesmoke;
- text-align: center;
- cursor: move;
+ flex: 0 1 30px;
+ background: darkslategray;
+ color: whitesmoke;
+ text-align: center;
+ cursor: move;
}
.overlayWindow-content {
- flex: 1 1 auto;
- display: flex;
- flex-direction: column;
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
}
.overlayWindow-closeButton {
- float: right;
- height: 30px;
- width: 30px;
+ float: right;
+ height: 30px;
+ width: 30px;
}
.overlayWindow-resizeDragger {
- background-color: rgb(0, 0, 0);
- position: absolute;
- right: 0px;
- bottom: 0px;
- width: 10px;
- height: 10px;
- cursor: nwse-resize;
+ background-color: rgb(0, 0, 0);
+ position: absolute;
+ right: 0px;
+ bottom: 0px;
+ width: 10px;
+ height: 10px;
+ cursor: nwse-resize;
}
.overlayView-doc {
- z-index: 9002; //so that it appears above chroma
- position: absolute;
+ z-index: 9002; //so that it appears above chroma
+ position: absolute;
} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 0f51cf9b2..f86406997 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -20,212 +20,213 @@ import { DefaultStyleProvider } from "./StyleProvider";
export type OverlayDisposer = () => void;
export type OverlayElementOptions = {
- x: number;
- y: number;
- width?: number;
- height?: number;
- title?: string;
+ x: number;
+ y: number;
+ width?: number;
+ height?: number;
+ title?: string;
};
export interface OverlayWindowProps {
- children: JSX.Element;
- overlayOptions: OverlayElementOptions;
- onClick: () => void;
+ children: JSX.Element;
+ overlayOptions: OverlayElementOptions;
+ onClick: () => void;
}
@observer
export class OverlayWindow extends React.Component<OverlayWindowProps> {
- @observable x: number;
- @observable y: number;
- @observable width: number;
- @observable height: number;
- constructor(props: OverlayWindowProps) {
- super(props);
-
- const opts = props.overlayOptions;
- this.x = opts.x;
- this.y = opts.y;
- this.width = opts.width || 200;
- this.height = opts.height || 200;
- }
-
- onPointerDown = (_: React.PointerEvent) => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- }
-
- onResizerPointerDown = (_: React.PointerEvent) => {
- document.removeEventListener("pointermove", this.onResizerPointerMove);
- document.removeEventListener("pointerup", this.onResizerPointerUp);
- document.addEventListener("pointermove", this.onResizerPointerMove);
- document.addEventListener("pointerup", this.onResizerPointerUp);
- }
-
- @action
- onPointerMove = (e: PointerEvent) => {
- this.x += e.movementX;
- this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
- this.y += e.movementY;
- this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
- }
-
- @action
- onResizerPointerMove = (e: PointerEvent) => {
- this.width += e.movementX;
- this.width = Math.max(this.width, 30);
- this.height += e.movementY;
- this.height = Math.max(this.height, 30);
- }
-
- onPointerUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- onResizerPointerUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onResizerPointerMove);
- document.removeEventListener("pointerup", this.onResizerPointerUp);
- }
-
- render() {
- return (
- <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"}
- <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
- </div>
- <div className="overlayWindow-content">
- {this.props.children}
- </div>
- <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
- </div>
- );
- }
+ @observable x: number;
+ @observable y: number;
+ @observable width: number;
+ @observable height: number;
+ constructor(props: OverlayWindowProps) {
+ super(props);
+
+ const opts = props.overlayOptions;
+ this.x = opts.x;
+ this.y = opts.y;
+ this.width = opts.width || 200;
+ this.height = opts.height || 200;
+ }
+
+ onPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerDown = (_: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ document.addEventListener("pointermove", this.onResizerPointerMove);
+ document.addEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent) => {
+ this.x += e.movementX;
+ this.x = Math.max(Math.min(this.x, window.innerWidth - this.width), 0);
+ this.y += e.movementY;
+ this.y = Math.max(Math.min(this.y, window.innerHeight - this.height), 0);
+ }
+
+ @action
+ onResizerPointerMove = (e: PointerEvent) => {
+ this.width += e.movementX;
+ this.width = Math.max(this.width, 30);
+ this.height += e.movementY;
+ this.height = Math.max(this.height, 30);
+ }
+
+ onPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+
+ onResizerPointerUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResizerPointerMove);
+ document.removeEventListener("pointerup", this.onResizerPointerUp);
+ }
+
+ render() {
+ return (
+ <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"}
+ <button onClick={this.props.onClick} className="overlayWindow-closeButton">X</button>
+ </div>
+ <div className="overlayWindow-content">
+ {this.props.children}
+ </div>
+ <div className="overlayWindow-resizeDragger" onPointerDown={this.onResizerPointerDown}></div>
+ </div>
+ );
+ }
}
@observer
export class OverlayView extends React.Component {
- public static Instance: OverlayView;
- @observable.shallow
- private _elements: JSX.Element[] = [];
-
- constructor(props: any) {
- super(props);
- if (!OverlayView.Instance) {
- OverlayView.Instance = this;
- }
- }
-
- @action
- addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
- const remove = action(() => {
- const index = this._elements.indexOf(ele);
- if (index !== -1) this._elements.splice(index, 1);
- });
- ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
- transform: `translate(${options.x}px, ${options.y}px)`,
- width: options.width,
- height: options.height,
- top: 0,
- left: 0
- }}>{ele}</div>;
- this._elements.push(ele);
- return remove;
- }
-
- @action
- addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
- const remove = action(() => {
- const index = this._elements.indexOf(contents);
- if (index !== -1) this._elements.splice(index, 1);
- });
- contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
- this._elements.push(contents);
- return remove;
- }
-
-
- @computed get overlayDocs() {
- return CurrentUserUtils.OverlayDocs?.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(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", 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;
+ public static Instance: OverlayView;
+ @observable.shallow
+ private _elements: JSX.Element[] = [];
+
+ constructor(props: any) {
+ super(props);
+ if (!OverlayView.Instance) {
+ OverlayView.Instance = this;
+ }
+ }
+
+ @action
+ addElement(ele: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+ const remove = action(() => {
+ const index = this._elements.indexOf(ele);
+ if (index !== -1) this._elements.splice(index, 1);
});
-
- 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={undefined}
- PanelWidth={returnOne}
- PanelHeight={returnOne}
- ScreenToLocalTransform={Transform.Identity}
- renderDepth={1}
- isDocumentActive={returnTrue}
- isContentActive={emptyFunction}
- whenChildContentsActiveChanged={emptyFunction}
- focus={DocUtils.DefaultFocus}
- styleProvider={DefaultStyleProvider}
- layerProvider={undefined}
- docViewPath={returnEmptyDoclist}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />
- </div>;
- });
- }
-
- public static ShowSpinner() {
- return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
- }
-
-
- render() {
- return (
- <div className="overlayView" id="overlayView">
- <div>
- {this._elements}
- </div>
- <CollectionFreeFormLinksView key="freeformLinks" />
- {this.overlayDocs}
- </div>
- );
- }
+ ele = <div key={Utils.GenerateGuid()} className="overlayView-wrapperDiv" style={{
+ transform: `translate(${options.x}px, ${options.y}px)`,
+ width: options.width,
+ height: options.height,
+ top: 0,
+ left: 0
+ }}>{ele}</div>;
+ this._elements.push(ele);
+ return remove;
+ }
+
+ @action
+ addWindow(contents: JSX.Element, options: OverlayElementOptions): OverlayDisposer {
+ const remove = action(() => {
+ const index = this._elements.indexOf(contents);
+ if (index !== -1) this._elements.splice(index, 1);
+ });
+ contents = <OverlayWindow onClick={remove} key={Utils.GenerateGuid()} overlayOptions={options}>{contents}</OverlayWindow>;
+ this._elements.push(contents);
+ return remove;
+ }
+
+
+ @computed get overlayDocs() {
+ return CurrentUserUtils.OverlayDocs?.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(Cast(Doc.UserDoc().myOverlayDocs, Doc, null), "data", 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={undefined}
+ PanelWidth={() => NumCast(d._width)}
+ PanelHeight={() => NumCast(d._height)}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={1}
+ isDocumentActive={returnTrue}
+ isContentActive={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={DocUtils.DefaultFocus}
+ styleProvider={DefaultStyleProvider}
+ layerProvider={undefined}
+ docViewPath={returnEmptyDoclist}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ </div>;
+ });
+ }
+
+ public static ShowSpinner() {
+ return OverlayView.Instance.addElement(<ReactLoading type="spinningBubbles" color="green" height={250} width={250} />, { x: 300, y: 200 });
+ }
+
+
+ render() {
+ return (
+ <div className="overlayView" id="overlayView">
+ <div>
+ {this._elements}
+ </div>
+ <CollectionFreeFormLinksView key="freeformLinks" />
+ {this.overlayDocs}
+ </div>
+ );
+ }
}
// bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
- OverlayView.Instance.addWindow(<ScriptingRepl />, options);
+ OverlayView.Instance.addWindow(<ScriptingRepl />, options);
}); \ No newline at end of file
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 082492671..4ead8eaf0 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -3,6 +3,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { Doc, DocListCast } from "../../fields/Doc";
import { Cast } from "../../fields/Types";
+import { emptyFunction } from "../../Utils";
import { DocumentType } from "../documents/DocumentTypes";
import { LinkManager } from "../util/LinkManager";
import { SelectionManager } from "../util/SelectionManager";
@@ -38,6 +39,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other;
if (otherdoc) {
+ otherdoc.hidden = false;
this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right");
}
}
@@ -45,7 +47,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
render() {
return !SelectionManager.Views().length ? (null) : <div>
{this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>}
- <LinkMenu docView={SelectionManager.Views().lastElement()} itemHandler={this.getOnClick} position={{ x: 0 }} />
+ <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 015e0c8ee..1af706bb5 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -39,6 +39,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
col._panX = NumCast(target.x) + NumCast(target._width) / 2;
col._panY = NumCast(target.y) + NumCast(target._height) / 2;
}
+ col.hidden = false;
this.props.addDocTab(col, "toggle:right");
setTimeout(() => DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100);
}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 91cf83619..21c688421 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1360,11 +1360,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{this.optionsSubMenu}
- {this.sharingSubMenu}
+ {this.fieldsSubMenu}
- {isNovice ? null : this.filtersSubMenu}
+ {isNovice ? null : this.sharingSubMenu}
- {isNovice ? null : this.fieldsSubMenu}
+ {isNovice ? null : this.filtersSubMenu}
{isNovice ? null : this.layoutSubMenu}
</div>;
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index fae385660..04c0565ea 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -61,7 +61,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
FormattedTextBox.SelectOnLoad = target[Id];
FormattedTextBox.DontSelectInitialText = true;
this.allMetadata.map(tag => target[tag] = tag);
- DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline markup");
+ DocUtils.MakeLink({ doc: anchor }, { doc: target }, "inline comment:comment on");
this.addDocument(target);
this._stackRef.current?.focusDocument(target);
}
@@ -88,7 +88,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey);
docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
showTitle = () => "title";
- setHeightCallback = (height: number) => this.props.setHeight(height + this.filtersHeight());
+ setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight());
render() {
const renderTag = (tag: string) => {
const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index f26ed1f2d..8929954c8 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -25,5 +25,5 @@
}
.styleProvider-treeView-icon {
- display: none;
+ opacity: 0;
} \ No newline at end of file
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 649ee8394..a530ff90a 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -3,10 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
import { action, runInAction } from 'mobx';
+import { extname } from 'path';
import { Doc, Opt, StrListCast } from "../../fields/Doc";
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
-import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
+import { BoolCast, Cast, ImageCast, NumCast, StrCast } from "../../fields/Types";
import { DashColor, lightOrDark } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -22,6 +23,7 @@ import { FieldViewProps } from './nodes/FieldView';
import { SliderBox } from './nodes/SliderBox';
import "./StyleProvider.scss";
import React = require("react");
+import { InkingStroke } from './InkingStroke';
export enum StyleLayers {
Background = "background"
@@ -80,7 +82,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const isAnchor = property.includes(":anchor");
const isAnnotated = property.includes(":annotated");
const isOpen = property.includes(":open");
- const fieldKey = (props as any)?.fieldKey ? (props as any).fieldKey + "-" : isCaption ? "caption-" : "";
+ const fieldKey = props?.fieldKey ? props.fieldKey + "-" : isCaption ? "caption-" : "";
const comicStyle = () => doc && !Doc.IsSystem(doc) && Doc.UserDoc().renderStyle === "comic";
const isBackground = () => StrListCast(doc?._layerTags).includes(StyleLayers.Background);
const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
@@ -88,7 +90,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + ((Math.abs(x * y) * 9301 + 49297) % 233280 / 233280) * (max - min);
switch (property.split(":")[0]) {
- case StyleProp.TreeViewIcon: return Doc.toIcon(doc, isOpen);
+ case StyleProp.TreeViewIcon:
+ const img = ImageCast(doc?.icon, ImageCast(doc?.data));
+ if (img) {
+ const ext = extname(img.url.href);
+ const url = doc?.icon ? img.url.href : img.url.href.replace(ext, "_s" + ext);
+ return <img src={url} width={20} height={15} style={{ margin: "auto", display: "block", objectFit: "contain" }} />;
+ }
+ return Doc.toIcon(doc, isOpen);
case StyleProp.DocContents: return undefined;
case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey";
case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
@@ -98,26 +107,30 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
StrCast(doc._showTitle,
props?.showTitle?.() ||
- (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ?
+ (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ?
(doc.author === Doc.CurrentUserEmail ? StrCast(Doc.UserDoc().showTitle) :
"author;creationDate") : "")) || "");
case StyleProp.Color:
if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY;
const docColor: Opt<string> = StrCast(doc?.[fieldKey + "color"], StrCast(doc?._color));
if (docColor) return docColor;
- const backColor = backgroundCol();
+ const docView = props?.DocumentView?.();
+ const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, "backgroundColor");
if (!backColor) return undefined;
return lightOrDark(backColor);
- case StyleProp.Hidden: return BoolCast(doc?._hidden);
+ case StyleProp.Hidden: return BoolCast(doc?.hidden);
case StyleProp.BorderRounding: return StrCast(doc?.[fieldKey + "borderRounding"], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? "50%" : ""));
case StyleProp.TitleHeight: return 15;
case StyleProp.BorderPath: return comicStyle() && props?.renderDepth && doc?.type !== DocumentType.INK ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, .08), width: 3 } : { path: undefined, width: 0 };
case StyleProp.JitterRotation: return comicStyle() ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
case StyleProp.HeaderMargin: return ([CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
- doc?.type === DocumentType.RTF) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
+ doc?.type === DocumentType.RTF || doc?.type === DocumentType.LABEL) && showTitle() && !StrCast(doc?.showTitle).includes(":hover") ? 15 : 0;
case StyleProp.BackgroundColor: {
if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY;
- let docColor: Opt<string> = StrCast(doc?.[fieldKey + "backgroundColor"], StrCast(doc?._backgroundColor, isCaption ? "rgba(0,0,0,0.4)" : ""));
+ let docColor: Opt<string> =
+ StrCast(doc?.[fieldKey + "backgroundColor"],
+ StrCast(doc?._backgroundColor,
+ StrCast(props?.Document.backgroundColor, isCaption ? "rgba(0,0,0,0.4)" : "")));
switch (doc?.type) {
case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? "" : ""); break;
case DocumentType.PRES: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break;
@@ -127,7 +140,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case DocumentType.INK: docColor = doc?.isInkMask ? "rgba(0,0,0,0.7)" : undefined; break;
case DocumentType.SLIDER: break;
case DocumentType.EQUATION: docColor = docColor || "transparent"; break;
- case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined); break;
+ case DocumentType.LABEL: docColor = docColor || (doc.annotationOn !== undefined ? "rgba(128, 128, 128, 0.18)" : undefined) || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
case DocumentType.LINKANCHOR: docColor = isAnchor ? Colors.LIGHT_BLUE : "transparent"; break;
case DocumentType.LINK: docColor = (isAnchor ? docColor : "") || "transparent"; break;
@@ -140,13 +153,16 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case DocumentType.COL:
if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break;
docColor = docColor ||
- (doc?._isGroup ? "#00000004" : // very faint highlight to show bounds of group
- (doc?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc) ? (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY) : // system docs (seen in treeView) get a grayish background
- isBackground() ? "cyan" : // ?? is there a good default for a background collection
- doc.annotationOn ? "#00000015" : // faint interior for collections on PDFs, images, etc
+ (doc?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc) ? (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY) : // system docs (seen in treeView) get a grayish background
+ isBackground() ? "cyan" : // ?? is there a good default for a background collection
+ doc.annotationOn ? "#00000015" : // faint interior for collections on PDFs, images, etc
+ (doc?._isGroup ? undefined :
StrCast((props?.renderDepth || 0) > 0 ?
Doc.UserDoc().activeCollectionNestedBackground :
- Doc.UserDoc().activeCollectionBackground ?? (darkScheme() ? Colors.BLACK : Colors.WHITE))));
+ Doc.UserDoc().activeCollectionBackground ?? (darkScheme() ?
+ Colors.BLACK :
+ "linear-gradient(#065fff, #85c1f9)"))
+ ));
break;
//if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); break;
@@ -180,9 +196,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
if (doc?.type === DocumentType.MARKER) return "none";
if (props?.pointerEvents === "none") return "none";
const layer = doc && props?.layerProvider?.(doc);
- if (opacity() === 0 || (doc?.type === DocumentType.INK && !docProps?.treeViewDoc) || doc?.isInkMask) return "none";
+ const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
+ if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return "none";
if (layer === false && !selected && !SnappingManager.GetIsDragging()) return "none";
- if (doc?.type !== DocumentType.INK && layer === true) return "all";
+ if (!isInk && layer === true) return "all";
return undefined;
case StyleProp.Decorations:
if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
@@ -197,7 +214,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) {
- return <div className={`styleProvider-treeView-icon${doc[field] ? "-active" : ""}`}
+ return <div title={field} className={`styleProvider-treeView-icon${doc[field] ? "-active" : ""}`}
onClick={undoBatch(action((e: React.MouseEvent) => {
e.stopPropagation();
clickFunc ? clickFunc() : (doc[field] = doc[field] ? undefined : true);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 467c2893f..abb4b6bc6 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -44,14 +44,14 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
const curDoc = this.childLayoutPairs?.[index];
- const captionProps = { ...this.props, fieldKey: "caption" };
+ const captionProps = { ...OmitKeys(this.props, ["setHeight",]).omit, fieldKey: "caption" };
const marginX = NumCast(this.layoutDoc["caption-xMargin"]);
const marginY = NumCast(this.layoutDoc["caption-yMargin"]);
const showCaptions = StrCast(this.layoutDoc._showCaption);
return !(curDoc?.layout instanceof Doc) ? (null) :
<>
<div className="collectionCarouselView-image" key="image">
- <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
+ <DocumentView {...OmitKeys(this.props, ["setHeight", "NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
hideCaptions={showCaptions ? true : false}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 6931d9896..64a5f38d4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -11,7 +11,7 @@ import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { Cast, NumCast, StrCast } from "../../../fields/Types";
import { inheritParentAcls } from '../../../fields/util';
-import { emptyFunction } from '../../../Utils';
+import { emptyFunction, incrementTitleCopy } from '../../../Utils';
import { DocServer } from "../../DocServer";
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -92,7 +92,7 @@ export class CollectionDockingView extends CollectionSubView() {
DragManager.CompleteWindowDrag = undefined;
}
finishDrag?.(aborted);
- }
+ };
}
@undoBatch
public CloseFullScreen = () => {
@@ -316,7 +316,7 @@ export class CollectionDockingView extends CollectionSubView() {
}
this._goldenLayout.init();
this._goldenLayout.root.layoutManager.on('itemDropped', this.tabItemDropped);
- this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart)
+ this._goldenLayout.root.layoutManager.on('dragStart', this.tabDragStart);
this._goldenLayout.root.layoutManager.on('activeContentItemChanged', this.stateChanged);
}
}
@@ -390,7 +390,6 @@ export class CollectionDockingView extends CollectionSubView() {
}
public static async Copy(doc: Doc, clone = false) {
- clone = !Doc.UserDoc().noviceMode;
let json = StrCast(doc.dockingConfig);
if (clone) {
const cloned = (await Doc.MakeClone(doc));
@@ -403,13 +402,13 @@ export class CollectionDockingView extends CollectionSubView() {
const origtabs = origtabids.map(id => DocServer.GetCachedRefField(id)).filter(f => f).map(f => f as Doc);
const newtabs = origtabs.map(origtab => {
const origtabdocs = DocListCast(origtab.data);
- const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true) : Doc.MakeAlias(origtab);
+ const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab);
const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc));
newtabdocs.length && (Doc.GetProto(newtab).data = new List<Doc>(newtabdocs));
json = json.replace(origtab[Id], newtab[Id]);
return newtab;
});
- return Docs.Create.DockDocument(newtabs, json, { title: "Snapshot: " + doc.title });
+ return Docs.Create.DockDocument(newtabs, json, { title: incrementTitleCopy(StrCast(doc.title)) });
}
@action
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 0ca0a463e..4489601db 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../fields/Doc";
import { NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnTrue, setupMoveUpEvents } from "../../../Utils";
+import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
import { DocUtils } from "../../documents/Documents";
import { SelectionManager } from "../../util/SelectionManager";
import { SnappingManager } from "../../util/SnappingManager";
@@ -11,6 +11,7 @@ import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormV
import "./CollectionPileView.scss";
import { CollectionSubView } from "./CollectionSubView";
import React = require("react");
+import { ScriptField } from "../../../fields/ScriptField";
@observer
export class CollectionPileView extends CollectionSubView() {
@@ -47,19 +48,20 @@ export class CollectionPileView extends CollectionSubView() {
return this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
}
+ toggleIcon = () => {
+ return ScriptField.MakeScript("documentView.iconify()", { documentView: "any" });
+ }
+
// returns the contents of the pileup in a CollectionFreeFormView
@computed get contents() {
const isStarburst = this.layoutEngine() === "starburst";
- const draggingSelf = this.props.isSelected();
return <div className="collectionPileView-innards"
- style={{
- pointerEvents: isStarburst || (SnappingManager.GetIsDragging() && !draggingSelf) ? undefined : "none",
- zIndex: isStarburst && !SnappingManager.GetIsDragging() ? -10 : "auto"
- }} >
+ style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : "none" }} >
<CollectionFreeFormView {...this.props}
layoutEngine={this.layoutEngine}
childDocumentsActive={isStarburst ? returnTrue : undefined}
addDocument={this.addPileDoc}
+ childClickScript={this.toggleIcon()}
moveDocument={this.removePileDoc} />
</div>;
}
@@ -72,13 +74,13 @@ export class CollectionPileView extends CollectionSubView() {
this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize);
- DocUtils.pileup(this.childDocs, undefined, undefined, false);
+ DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
this.layoutDoc._panX = 0;
this.layoutDoc._panY = -10;
this.props.Document._pileLayoutEngine = 'pass';
} else {
const defaultSize = 25;
- !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 500);
+ !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
!this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
if (this.layoutEngine() === 'pass') {
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 8634ea139..509005b45 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -141,7 +141,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
() => this.layoutDoc._columnHeaders = new List()
);
this._autoHeightDisposer = reaction(() => this.layoutDoc._autoHeight,
- autoHeight => autoHeight && this.props.setHeight(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
+ autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
this.headerMargin + (this.isStackingView ?
Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))) :
this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0)))));
@@ -408,7 +408,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace("px", "")))));
if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
- this.props.setHeight(height);
+ this.props.setHeight?.(height);
}
}
}));
@@ -458,7 +458,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.observer = new _global.ResizeObserver(action((entries: any) => {
if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), 0);
- this.props.setHeight(this.headerMargin + height);
+ this.props.setHeight?.(this.headerMargin + height);
}
}));
this.observer.observe(ref);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index d8f1287cd..30e0adf24 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
import { action, computed, IReactionDisposer, reaction, observable, runInAction } from "mobx";
import CursorField from "../../../fields/CursorField";
import { Doc, Opt, Field, DocListCast, AclPrivate, StrListCast } from "../../../fields/Doc";
-import { Id, ToString } from "../../../fields/FieldSymbols";
+import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec } from "../../../fields/Schema";
import { ScriptField } from "../../../fields/ScriptField";
@@ -453,7 +453,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if (completed) completed(set);
else {
if (isFreeformView && generatedDocuments.length > 1) {
- addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!);
+ addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!,);
} else {
generatedDocuments.forEach(addDocument);
}
@@ -480,5 +480,4 @@ import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTex
import { CollectionView, CollectionViewType, CollectionViewProps } from "./CollectionView";
import { SelectionManager } from "../../util/SelectionManager";
import { OverlayView } from "../OverlayView";
-import { GetEffectiveAcl, TraceMobx } from "../../../fields/util";
-
+import { GetEffectiveAcl, TraceMobx } from "../../../fields/util"; \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index d6398fda5..e63ea7151 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -235,6 +235,7 @@ export class CollectionTimeView extends CollectionSubView() {
ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
+ const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField));
pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotDoc._pivotField;
@@ -249,5 +250,13 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
pivotDoc._pivotField = filterVals[0];
}
}
+ const newFilters = StrListCast(pivotDoc._docFilters);
+ if (newFilters.length && originalFilter.length &&
+ newFilters[newFilters.length - 1] === originalFilter[originalFilter.length - 1]) {
+ pivotDoc._prevFilterIndex = --prevFilterIndex;
+ pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined;
+ pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined;
+ pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined;
+ }
});
}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index b664d9d82..93523a6cf 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -1,7 +1,9 @@
@import "../global/globalCssVariables";
+
.collectionTreeView-container {
transform-origin: top left;
+ height: 100%;
}
.collectionTreeView-dropTarget {
border-width: $COLLECTION_BORDER_WIDTH;
@@ -30,9 +32,11 @@
width: unset;
height: unset;
}
+ &:hover {
+ cursor: ns-resize;
+ }
}
-
.no-indent {
padding-left: 0;
width: max-content;
@@ -71,6 +75,11 @@
display: none;
}
+.collectionTreeView-contents {
+ display: flex;
+ flex-direction: column;
+}
+
.collectionTreeView-titleBar {
display: inline-block;
width: 100%;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e84517f40..41970eb96 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -88,7 +88,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
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());
this.layoutDoc._autoHeightMargins = bodyHeight;
- this.props.setHeight(bodyHeight + titleHeight);
+ this.props.setHeight?.(bodyHeight + titleHeight);
}
}
unobserveHeight = (ref: any) => {
@@ -341,14 +341,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div className="collectionTreeView-contents" key="tree" style={{
...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
overflow: "auto",
- height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+ height: "100%"//this.layoutDoc._autoHeight ? "max-content" : "100%"
}} >
{titleBar}
<div className="collectionTreeView-container"
style={{
transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
paddingLeft: `${this.marginX()}px`,
- height: "max-content",
width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
}}
onContextMenu={this.onContextMenu}>
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index bd3e810c9..8e45ec3b3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -33,6 +33,7 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TabDocView.scss";
import React = require("react");
+import { List } from '../../../fields/List';
const _global = (window /* browser */ || global /* node */) as any;
interface TabDocViewProps {
@@ -209,9 +210,18 @@ export class TabDocView extends React.Component<TabDocViewProps> {
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;
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 2e33d3564..b91737a1d 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -21,7 +21,9 @@
}
.treeView-bulletIcons {
- width: $TREE_BULLET_WIDTH;
+ // width: $TREE_BULLET_WIDTH;
+ width: 100%;
+ height: 100%;
.treeView-expandIcon {
display: none;
@@ -37,22 +39,40 @@
&:hover {
.treeView-expandIcon {
- display: unset;
+ display: unset;
}
}
}
+ .treeView-bulletIcons:hover img {
+ left: 14px;
+ position: absolute;
+ transform-origin: center left;
+ transform: scale(6);
+ pointer-events:none;
+ }
.bullet {
position: relative;
width: $TREE_BULLET_WIDTH;
color: $medium-gray;
margin-top: 3px;
- transform: scale(1.3, 1.3);
+ // 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;
}
}
+.treeView-sorting {
+ position: absolute;
+ height: max-content;
+ pointer-events: none;
+ color: white;
+ border-radius: 4px;
+ font-size: 10px;
+}
+.treeView-container-active {
+ cursor: default;
+}
.treeView-container-outline-active
.treeView-container-active {
z-index: 100;
@@ -60,6 +80,10 @@
pointer-events: all;
}
+.bullet:hover {
+ z-index: 100;
+}
+
.treeView-openRight {
display: none;
height: 17px;
@@ -123,7 +147,9 @@
}
>svg {
- display: none;
+ //display: none;
+ opacity: 0;
+ pointer-events: none;
}
}
}
@@ -150,6 +176,8 @@
>svg,
.styleProvider-treeView-icon {
display: inherit;
+ opacity: unset;
+ pointer-events: unset;
}
}
}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index ff5c4bab1..647476784 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,7 +1,8 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, WidthSym, StrListCast } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, DocListCastOrNull, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -9,7 +10,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, simulateMouseClick, Utils, returnOne } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, simulateMouseClick, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
@@ -21,16 +22,15 @@ import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { EditableView } from "../EditableView";
import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
-import { DocumentView, DocumentViewProps, StyleProviderFunc, DocumentViewInternal } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { KeyValueBox } from '../nodes/KeyValueBox';
-import { SliderBox } from '../nodes/SliderBox';
-import { StyleProp, testDocProps } from '../StyleProvider';
+import { StyleProp } from '../StyleProvider';
import { CollectionTreeView } from './CollectionTreeView';
import { CollectionView, CollectionViewType } from './CollectionView';
import "./TreeView.scss";
import React = require("react");
+import { KeyValueBox } from '../nodes/KeyValueBox';
export interface TreeViewProps {
treeView: CollectionTreeView;
@@ -79,6 +79,7 @@ export class TreeView extends React.Component<TreeViewProps> {
static _editTitleOnLoad: Opt<{ id: string, parent: TreeView | CollectionTreeView | undefined }>;
static _openTitleScript: Opt<ScriptField | undefined>;
static _openLevelScript: Opt<ScriptField | undefined>;
+ static _sortUIMap = [["up", "crimson", "↑"], ["down", "blue", "↓"], ["z", "green", "z"], ["x", "darkgray", '\u00A0\u00A0\u00A0']];
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
@observable _docRef: Opt<DocumentView>;
@@ -101,17 +102,18 @@ export class TreeView extends React.Component<TreeViewProps> {
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
return this.doc.viewType === CollectionViewType.Docking ? this.fieldKey :
- this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "layout") :
- this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
+ this.props.treeView.dashboardMode ? this.fieldKey :
+ this.props.treeView.fileSysMode ? (this.doc.isFolder ? this.fieldKey : "aliases") :
+ this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.UserDoc().noviceMode ? "layout" : StrCast(this.props.treeView.doc.treeViewExpandedView, "fields");
}
@computed get doc() { return this.props.document; }
@computed get treeViewOpen() { return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, "treeViewOpen", "boolean", true)) || this._transientOpenState; }
@computed get treeViewExpandedView() { return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView; }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.containerCollection.maxEmbedHeight, 200); }
- @computed get dataDoc() { return this.doc[DataSym]; }
+ @computed get dataDoc() { return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DataSym]; }
@computed get layoutDoc() { return Doc.Layout(this.doc); }
- @computed get fieldKey() { return Doc.LayoutFieldKey(this.doc); }
+ @computed get fieldKey() { return StrCast(this.doc._treeViewFieldKey, Doc.LayoutFieldKey(this.doc)); }
@computed get childDocs() { return this.childDocList(this.fieldKey); }
@computed get childLinks() { return this.childDocList("links"); }
@computed get childAliases() { return this.childDocList("aliases"); }
@@ -161,12 +163,12 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
- // this.props.addDocTab(CurrentUserUtils.ActiveDashboard.isShared ? Doc.MakeAlias(this.props.document) : this.props.document, "add:right");
- const bestAlias = DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
- this.props.addDocTab(bestAlias ?? Doc.MakeAlias(this.props.document), "add:right");
-
+ const bestAlias = docView.props.Document.author === Doc.CurrentUserEmail ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
+ const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail);
+ this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), "lightbox");
}
}
+
constructor(props: any) {
super(props);
if (!TreeView._openLevelScript) {
@@ -235,7 +237,7 @@ export class TreeView extends React.Component<TreeViewProps> {
title: "-title-",
treeViewExpandedViewLock: true, treeViewExpandedView: "data",
_viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline",
- x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10
+ x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, _width: 1000, _height: 10
});
Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
Doc.GetProto(bullet).data = new List<Doc>([]);
@@ -290,7 +292,12 @@ export class TreeView extends React.Component<TreeViewProps> {
(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
if (canAdd) {
- return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
+ return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) =>
+ (move ?
+ move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false)
+ :
+ addDoc(d)) || added,
+ false));
}
return false;
}
@@ -378,8 +385,14 @@ export class TreeView extends React.Component<TreeViewProps> {
return rows;
}
- rtfWidth = () => Math.min(this.layoutDoc?.[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1);
- rtfHeight = () => this.rtfWidth() <= this.layoutDoc?.[WidthSym]() ? Math.min(this.layoutDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
+ rtfWidth = () => {
+ const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ""))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+ return Math.min(layout[WidthSym](), (this.props.panelWidth() - treeBulletWidth())) / (this.props.treeView.props.scaling?.() || 1);
+ }
+ rtfHeight = () => {
+ const layout = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ""))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+ return this.rtfWidth() <= layout[WidthSym]() ? Math.min(layout[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT;
+ }
rtfOutlineHeight = () => Math.max(this.layoutDoc?.[HeightSym](), treeBulletWidth());
expandPanelHeight = () => {
if (this.layoutDoc._fitWidth) return this.docHeight();
@@ -398,13 +411,16 @@ export class TreeView extends React.Component<TreeViewProps> {
TraceMobx();
const expandKey = this.treeViewExpandedView;
if (["links", "annotations", "aliases", this.fieldKey].includes(expandKey)) {
+ const sorting = StrCast(this.doc.treeViewSortCriterion);
+ const color = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[1];
+ const label = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[2];
const key = (expandKey === "annotations" ? `${this.fieldKey}-` : "") + expandKey;
const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
// if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't),
// then the modification would be done here
const ordering = StrCast(this.doc.treeViewSortCriterion);
- if (ordering === "Z") {
+ if (ordering === "z") {
const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering);
doc.zIndex = addBefore ? NumCast(addBefore.zIndex) + (before ? -0.5 : 0.5) : 1000;
docs.push(doc);
@@ -417,24 +433,29 @@ export class TreeView extends React.Component<TreeViewProps> {
const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
const docs = expandKey === "aliases" ? this.childAliases : expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs;
let downX = 0, downY = 0;
- const sortings = ["up", "down", "Z", undefined];
+ const sortings = this.props.treeView.rootDoc === Doc.UserDoc().myFilesystem ? ["up", "down", "x"] : ["up", "down", "z", "x"];
const curSort = Math.max(0, sortings.indexOf(Cast(this.doc.treeViewSortCriterion, "string", null)));
- return <ul key={expandKey + "more"} title={"sort: " + sortings[curSort]} className={this.doc.treeViewHideTitle ? "no-indent" : ""}
- onPointerDown={e => { downX = e.clientX; downY = e.clientY; e.stopPropagation(); }}
- onClick={(e) => {
- if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
- !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortings[(curSort + 1) % sortings.length]);
- e.stopPropagation();
- }
- }}>
- {!docs ? (null) :
- TreeView.GetChildElements(docs, this.props.treeView, this, this.layoutDoc,
- this.dataDoc, this.props.containerCollection, this.props.prevSibling, addDoc, remDoc, this.move,
- StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
- this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
- [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
- this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
- </ul >;
+ return <>
+ {!docs?.length ? (null) : <div className={'treeView-sorting'} style={{ background: color }} >
+ {label}
+ </div>}
+ <ul key={expandKey + "more"} title="click to change sort order" className={this.doc.treeViewHideTitle ? "no-indent" : ""}
+ onPointerDown={e => { downX = e.clientX; downY = e.clientY; e.stopPropagation(); }}
+ onClick={(e) => {
+ if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
+ !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortings[(curSort + 1) % sortings.length]);
+ e.stopPropagation();
+ }
+ }}>
+ {!docs ? (null) :
+ TreeView.GetChildElements(docs, this.props.treeView, this, this.layoutDoc,
+ this.dataDoc, this.props.containerCollection, this.props.prevSibling, addDoc, remDoc, this.move,
+ StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform,
+ this.props.isContentActive, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields,
+ [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenChildContentsActiveChanged,
+ this.props.dontRegisterView, emptyFunction, emptyFunction, this.childContextMenuItems())}
+ </ul >
+ </>;
} else if (this.treeViewExpandedView === "fields") {
return <ul key={this.doc[Id] + this.doc.title}>
<div style={{ display: "inline-block" }} >
@@ -471,7 +492,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onClick={this.bulletClick}
style={this.props.treeView.outlineMode ? { opacity: this.titleStyleProvider?.(this.doc, this.props.treeView.props, StyleProp.Opacity) } : {
color: StrCast(this.doc.color, checked === "unchecked" ? "white" : "inherit"),
- opacity: checked === "unchecked" ? undefined : 0.4
+ opacity: checked === "unchecked" || typeof iconType !== "string" ? undefined : 0.4
}}>
{this.props.treeView.outlineMode ?
!(this.doc.text as RichTextField)?.Text ? (null) :
@@ -484,22 +505,19 @@ export class TreeView extends React.Component<TreeViewProps> {
checked === "unchecked" ? "square" :
!this.treeViewOpen ? "caret-right" : "caret-down"} />
</div>
- {this.onCheckedClick ? (null) : <FontAwesomeIcon icon={iconType} />}
+ {this.onCheckedClick ? (null) : typeof iconType === "string" ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType}
</div>
}
</div>;
}
@computed get validExpandViewTypes() {
- if (this.props.treeView.dashboardMode && Doc.UserDoc().noviceMode) {
- return [this.doc.viewType === CollectionViewType.Docking ? this.fieldKey : "layout"];
- }
- const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length ? "annotations" : "";
- const links = () => DocListCast(this.doc.links).length ? "links" : "";
- const data = () => this.childDocs ? this.fieldKey : "";
+ const annos = () => DocListCast(this.doc[this.fieldKey + "-annotations"]).length && !this.props.treeView.dashboardMode ? "annotations" : "";
+ const links = () => DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? "links" : "";
+ const data = () => this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : "";
const aliases = () => this.props.treeView.dashboardMode ? "" : "aliases";
const fields = () => Doc.UserDoc().noviceMode ? "" : "fields";
- const layout = this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"];
+ const layout = (Doc.UserDoc().noviceMode) || this.doc.viewType === CollectionViewType.Docking ? [] : ["layout"];
return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
}
@action
@@ -514,9 +532,9 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get headerElements() {
return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? (null)
: <>
- {this.doc.hideContextMenu ? (null) : <FontAwesomeIcon key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />}
+ {this.doc.hideContextMenu ? (null) : <FontAwesomeIcon title="context menu" key="bars" icon="bars" size="sm" onClick={e => { this.showContextMenu(e); e.stopPropagation(); }} />}
{this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? (null) :
- <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
+ <span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
{this.treeViewExpandedView}
</span>}
</>;
@@ -578,14 +596,21 @@ export class TreeView extends React.Component<TreeViewProps> {
return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
}
onKeyDown = (e: React.KeyboardEvent) => {
- if (this.doc.treeViewHideHeader || this.props.treeView.outlineMode) {
- e.stopPropagation();
- e.preventDefault();
+ if (this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode) {
switch (e.key) {
- case "Tab": setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
+ case "Tab":
+ e.stopPropagation();
+ e.preventDefault();
+ setTimeout(() => RichTextMenu.Instance.TextView?.EditorView?.focus(), 150);
return UndoManager.RunInBatch(() => e.shiftKey ? this.props.outdentDocument?.(true) : this.props.indentDocument?.(true), "tab");
- case "Backspace": return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc);
- case "Enter": return UndoManager.RunInBatch(this.makeTextCollection, "bullet");
+ case "Backspace":
+ e.stopPropagation();
+ e.preventDefault();
+ return !(this.doc.text as RichTextField)?.Text && this.props.removeDoc?.(this.doc);
+ case "Enter":
+ e.stopPropagation();
+ e.preventDefault();
+ return UndoManager.RunInBatch(this.makeTextCollection, "bullet");
}
}
}
@@ -690,7 +715,7 @@ export class TreeView extends React.Component<TreeViewProps> {
renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
return <>
<div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader"
- style={{ width: "max-content" }}
+ style={{ width: StrCast(this.doc.treeViewHeaderWidth, "max-content") }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -704,8 +729,7 @@ export class TreeView extends React.Component<TreeViewProps> {
renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
- const layout = StrCast(Doc.LayoutField(this.layoutDoc));
- const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
+ const isExpandable = this.doc._treeViewGrowsHorizontally;
const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
const panelHeight = asText ? this.rtfOutlineHeight : isExpandable ? this.rtfHeight : this.expandPanelHeight;
return <DocumentView key={this.doc[Id]} ref={action((r: DocumentView | null) => this._dref = r)}
@@ -716,6 +740,7 @@ export class TreeView extends React.Component<TreeViewProps> {
NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined}
NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined}
LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined}
+ LayoutTemplate={this.props.treeView.props.childLayoutTemplate}
isContentActive={isActive}
isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
@@ -758,17 +783,17 @@ export class TreeView extends React.Component<TreeViewProps> {
}
// renders the document in the header field instead of a text proxy.
- @computed get renderDocumentAsHeader() {
+ renderDocumentAsHeader = (asText: boolean) => {
return <>
{this.renderBullet}
- {this.renderEmbeddedDocument(true, this.props.isContentActive)}
+ {this.renderEmbeddedDocument(asText, this.props.isContentActive)}
</>;
}
@computed get renderBorder() {
- const sorting = this.doc[`${this.fieldKey}-sortCriterion`];
- return <div className={`treeView-border${this.props.treeView.outlineMode ? "outline" : ""}`}
- style={{ borderColor: sorting === undefined ? undefined : sorting === "up" ? "crimson" : sorting === "down" ? "blue" : "green" }}>
+ const sorting = StrCast(this.doc.treeViewSortCriterion);
+ const color = (TreeView._sortUIMap.find((sortUIfields) => sortUIfields[0] === sorting) ?? TreeView._sortUIMap.lastElement())[1];
+ return <div className={`treeView-border${this.props.treeView.outlineMode ? "outline" : ""}`} style={{ borderColor: color }}>
{!this.treeViewOpen ? (null) : this.renderContent}
</div>;
}
@@ -785,7 +810,7 @@ export class TreeView extends React.Component<TreeViewProps> {
render() {
TraceMobx();
- const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
+ 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 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
<div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
ref={this.createTreeDropTarget}
@@ -793,9 +818,9 @@ export class TreeView extends React.Component<TreeViewProps> {
//onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
onKeyDown={this.onKeyDown}>
<li className="collection-child">
- {hideTitle && this.doc.type !== DocumentType.RTF ?
+ {hideTitle && this.doc.type !== DocumentType.RTF && !this.doc.treeViewRenderAsBulletHeader ? // should test for prop 'treeViewRenderDocWithBulletAsHeader"
this.renderEmbeddedDocument(false, returnFalse) :
- this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
+ this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader(!this.doc.treeViewRenderAsBulletHeader) : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>;
}
@@ -817,9 +842,9 @@ export class TreeView extends React.Component<TreeViewProps> {
};
docs.sort(function (d1, d2): 0 | 1 | -1 {
const a = (criterion === "up" ? d2 : d1);
- const b = (criterion === "up" ? d1 : d2);
- const first = a[criterion === "Z" ? "zIndex" : "title"];
- const second = b[criterion === "Z" ? "zIndex" : "title"];
+ const b = (criterion === "down" ? d1 : d2);
+ const first = a[criterion === "z" ? "zIndex" : "title"];
+ const second = b[criterion === "z" ? "zIndex" : "title"];
if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1;
if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second);
return criterion ? 1 : -1;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 9fed82dae..16c7df311 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -111,23 +111,25 @@ export function computerStarburstLayout(
viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[],
engineProps: any
) {
+ const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel
const docMap = new Map<string, PoolData>();
- const burstRadius = [NumCast(pivotDoc._starburstRadius, panelDim[0]), NumCast(pivotDoc._starburstRadius, panelDim[1])];
- const docScale = NumCast(pivotDoc._starburstDocScale);
- const docSize = docScale * 100; // assume a icon sized at 100
- const scaleDim = [burstRadius[0] + docSize, burstRadius[1] + docSize];
+ const docSize = mustFit ? panelDim[0] * .33 : 75; // assume an icon sized at 75
+ const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize];
+ const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize];
childPairs.forEach(({ layout, data }, i) => {
+ const docSize = layout.layoutKey === "layout_icon" ? (mustFit ? panelDim[0] * .33 : 75) : 400; // assume a icon sized at 75
const deg = i / childPairs.length * Math.PI * 2;
docMap.set(layout[Id], {
- x: Math.cos(deg) * (burstRadius[0] / 3) - docScale * layout[WidthSym]() / 2,
- y: Math.sin(deg) * (burstRadius[1] / 3) - docScale * layout[HeightSym]() / 2,
- width: docScale * layout[WidthSym](),
- height: docScale * layout[HeightSym](),
+ x: Math.cos(deg) * burstRadius[0] - docSize / 2,
+ y: Math.sin(deg) * burstRadius[1] - docSize * layout[HeightSym]() / layout[WidthSym]() / 2,
+ width: docSize,//layout[WidthSym](),
+ height: docSize * layout[HeightSym]() / layout[WidthSym](),
+ zIndex: NumCast(layout.zIndex),
pair: { layout, data },
replica: ""
});
});
- const divider = { type: "div", color: "transparent", x: -burstRadius[0] / 3, y: 0, width: 15, height: 15, payload: undefined };
+ const divider = { type: "div", color: "transparent", x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
}
@@ -265,7 +267,13 @@ export function computePivotLayout(
});
const dividers = sortedPivotKeys.map((key, i) =>
- ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters }));
+ ({
+ type: "div", color: "lightGray",
+ x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2,
+ y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander,
+ height: maxColHeight,
+ payload: pivotColumnGroups.get(key)!.filters
+ }));
groupNames.push(...dividers);
return normalizeResults(panelDim, max_text, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
}
@@ -402,7 +410,7 @@ function normalizeResults(
const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
const docEles = Array.from(docMap.entries()).map(ele => ele[1]);
const aggBounds = aggregateBounds(extras.concat(grpEles.concat(docEles.map(de => ({ ...de, type: "doc", payload: "" })))).filter(e => e.zIndex !== -99), 0, 0);
- aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
+ aggBounds.r = aggBounds.x + Math.max(minWidth, aggBounds.r - aggBounds.x);
const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
if (Number.isNaN(scale)) scale = 1;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 239aacd4f..ec2fa8d63 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -9,6 +9,7 @@ import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fiel
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
+import { ImageField } from "../../../../fields/URLField";
import { createSchema, listSpec } from "../../../../fields/Schema";
import { ScriptField } from "../../../../fields/ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
@@ -41,6 +42,8 @@ import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDo
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { PresBox } from "../../nodes/trails/PresBox";
+import { VideoBox } from "../../nodes/VideoBox";
+import { CreateImage } from "../../nodes/WebBoxRenderer";
import { StyleLayers, StyleProp } from "../../StyleProvider";
import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
@@ -279,7 +282,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.context !== this.props.Document) { // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document
const source = Docs.Create.TextDocument("", { _width: 200, _height: 75, x: xp, y: yp, title: "dropped annotation" });
this.props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: linkDragData.linkSourceGetAnchor() }, "doc annotation", ""); // TODODO this is where in text links get passed
+ de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, "annotated by:annotation of", ""); // TODODO this is where in text links get passed
}
e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document
return true;
@@ -455,9 +458,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
e.preventDefault();
break;
case InkTool.None:
- this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
+ if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
+ this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ }
break;
}
}
@@ -935,8 +940,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (deltaScale * invTransform.Scale > 20) {
deltaScale = 20 / invTransform.Scale;
}
- if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) {
- deltaScale = 1 / invTransform.Scale;
+ if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
+ deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
}
const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
@@ -962,18 +967,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) {
- // if not presenting, just retrace the movements
- if (Doc.UserDoc()?.presentationMode === 'watching') {
- // RecordingApi.followMovements(presentation, this)
- return
- }
-
- if (Doc.UserDoc()?.presentationMode === 'recording') {
- // store as many movments as possible
- // this.storedMovements.push({time: new Date().getTime(), panX, panY})
- const err = RecordingApi.trackMovements(panX, panY)
- }
-
if (!this.isAnnotationOverlay && clamp) {
// this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds
const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc);
@@ -1000,8 +993,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || CurrentUserUtils.OverlayDocs.includes(this.Document)) {
this._viewTransition = panTime;
const scale = this.getLocalTransform().inverse().Scale;
- const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+ const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
+ const minPanX = NumCast(this.rootDoc._panXMin, 0);
+ const minPanY = NumCast(this.rootDoc._panYMin, 0);
+ const newPanX = Math.min(
+ minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX));
+ const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) :
+ minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight)), Math.max(minPanY, panY));
!this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
!this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
}
@@ -1027,8 +1025,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
} else if (doc.isInkMask) {
doc.zIndex = 5000;
} else {
- const docs = this.childLayoutPairs.map(pair => pair.layout);
- docs.slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
+ const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
+ docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
if (zlast - docs.length > 100) {
for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
@@ -1067,18 +1065,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
HistoryUtil.pushState(state);
}
}
- if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
- SelectionManager.DeselectAll();
- }
+ // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
+ // SelectionManager.DeselectAll();
+ // }
if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined) {
this.props.focus(doc, options);
} else {
const xfToCollection = options?.docTransform ?? Transform.Identity();
- const layoutdoc = Doc.Layout(doc);
const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey] };
const newState = HistoryUtil.getState();
const cantTransform = this.props.isAnnotationOverlay || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
- const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined);
+ const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || .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 };
HistoryUtil.pushState(newState);
@@ -1125,10 +1122,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
+ const layoutdoc = Doc.Layout(doc);
const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1);
const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1);
const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y));
- const pt2 = xf.transformPoint(NumCast(doc.x) + doc[WidthSym](), NumCast(doc.y) + doc[HeightSym]());
+ const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]());
const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1] };
const cx = NumCast(this.layoutDoc._panX);
const cy = NumCast(this.layoutDoc._panY);
@@ -1158,7 +1156,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- getChildDocView(entry: PoolData, renderIndex: number) {
+ getChildDocView(entry: PoolData) {
const childLayout = entry.pair.layout;
const childData = entry.pair.data;
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
@@ -1167,8 +1165,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Document={childLayout}
renderDepth={this.props.renderDepth + 1}
replica={entry.replica}
- renderIndex={renderIndex}
dataTransition={entry.transition}
+ suppressSetHeight={this.layoutEngine ? true : false}
renderCutoffProvider={this.renderCutoffProvider}
ContainingCollectionView={this.props.CollectionView}
ContainingCollectionDoc={this.props.Document}
@@ -1311,10 +1309,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return [] as ViewDefResult[];
}
+ @computed get layoutEngine() { return this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); }
@computed get doInternalLayoutComputation() {
TraceMobx();
const newPool = new Map<string, PoolData>();
- switch (this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine)) {
+ 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) };
@@ -1344,7 +1343,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const elements = computedElementData.slice();
Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach((entry, i) =>
elements.push({
- ele: this.getChildDocView(entry[1], i),
+ ele: this.getChildDocView(entry[1]),
bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica)
}));
@@ -1427,6 +1426,37 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}));
}
+ updateIcon = () => {
+ this.props.docViewPath().lastElement().ContentDiv!.style.width = (this.layoutDoc[WidthSym]()).toString();
+ this.props.docViewPath().lastElement().ContentDiv!.style.height = (this.layoutDoc[HeightSym]()).toString();
+ const htmlString = this._mainCont && new XMLSerializer().serializeToString(this.props.docViewPath().lastElement().ContentDiv!);
+ this.props.docViewPath().lastElement().ContentDiv!.style.width = "";
+ this.props.docViewPath().lastElement().ContentDiv!.style.height = "";
+ const nativeWidth = this.layoutDoc[WidthSym]();
+ const nativeHeight = this.layoutDoc[HeightSym]();
+
+ CreateImage(
+ "",
+ document.styleSheets,
+ htmlString?.replace(/docView-hack/g, 'documentView-hack'),
+ nativeWidth,
+ nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
+ NumCast(this.layoutDoc._scrollTop)
+ ).then
+ ((data_url: any) => {
+ VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then(
+ returnedfilename => setTimeout(action(() => {
+
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = nativeWidth;
+ this.dataDoc["icon-nativeHeight"] = nativeHeight;
+ }), 500));
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
+ }
+
componentWillUnmount() {
Object.values(this._disposers).forEach(disposer => disposer?.());
this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);
@@ -1492,6 +1522,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
appearanceItems.push({ description: "Reset View", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
!Doc.UserDoc().noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
appearanceItems.push({ description: `${this.fitToContent ? "Make Zoomable" : "Scale to Window"}`, event: () => this.Document._fitToBox = !this.fitToContent, icon: !this.fitToContent ? "expand-arrows-alt" : "compress-arrows-alt" });
+ appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
this.props.ContainingCollectionView &&
appearanceItems.push({ description: "Ungroup collection", event: this.promoteCollection, icon: "table" });
!Doc.UserDoc().noviceMode ? appearanceItems.push({ description: "Arrange contents in grid", event: this.layoutDocsInGrid, icon: "table" }) : null;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 62510ce9d..41e4d6b6a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -10,6 +10,7 @@
user-select: none;
}
+
.marqueeView:focus-within {
overflow: hidden;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b8f687489..5f303b428 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -519,15 +519,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.y = NumCast(d.y) - this.Bounds.top;
return d;
});
- const summary = Docs.Create.TextDocument("", { x: this.Bounds.left, y: this.Bounds.top, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
- const portal = Doc.MakeAlias(summary);
- Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected);
- Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations");
- summary._backgroundColor = "#e2ad32";
- portal.layoutKey = "layout_portal";
- portal.title = "document collection";
- DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing", "");
+ const summary = Docs.Create.TextDocument("", { _backgroundColor: "#e2ad32", x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
+ const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: "transparent" });
+ DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summary of:summarized by", "");
+ portal.hidden = true;
+ this.props.addDocument?.(portal);
this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
index a6171af51..845b07c51 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.scss
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -13,6 +13,10 @@
display: flex;
flex-direction: row;
+ .document-wrapper:hover {
+ z-index: 2000;
+ }
+
.react-grid-layout {
width: 100%;
}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 58ea7410d..da102fe18 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -166,7 +166,7 @@ export class CollectionGridView extends CollectionSubView() {
ScreenToLocalTransform={dxf}
onClick={this.onChildClickHandler}
renderDepth={this.props.renderDepth + 1}
- dontCenter={"y"}
+ dontCenter={this.props.Document.centerY ? "" : "y"}
/>;
}
@@ -283,6 +283,7 @@ export class CollectionGridView extends CollectionSubView() {
onContextMenu = () => {
const displayOptionsMenu: ContextMenuProps[] = [];
displayOptionsMenu.push({ description: "Toggle Content Display Style", event: () => this.props.Document.display = this.props.Document.display ? undefined : "contents", icon: "copy" });
+ displayOptionsMenu.push({ description: "Toggle Vertical Centering", event: () => this.props.Document.centerY = !this.props.Document.centerY, icon: "copy" });
ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" });
}
@@ -293,7 +294,7 @@ export class CollectionGridView extends CollectionSubView() {
if (this.props.isContentActive(true)) {
setupMoveUpEvents(this, e, returnFalse, returnFalse,
(e: PointerEvent, doubleTap?: boolean) => {
- if (doubleTap) {
+ if (doubleTap && !e.button) {
undoBatch(action(() => {
const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 });
FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index bc922be36..17d28e886 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -2,8 +2,7 @@ import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../fields/Doc";
import { LinkManager } from "../../util/LinkManager";
-import { DocumentLinksButton } from "../nodes/DocumentLinksButton";
-import { DocumentView, DocumentViewSharedProps } from "../nodes/DocumentView";
+import { DocumentView } from "../nodes/DocumentView";
import { LinkDocPreview } from "../nodes/LinkDocPreview";
import { LinkEditor } from "./LinkEditor";
import './LinkMenu.scss';
@@ -14,6 +13,7 @@ interface Props {
docView: DocumentView;
position?: { x?: number, y?: number };
itemHandler?: (doc: Doc) => void;
+ clearLinkEditor: () => void;
}
/**
@@ -21,7 +21,7 @@ interface Props {
*/
@observer
export class LinkMenu extends React.Component<Props> {
- private _editorRef = React.createRef<HTMLDivElement>();
+ _editorRef = React.createRef<HTMLDivElement>();
@observable _editingLink?: Doc;
@observable _linkMenuRef = React.createRef<HTMLDivElement>();
@@ -29,16 +29,21 @@ export class LinkMenu extends React.Component<Props> {
return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds());
}
+ clear = action(() => {
+ this.props.clearLinkEditor();
+ this._editingLink = undefined;
+ });
+
componentDidMount() { document.addEventListener("pointerdown", this.onPointerDown); }
componentWillUnmount() { document.removeEventListener("pointerdown", this.onPointerDown); }
- onPointerDown = (e: PointerEvent) => {
+ onPointerDown = action((e: PointerEvent) => {
LinkDocPreview.Clear();
if (!this._linkMenuRef.current?.contains(e.target as any) &&
!this._editorRef.current?.contains(e.target as any)) {
- DocumentLinksButton.ClearLinkEditor();
+ this.clear();
}
- }
+ });
/**
* maps each link to a JSX element to be rendered
@@ -54,6 +59,7 @@ export class LinkMenu extends React.Component<Props> {
sourceDoc={this.props.docView.props.Document}
group={group[1]}
groupType={group[0]}
+ clearLinkEditor={this.clear}
showEditor={action(linkDoc => this._editingLink = linkDoc)} />);
return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 5b1e29e67..fa6a2f506 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -13,6 +13,7 @@ interface LinkMenuGroupProps {
sourceDoc: Doc;
group: Doc[];
groupType: string;
+ clearLinkEditor: () => void;
showEditor: (linkDoc: Doc) => void;
docView: DocumentView;
itemHandler?: (doc: Doc) => void;
@@ -53,6 +54,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
linkDoc={linkDoc}
sourceDoc={this.props.sourceDoc}
destinationDoc={destination}
+ clearLinkEditor={this.props.clearLinkEditor}
showEditor={this.props.showEditor}
menuRef={this._menuRef} />;
}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index d6400a6b3..3ddcf803d 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -13,7 +13,6 @@ import { DragManager } from '../../util/DragManager';
import { Hypothesis } from '../../util/HypothesisUtils';
import { LinkManager } from '../../util/LinkManager';
import { undoBatch } from '../../util/UndoManager';
-import { DocumentLinksButton } from '../nodes/DocumentLinksButton';
import { DocumentView } from '../nodes/DocumentView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import './LinkMenuItem.scss';
@@ -26,6 +25,7 @@ interface LinkMenuItemProps {
docView: DocumentView;
sourceDoc: Doc;
destinationDoc: Doc;
+ clearLinkEditor: () => void;
showEditor: (linkDoc: Doc) => void;
menuRef: React.Ref<HTMLDivElement>;
itemHandler?: (doc: Doc) => void;
@@ -92,12 +92,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
const eleClone: any = this._drag.current!.cloneNode(true);
eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
StartLinkTargetsDrag(eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
- DocumentLinksButton.ClearLinkEditor();
+ this.props.clearLinkEditor();
return true;
},
emptyFunction,
() => {
- DocumentLinksButton.ClearLinkEditor();
+ this.props.clearLinkEditor();
if (this.props.itemHandler) {
this.props.itemHandler?.(this.props.linkDoc);
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 5149d370f..3f16d3c49 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, Opt } from "../../../fields/Doc";
import { List } from "../../../fields/List";
@@ -17,7 +17,6 @@ import { StyleProp } from "../StyleProvider";
import "./CollectionFreeFormDocumentView.scss";
import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { DocumentType } from "../../documents/DocumentTypes";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined;
@@ -29,7 +28,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
jitterRotation: number;
dataTransition?: string;
replica: string;
- renderIndex: number;
CollectionFreeFormView: CollectionFreeFormView;
}
@@ -167,16 +165,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelHeight: this.panelHeight,
};
const background = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && DashColor(background).alpha() !== 1 ? "multiply" : undefined);
+ const mixBlendMode = StrCast(this.layoutDoc.mixBlendMode) as any || (typeof background === "string" && background && (!background.startsWith("linear") && DashColor(background).alpha() !== 1) ? "multiply" : undefined);
return <div className={"collectionFreeFormDocumentView-container"}
style={{
outline: this.Highlight ? "orange solid 2px" : "",
width: this.panelWidth(),
- height: !this.ShowTitle && this.layoutDoc.autoHeight && this.rootDoc.type === DocumentType.RTF ? undefined : this.panelHeight(),
+ height: this.panelHeight(),
transform: this.transform,
transition: this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition),
zIndex: this.ZInd,
- mixBlendMode,
+ mixBlendMode: mixBlendMode,
display: this.ZInd === -99 ? "none" : undefined
}} >
{this.props.renderCutoffProvider(this.props.Document) ?
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index da9b448ec..70732e74c 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -113,7 +113,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & Fo
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
scaling?: () => number,
- setHeight: (height: number) => void,
+ setHeight?: (height: number) => void,
layoutKey: string,
}> {
@computed get layout(): string {
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 433a0bf48..a9c998757 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -1,3 +1,4 @@
+
import { observer } from "mobx-react";
import * as React from "react";
import { DocumentView } from "./DocumentView";
diff --git a/src/client/views/nodes/DocumentLinksButton.scss b/src/client/views/nodes/DocumentLinksButton.scss
index 9ab3171d3..0f3eb14bc 100644
--- a/src/client/views/nodes/DocumentLinksButton.scss
+++ b/src/client/views/nodes/DocumentLinksButton.scss
@@ -2,7 +2,9 @@
.documentLinksButton-wrapper {
transform-origin: top left;
+ width: 100%;
}
+
.documentLinksButton-menu {
width: 100%;
height: 100%;
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 6d96a9ab6..7f69adf6c 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -1,26 +1,23 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
-import { action, computed, observable, runInAction, trace } from "mobx";
+import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { ScriptField } from "../../../fields/ScriptField";
import { StrCast } from "../../../fields/Types";
import { TraceMobx } from "../../../fields/util";
import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { Docs, DocUtils } from "../../documents/Documents";
+import { DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { Hypothesis } from "../../util/HypothesisUtils";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { Colors } from "../global/globalEnums";
-import { LightboxView } from "../LightboxView";
import './DocumentLinksButton.scss';
import { DocumentView } from "./DocumentView";
import { LinkDescriptionPopup } from "./LinkDescriptionPopup";
import { TaskCompletionBox } from "./TaskCompletedBox";
import React = require("react");
+import { DocumentType } from "../../documents/DocumentTypes";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -46,8 +43,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
@observable public static invisibleWebDoc: Opt<Doc>;
public static invisibleWebRef = React.createRef<HTMLDivElement>();
- @action public static ClearLinkEditor() { DocumentLinksButton.LinkEditorDocView = undefined; }
-
@action @undoBatch
onLinkButtonMoved = (e: PointerEvent) => {
if (this.props.InMenu && this.props.StartLink) {
@@ -164,7 +159,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
} else if (startLink !== endLink) {
endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
- const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : "link", undefined, undefined, true);
+ const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink },
+ DocumentLinksButton.AnnotationId ? "hypothes.is annotation" : undefined, undefined, undefined, true);
LinkManager.currentLink = linkDoc;
@@ -248,9 +244,9 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
</div>
:
- <div className="documentLinksButton-menu" ref={this._linkButton}>
+ <div className="documentLinksButton-menu" >
{this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? //if the origin node is not this node
- <div className={"documentLinksButton-endLink"}
+ <div className={"documentLinksButton-endLink"} ref={this._linkButton}
onPointerDown={DocumentLinksButton.StartLink && this.completeLink}
onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
@@ -259,7 +255,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
}
{
this.props.InMenu && this.props.StartLink ? //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
- <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+ <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton}
+ onPointerDown={isActive ? undefined : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
:
@@ -278,7 +275,11 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
//render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu
return (!Array.from(this.filteredLinks).length && !this.props.AlwaysOn) ? (null) :
- <div className="documentLinksButton-wrapper" >
+ <div className="documentLinksButton-wrapper"
+ style={{
+ transform: this.props.InMenu ? undefined :
+ `scale(${(this.props.ContentScaling?.() || 1) * this.props.View.screenToLocalTransform().Scale})`
+ }} >
{
(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) ||
(!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ?
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4565f8504..5c5d2df10 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -3,6 +3,9 @@
.documentView-effectsWrapper {
border-radius: inherit;
}
+.documentView-hack {
+ display: inline;
+}
.documentView-customBorder {
width:100%;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3f1a703bf..f48173975 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -53,1310 +53,1330 @@ import { DocServer } from "../../DocServer";
const { Howl } = require('howler');
interface Window {
- MediaRecorder: MediaRecorder;
+ MediaRecorder: MediaRecorder;
}
declare class MediaRecorder {
- // whatever MediaRecorder has
- constructor(e: any);
+ // whatever MediaRecorder has
+ constructor(e: any);
}
export enum ViewAdjustment {
- resetView = 1,
- doNothing = 0
+ resetView = 1,
+ doNothing = 0
}
export const ViewSpecPrefix = "viewSpec"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
export interface DocFocusOptions {
- originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
- willZoom?: boolean; // determines whether to zoom in on target document
- scale?: number; // percent of containing frame to zoom into document
- afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
- docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
- instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+ originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
+ willZoom?: boolean; // determines whether to zoom in on target document
+ scale?: number; // percent of containing frame to zoom into document
+ afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
+ docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
+ instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
}
export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void;
export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
export interface DocComponentView {
- getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
- scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
- setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
- reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
- shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
- menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
- isAnyChildContentActive?: () => boolean; // is any child content of the document active
- getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
- setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
- playFrom?: (time: number, endTime?: number) => void;
- Pause?: () => void;
- setFocus?: () => void;
- componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
- fieldKey?: string;
- annotationKey?: string;
- getTitle?: () => string;
- getScrollHeight?: () => number;
- getCenter?: (xf: Transform) => { X: number, Y: number };
- ptToScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number };
- ptFromScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number };
- snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: number };
- search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
+ updateIcon?: () => void; // updates the icon representation of the document
+ getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+ scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
+ setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+ reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
+ shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
+ menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
+ isAnyChildContentActive?: () => boolean; // is any child content of the document active
+ getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+ setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
+ playFrom?: (time: number, endTime?: number) => void;
+ Pause?: () => void;
+ setFocus?: () => void;
+ componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+ fieldKey?: string;
+ annotationKey?: string;
+ getTitle?: () => string;
+ getScrollHeight?: () => number;
+ getCenter?: (xf: Transform) => { X: number, Y: number };
+ ptToScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number };
+ ptFromScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number };
+ snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: number };
+ search?: (str: string, bwd?: boolean, clear?: boolean) => boolean;
}
+// These props are passed to both FieldViews and DocumentViews
export interface DocumentViewSharedProps {
- renderDepth: number;
- Document: Doc;
- DataDoc?: Doc;
- fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document
- ContainingCollectionView: Opt<CollectionView>;
- ContainingCollectionDoc: Opt<Doc>;
- thumbShown?: () => boolean;
- isHovering?: () => boolean;
- setContentView?: (view: DocComponentView) => any;
- CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
- PanelWidth: () => number;
- PanelHeight: () => number;
- docViewPath: () => DocumentView[];
- dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
- layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean);
- styleProvider: Opt<StyleProviderFunc>;
- focus: DocFocusFunc;
- fitWidth?: (doc: Doc) => boolean;
- docFilters: () => string[];
- docRangeFilters: () => string[];
- searchFilterDocs: () => Doc[];
- showTitle?: () => string;
- whenChildContentsActiveChanged: (isActive: boolean) => void;
- rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
- addDocTab: (doc: Doc, where: string) => boolean;
- filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
- addDocument?: (doc: Doc | Doc[]) => boolean;
- removeDocument?: (doc: Doc | Doc[]) => boolean;
- moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- pinToPres: (document: Doc) => void;
- ScreenToLocalTransform: () => Transform;
- bringToFront: (doc: Doc, sendToBack?: boolean) => void;
- dropAction?: dropActionType;
- dontRegisterView?: boolean;
- hideLinkButton?: boolean;
- hideCaptions?: boolean;
- ignoreAutoHeight?: boolean;
- forceAutoHeight?: boolean;
- disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
- pointerEvents?: string;
- scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
- createNewFilterDoc?: () => void;
- updateFilterDoc?: (doc: Doc) => void;
+ fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews
+ DocumentView?: () => DocumentView;
+ renderDepth: number;
+ Document: Doc;
+ DataDoc?: Doc;
+ fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document
+ ContainingCollectionView: Opt<CollectionView>;
+ ContainingCollectionDoc: Opt<Doc>;
+ suppressSetHeight?: boolean;
+ thumbShown?: () => boolean;
+ isHovering?: () => boolean;
+ setContentView?: (view: DocComponentView) => any;
+ CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ docViewPath: () => DocumentView[];
+ dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
+ layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean);
+ styleProvider: Opt<StyleProviderFunc>;
+ focus: DocFocusFunc;
+ fitWidth?: (doc: Doc) => boolean;
+ docFilters: () => string[];
+ docRangeFilters: () => string[];
+ searchFilterDocs: () => Doc[];
+ showTitle?: () => string;
+ whenChildContentsActiveChanged: (isActive: boolean) => void;
+ rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
+ addDocTab: (doc: Doc, where: string) => boolean;
+ filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
+ addDocument?: (doc: Doc | Doc[]) => boolean;
+ removeDocument?: (doc: Doc | Doc[]) => boolean;
+ moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+ pinToPres: (document: Doc) => void;
+ ScreenToLocalTransform: () => Transform;
+ bringToFront: (doc: Doc, sendToBack?: boolean) => void;
+ dropAction?: dropActionType;
+ dontRegisterView?: boolean;
+ hideLinkButton?: boolean;
+ hideCaptions?: boolean;
+ ignoreAutoHeight?: boolean;
+ forceAutoHeight?: boolean;
+ disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
+ pointerEvents?: string;
+ scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
+ createNewFilterDoc?: () => void;
+ updateFilterDoc?: (doc: Doc) => void;
}
+
+// these props are specific to DocuentViews
export interface DocumentViewProps extends DocumentViewSharedProps {
- // properties specific to DocumentViews but not to FieldView
- freezeDimensions?: boolean;
- hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
- hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
- hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
- treeViewDoc?: Doc;
- isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
- isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
- contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
- radialMenu?: String[];
- LayoutTemplateString?: string;
- dontCenter?: "x" | "y" | "xy";
- ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal
- NativeWidth?: () => number;
- NativeHeight?: () => number;
- LayoutTemplate?: () => Opt<Doc>;
- contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[];
- onClick?: () => ScriptField;
- onDoubleClick?: () => ScriptField;
- onPointerDown?: () => ScriptField;
- onPointerUp?: () => ScriptField;
+ // properties specific to DocumentViews but not to FieldView
+ freezeDimensions?: boolean;
+ hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
+ hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
+ hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
+ treeViewDoc?: Doc;
+ isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
+ isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
+ contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
+ radialMenu?: String[];
+ LayoutTemplateString?: string;
+ dontCenter?: "x" | "y" | "xy";
+ ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal
+ NativeWidth?: () => number;
+ NativeHeight?: () => number;
+ LayoutTemplate?: () => Opt<Doc>;
+ contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[];
+ onClick?: () => ScriptField;
+ onDoubleClick?: () => ScriptField;
+ onPointerDown?: () => ScriptField;
+ onPointerUp?: () => ScriptField;
}
+// these props are only available in DocumentViewIntenral
export interface DocumentViewInternalProps extends DocumentViewProps {
- NativeWidth: () => number;
- NativeHeight: () => number;
- isSelected: (outsideReaction?: boolean) => boolean;
- select: (ctrlPressed: boolean) => void;
- DocumentView: () => DocumentView;
- viewPath: () => DocumentView[];
+ NativeWidth: () => number;
+ NativeHeight: () => number;
+ isSelected: (outsideReaction?: boolean) => boolean;
+ select: (ctrlPressed: boolean) => void;
+ DocumentView: () => DocumentView;
+ viewPath: () => DocumentView[];
}
@observer
export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
- public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
- _animateScaleTime = 300; // milliseconds;
- @observable _animateScalingTo = 0;
- @observable _mediaState = 0;
- @observable _pendingDoubleClick = false;
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private _downX: number = 0;
- private _downY: number = 0;
- private _firstX: number = -1;
- private _firstY: number = -1;
- private _lastTap: number = 0;
- private _doubleTap = false;
- private _mainCont = React.createRef<HTMLDivElement>();
- private _titleRef = React.createRef<EditableView>();
- private _timeout: NodeJS.Timeout | undefined;
- private _dropDisposer?: DragManager.DragDropDisposer;
- private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
- protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
- @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
-
- private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; }
- public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
- public get ContentDiv() { return this._mainCont.current; }
- public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); }
- @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); }
- @computed get ContentScale() { return this.props.ContentScaling?.() || 1; }
- @computed get thumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); }
- @computed get hidden() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Hidden); }
- @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); }
- @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); }
- @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); }
- @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); }
- @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ":selected" : "")); }
- @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); }
- @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); }
- @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; }
- @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; }
- @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); }
- @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); }
- @computed get nativeWidth() { return this.props.NativeWidth(); }
- @computed get nativeHeight() { return this.props.NativeHeight(); }
- @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); }
- @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); }
- @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); }
- @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); }
-
-
- componentWillUnmount() { this.cleanupHandlers(true); }
- componentDidMount() { this.setupHandlers(); }
- //componentDidUpdate() { this.setupHandlers(); }
- setupHandlers() {
- this.cleanupHandlers(false);
- if (this._mainCont.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document);
- this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this));
- this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
- }
- }
- cleanupHandlers(unbrush: boolean) {
- this._dropDisposer?.();
- this._multiTouchDisposer?.();
- this._holdDisposer?.();
- unbrush && Doc.UnBrushDoc(this.props.Document);
- Object.values(this._disposers).forEach(disposer => disposer?.());
- }
-
- handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
- this.removeMoveListeners();
- this.removeEndListeners();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- if (RadialMenu.Instance._display === false) {
- this.addHoldMoveListeners();
- this.addHoldEndListeners();
- this.onRadialMenu(e, me);
+ public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
+ _animateScaleTime = 300; // milliseconds;
+ @observable _animateScalingTo = 0;
+ @observable _mediaState = 0;
+ @observable _pendingDoubleClick = false;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _downX: number = 0;
+ private _downY: number = 0;
+ private _firstX: number = -1;
+ private _firstY: number = -1;
+ private _lastTap: number = 0;
+ private _doubleTap = false;
+ private _mainCont = React.createRef<HTMLDivElement>();
+ private _titleRef = React.createRef<EditableView>();
+ private _timeout: NodeJS.Timeout | undefined;
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+ @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
+
+ private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; }
+ public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
+ public get ContentDiv() { return this._mainCont.current; }
+ public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); }
+ @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt<string>); }
+ @computed get ContentScale() { return this.props.ContentScaling?.() || 1; }
+ @computed get thumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); }
+ @computed get hidden() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); }
+ @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); }
+ @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); }
+ @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); }
+ @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); }
+ @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ":selected" : "")); }
+ @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); }
+ @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); }
+ @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; }
+ @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; }
+ @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); }
+ @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); }
+ @computed get nativeWidth() { return this.props.NativeWidth(); }
+ @computed get nativeHeight() { return this.props.NativeHeight(); }
+ @computed get onClickHandler() { return this.props.onClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null)); }
+ @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); }
+ @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); }
+ @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); }
+
+
+ componentWillUnmount() { this.cleanupHandlers(true); }
+ componentDidMount() { this.setupHandlers(); }
+ //componentDidUpdate() { this.setupHandlers(); }
+ setupHandlers() {
+ this.cleanupHandlers(false);
+ if (this._mainCont.current) {
+ this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document);
+ this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this));
+ this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
+ }
+ }
+ cleanupHandlers(unbrush: boolean) {
+ this._dropDisposer?.();
+ this._multiTouchDisposer?.();
+ this._holdDisposer?.();
+ unbrush && Doc.UnBrushDoc(this.props.Document);
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ }
+
+ handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
+ this.removeMoveListeners();
+ this.removeEndListeners();
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ if (RadialMenu.Instance._display === false) {
+ this.addHoldMoveListeners();
+ this.addHoldEndListeners();
+ this.onRadialMenu(e, me);
+ const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
+ this._firstX = pt.pageX;
+ this._firstY = pt.pageY;
+ }
+ }
+
+ handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- this._firstX = pt.pageX;
- this._firstY = pt.pageY;
- }
- }
-
- handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-
- if (this._firstX === -1 || this._firstY === -1) {
- return;
- }
- if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
- this.handle1PointerHoldEnd(e, me);
- }
- }
-
- handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- this.removeHoldMoveListeners();
- this.removeHoldEndListeners();
- RadialMenu.Instance.closeMenu();
- this._firstX = -1;
- this._firstY = -1;
- SelectionManager.DeselectAll();
- me.touchEvent.stopPropagation();
- me.touchEvent.preventDefault();
- e.stopPropagation();
- if (RadialMenu.Instance.used) {
- this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
- }
- }
-
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (!e.nativeEvent.cancelBubble && !this.props.isSelected()) {
+
+ if (this._firstX === -1 || this._firstY === -1) {
+ return;
+ }
+ if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
+ this.handle1PointerHoldEnd(e, me);
+ }
+ }
+
+ handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
+ this.removeHoldMoveListeners();
+ this.removeHoldEndListeners();
+ RadialMenu.Instance.closeMenu();
+ this._firstX = -1;
+ this._firstY = -1;
+ SelectionManager.DeselectAll();
+ me.touchEvent.stopPropagation();
+ me.touchEvent.preventDefault();
e.stopPropagation();
- e.preventDefault();
+ if (RadialMenu.Instance.used) {
+ this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
+ }
+ }
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- }
- }
-
- handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- SelectionManager.DeselectAll();
- if (this.Document.onPointerDown) return;
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch) {
- this._downX = touch.clientX;
- this._downY = touch.clientY;
- if (!e.nativeEvent.cancelBubble) {
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation();
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
+ handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ if (!e.nativeEvent.cancelBubble && !this.props.isSelected()) {
+ e.stopPropagation();
+ e.preventDefault();
+
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
}
- }
- }
+ }
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (e.cancelBubble && this.props.isDocumentActive?.()) {
- this.removeMoveListeners();
- }
- else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
+ SelectionManager.DeselectAll();
+ if (this.Document.onPointerDown) return;
const touch = me.touchEvent.changedTouches.item(0);
- if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
- this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
- }
+ if (touch) {
+ this._downX = touch.clientX;
+ this._downY = touch.clientY;
+ if (!e.nativeEvent.cancelBubble) {
+ if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation();
+ this.removeMoveListeners();
+ this.addMoveListeners();
+ this.removeEndListeners();
+ this.addEndListeners();
+ e.stopPropagation();
+ }
}
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- }
-
- @action
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
- if (pinching !== 0 && oldPoint1 && oldPoint2) {
- const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX));
- const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY));
- const dX = -1 * Math.sign(dW);
- const dY = -1 * Math.sign(dH);
-
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(this.props.Document);
- const layoutDoc = Document(Doc.Layout(this.props.Document));
- let nwidth = Doc.NativeWidth(layoutDoc);
- let nheight = Doc.NativeHeight(layoutDoc);
- const width = (layoutDoc._width || 0);
- const height = (layoutDoc._height || (nheight / nwidth * width));
- const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale;
- const actualdW = Math.max(width + (dW * scale), 20);
- const actualdH = Math.max(height + (dH * scale), 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (nwidth && nheight);
- if (fixedAspect && (!nwidth || !nheight)) {
- Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0);
- Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0);
- }
- if (nwidth > 0 && nheight > 0) {
- if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) {
- Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc));
+ }
+
+ handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+ if (e.cancelBubble && this.props.isDocumentActive?.()) {
+ this.removeMoveListeners();
+ }
+ else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ const touch = me.touchEvent.changedTouches.item(0);
+ if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
+ if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
+ this.cleanUpInteractions();
+ this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
}
- layoutDoc._width = actualdW;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
- else layoutDoc._height = actualdH;
- }
- else {
- if (!fixedAspect) {
- Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc));
+ }
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+ }
+
+ @action
+ handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
+ const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
+ const pt1 = myTouches[0];
+ const pt2 = myTouches[1];
+ const oldPoint1 = this.prevPoints.get(pt1.identifier);
+ const oldPoint2 = this.prevPoints.get(pt2.identifier);
+ const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
+ if (pinching !== 0 && oldPoint1 && oldPoint2) {
+ const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX));
+ const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY));
+ const dX = -1 * Math.sign(dW);
+ const dY = -1 * Math.sign(dH);
+
+ if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
+ const doc = Document(this.props.Document);
+ const layoutDoc = Document(Doc.Layout(this.props.Document));
+ let nwidth = Doc.NativeWidth(layoutDoc);
+ let nheight = Doc.NativeHeight(layoutDoc);
+ const width = (layoutDoc._width || 0);
+ const height = (layoutDoc._height || (nheight / nwidth * width));
+ const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale;
+ const actualdW = Math.max(width + (dW * scale), 20);
+ const actualdH = Math.max(height + (dH * scale), 20);
+ doc.x = (doc.x || 0) + dX * (actualdW - width);
+ doc.y = (doc.y || 0) + dY * (actualdH - height);
+ const fixedAspect = e.ctrlKey || (nwidth && nheight);
+ if (fixedAspect && (!nwidth || !nheight)) {
+ Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0);
+ Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0);
+ }
+ if (nwidth > 0 && nheight > 0) {
+ if (Math.abs(dW) > Math.abs(dH)) {
+ if (!fixedAspect) {
+ Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc));
+ }
+ layoutDoc._width = actualdW;
+ if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width;
+ else layoutDoc._height = actualdH;
+ }
+ else {
+ if (!fixedAspect) {
+ Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc));
+ }
+ layoutDoc._height = actualdH;
+ if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
+ else layoutDoc._width = actualdW;
+ }
+ } else {
+ dW && (layoutDoc._width = actualdW);
+ dH && (layoutDoc._height = actualdH);
+ dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
}
- layoutDoc._height = actualdH;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height;
- else layoutDoc._width = actualdW;
- }
- } else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
- }
+ }
+ e.stopPropagation();
+ e.preventDefault();
}
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- @action
- onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
-
- // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 });
- const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 });
- // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 });
-
- SelectionManager.DeselectAll();
- }
-
- startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
- if (this._mainCont.current) {
- const dragData = new DragManager.DocumentDragData([this.props.Document]);
- const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0);
- dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top);
- dragData.dropAction = dropAction;
- dragData.treeViewDoc = this.props.treeViewDoc;
- dragData.removeDocument = this.props.removeDocument;
- dragData.moveDocument = this.props.moveDocument;
- const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
- DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) },
- () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed.
- ffview?.setupDragLines(false);
- }
- }
-
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey && !e.nativeEvent.cancelBubble) {
- e.stopPropagation();
- e.preventDefault();
- if (e.key === "†" || e.key === "t") {
- if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title";
- if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0);
- else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text...
- this._titleRef.current?.setIsFocused(false);
- this._componentView?.setFocus?.();
- }
+ }
+
+ @action
+ onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
+ const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
+ RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
+
+ // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 });
+ const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
+ (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 });
+ // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 });
+
+ SelectionManager.DeselectAll();
+ }
+
+ startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
+ if (this._mainCont.current) {
+ const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0);
+ dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top);
+ dragData.dropAction = dropAction;
+ dragData.treeViewDoc = this.props.treeViewDoc;
+ dragData.removeDocument = this.props.removeDocument;
+ dragData.moveDocument = this.props.moveDocument;
+ const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
+ DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) },
+ () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed.
+ ffview?.setupDragLines(false);
}
- }
- }
-
- 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))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => {
- this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]);
- });
- // after a timeout, the right _componentView should have been created, so call it to update its view spec values
- setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
- const focusSpeed = this._componentView?.scrollFocus?.(anchor, !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
- const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing;
- this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
- ...options, afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0))
- });
-
- }
- onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&
- (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
- let stopPropagate = true;
- let preventDefault = true;
- !StrListCast(this.props.Document._layerTags).includes(StyleLayers.Background) && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
- if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._pendingDoubleClick = false;
- this._timeout = undefined;
- }
- if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey } = e;
- const func = () => this.onDoubleClickHandler.script.run({
- this: this.layoutDoc,
- self: this.rootDoc,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX, clientY, shiftKey
- }, console.log);
- UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click");
- } else if (!Doc.IsSystem(this.rootDoc)) {
- UndoManager.RunInBatch(() =>
- LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.())
- , "double tap");
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
- }
- } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey } = e;
- const func = () => this.onClickHandler.script.run({
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX, clientY, shiftKey
- }, console.log).result?.select === true ? this.props.select(false) : "";
- const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click");
- if (this.onDoubleClickHandler) {
- runInAction(() => this._pendingDoubleClick = true);
- this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350);
- } else clickFunc();
- } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey);
- } else {
- if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
- stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
- } else {
- runInAction(() => this._pendingDoubleClick = true);
- this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350);
- this.props.select(e.ctrlKey || e.shiftKey);
- }
- preventDefault = false;
+ }
+
+ onKeyDown = (e: React.KeyboardEvent) => {
+ if (e.altKey && !e.nativeEvent.cancelBubble) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (e.key === "†" || e.key === "t") {
+ if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title";
+ if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0);
+ else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text...
+ this._titleRef.current?.setIsFocused(false);
+ this._componentView?.setFocus?.();
+ }
+ }
}
- stopPropagate && e.stopPropagation();
- preventDefault && e.preventDefault();
- }
- });
-
- onPointerDown = (e: React.PointerEvent): void => {
- // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
- if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) {
- if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- e.stopPropagation();
- if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
- // TODO: check here for panning/inking
+ }
+
+ 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))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => {
+ this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]);
+ });
+ // after a timeout, the right _componentView should have been created, so call it to update its view spec values
+ setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
+ const focusSpeed = this._componentView?.scrollFocus?.(anchor, !options?.instant || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
+ const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing;
+ this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
+ ...options, afterFocus: (didFocus: boolean) =>
+ new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0))
+ });
+
+ }
+ onClick = action((e: React.MouseEvent | React.PointerEvent) => {
+ if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 &&
+ (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
+ let stopPropagate = true;
+ let preventDefault = true;
+ !StrListCast(this.props.Document._layerTags).includes(StyleLayers.Background) && (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
+ if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
+ if (this._timeout) {
+ clearTimeout(this._timeout);
+ this._pendingDoubleClick = false;
+ this._timeout = undefined;
+ }
+ if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
+ const { clientX, clientY, shiftKey } = e;
+ const func = () => this.onDoubleClickHandler.script.run({
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ thisContainer: this.props.ContainingCollectionDoc,
+ documentView: this.props.DocumentView(),
+ clientX, clientY, shiftKey
+ }, console.log);
+ UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click");
+ } else if (!Doc.IsSystem(this.rootDoc)) {
+ UndoManager.RunInBatch(() =>
+ LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.())
+ , "double tap");
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
+ }
+ } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself
+ const { clientX, clientY, shiftKey } = e;
+ const func = () => this.onClickHandler.script.run({
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ _readOnly_: false,
+ scriptContext: this.props.scriptContext,
+ thisContainer: this.props.ContainingCollectionDoc,
+ documentView: this.props.DocumentView(),
+ clientX, clientY, shiftKey
+ }, console.log).result?.select === true ? this.props.select(false) : "";
+ const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click");
+ if (this.onDoubleClickHandler) {
+ runInAction(() => this._pendingDoubleClick = true);
+ this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350);
+ } else clickFunc();
+ } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+ this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey);
+ } else {
+ if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
+ stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ } else {
+ runInAction(() => this._pendingDoubleClick = true);
+ this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350);
+ this.props.select(e.ctrlKey || e.shiftKey);
+ }
+ preventDefault = false;
+ }
+ stopPropagate && e.stopPropagation();
+ preventDefault && e.preventDefault();
}
- return;
- }
- this._downX = e.clientX;
- this._downY = e.clientY;
- if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) &&
- // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
- !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
- !this.Document.ignoreClick &&
- !e.ctrlKey &&
- (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
- !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
- e.stopPropagation();
- // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though
- //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+ });
+
+ onPointerDown = (e: React.PointerEvent): void => {
+ // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
+ if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) {
+ if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ e.stopPropagation();
+ if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
+ // TODO: check here for panning/inking
+ }
+ return;
}
- if (this.props.isDocumentActive?.()) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) &&
+ // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
+ !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
+ if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ !this.Document.ignoreClick &&
+ !e.ctrlKey &&
+ (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
+ !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ e.stopPropagation();
+ // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though
+ //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+ }
+ if (this.props.isDocumentActive?.()) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ }
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointerup", this.onPointerUp);
}
+ }
+
+ onPointerMove = (e: PointerEvent): void => {
+ if (e.cancelBubble) return;
+ if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) return;
+
+ if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
+ if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+ if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType);
+ }
+ }
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+ }
+
+ cleanupPointerEvents = () => {
+ this.cleanUpInteractions();
+ document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- }
- }
-
- onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) return;
- if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool))) return;
-
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) {
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType);
- }
+ }
+
+ onPointerUp = (e: PointerEvent): void => {
+ this.cleanupPointerEvents();
+
+ if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
+ } else {
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
+ if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected
+ }
+ }
+
+ @undoBatch @action
+ toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => {
+ this.Document.ignoreClick = false;
+ if (setPushpin) {
+ this.Document.isPushpin = !this.Document.isPushpin;
+ this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton;
+ } else {
+ this.Document._isLinkButton = !this.Document._isLinkButton;
}
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- }
-
- cleanupPointerEvents = () => {
- this.cleanUpInteractions();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- onPointerUp = (e: PointerEvent): void => {
- this.cleanupPointerEvents();
-
- if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
- } else {
- this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
- // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
- if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected
- }
- }
-
- @undoBatch @action
- toggleFollowLink = (location: Opt<string>, zoom: boolean, setPushpin: boolean): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = !this.Document._isLinkButton;
- setPushpin && (this.Document.isPushpin = this.Document._isLinkButton);
- if (this.Document._isLinkButton && !this.onClickHandler) {
+ if (this.Document._isLinkButton && !this.onClickHandler) {
+ this.Document.followLinkZoom = zoom;
+ this.Document.followLinkLocation = location;
+ } else if (this.Document._isLinkButton && this.onClickHandler) {
+ this.Document._isLinkButton = false;
+ this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
+ }
+ }
+ @undoBatch @action
+ toggleTargetOnClick = (): void => {
+ this.Document.ignoreClick = false;
+ this.Document._isLinkButton = true;
+ this.Document.isPushpin = true;
+ }
+ @undoBatch @action
+ followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => {
+ this.Document.ignoreClick = false;
+ this.Document._isLinkButton = true;
+ this.Document.isPushpin = false;
this.Document.followLinkZoom = zoom;
this.Document.followLinkLocation = location;
- } else if (this.Document._isLinkButton && this.onClickHandler) {
+ }
+ @undoBatch @action
+ selectOnClick = (): void => {
+ this.Document.ignoreClick = false;
this.Document._isLinkButton = false;
- this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
- }
- @undoBatch @action
- toggleTargetOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.isPushpin = true;
- }
- @undoBatch @action
- followLinkOnClick = (location: Opt<string>, zoom: boolean,): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.isPushpin = false;
- this.Document.followLinkZoom = zoom;
- this.Document.followLinkLocation = location;
- }
- @undoBatch @action
- selectOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
- this.Document.isPushpin = false;
- this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
- @undoBatch
- noOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
- }
-
- @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
- @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" });
-
- @undoBatch @action
- drop = async (e: Event, de: DragManager.DropEvent) => {
- if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
- if (this.props.Document === CurrentUserUtils.ActiveDashboard) {
- alert((e.target as any)?.closest?.("*.lm_content") ?
- "You can't perform this move most likely because you don't have permission to modify the destination." :
- "Linking to document tabs not yet supported. Drop link on document content.");
- return;
- }
- const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
- if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
- if (linkdrag?.linkSourceDoc) {
- e.stopPropagation();
- if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
- de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
- }
- if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
- const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document;
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, "link", undefined, undefined, undefined, [de.x, de.y]);
- }
- }
- }
-
- @undoBatch
- @action
- makeIntoPortal = async () => {
- const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
- if (!portalLink) {
- const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" });
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to");
- }
- this.Document.followLinkLocation = "inPlace";
- this.Document.followLinkZoom = true;
- this.Document._isLinkButton = true;
- }
-
- @action
- onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
- if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) {
- e.preventDefault();
- e.stopPropagation();
- //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false);
- }
- // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
- if (e) {
- if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) {
- e.preventDefault();
- return;
+ this.Document.isPushpin = false;
+ this.Document.onClick = this.layoutDoc.onClick = undefined;
+ }
+ @undoBatch
+ noOnClick = (): void => {
+ this.Document.ignoreClick = false;
+ this.Document._isLinkButton = false;
+ }
+
+ @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
+ @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" });
+
+ @undoBatch @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
+ if (this.props.Document === CurrentUserUtils.ActiveDashboard) {
+ alert((e.target as any)?.closest?.("*.lm_content") ?
+ "You can't perform this move most likely because you don't have permission to modify the destination." :
+ "Linking to document tabs not yet supported. Drop link on document content.");
+ return;
}
- e.preventDefault();
- e.stopPropagation();
- e.persist();
-
- if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
- return;
+ const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
+ if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+ if (linkdrag?.linkSourceDoc) {
+ e.stopPropagation();
+ if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+ de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+ }
+ if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
+ const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document;
+ de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y]);
+ }
}
- }
-
- const cm = ContextMenu.Instance;
- if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
-
- if (e && !(e.nativeEvent as any).dash) {
- const onDisplay = () => setTimeout(() => {
- DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
- setTimeout(() => {
- const ele = document.elementFromPoint(e.clientX, e.clientY);
- simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY);
- });
- });
- if (navigator.userAgent.includes("Macintosh")) {
- cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay);
+ }
+
+ @undoBatch
+ @action
+ makeIntoPortal = async () => {
+ const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
+ if (!portalLink) {
+ const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" });
+ DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to:portal from");
}
- else {
- onDisplay();
+ this.Document.followLinkLocation = "inPlace";
+ this.Document.followLinkZoom = true;
+ this.Document._isLinkButton = true;
+ }
+
+ @action
+ onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
+ if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) {
+ e.preventDefault();
+ e.stopPropagation();
+ //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false);
}
- return;
- }
-
- const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
- StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
- cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
- this.props.contextMenuItems?.().forEach(item =>
- item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp }));
-
- if (!this.props.Document.isFolder) {
- const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
- const appearance = cm.findByDescription("UI Controls...");
- const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : [];
- !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" });
- !Doc.UserDoc().noviceMode && appearanceItems.push({
- description: "Add a Field", event: () => {
- const alias = Doc.MakeAlias(this.rootDoc);
- alias.layout = FormattedTextBox.LayoutString("newfield");
- alias.title = "newfield";
- alias._yMargin = 10;
- alias._height = 35;
- alias._width = 100;
- alias.syncLayoutFieldWithTitle = true;
- alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
- alias.y = NumCast(this.rootDoc.y);
- this.props.addDocument?.(alias);
- }, icon: "eye"
- });
- DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" });
- !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" });
-
- if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
- !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" });
- const existingOnClick = cm.findByDescription("OnClick...");
- const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
-
- const zorders = cm.findByDescription("ZOrder...");
- const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : [];
- if (this.props.bringToFront !== emptyFunction) {
- zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" });
- zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" });
- zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" });
- }
- !zorders && cm.addItem({ description: "ZOrder...", subitems: zorderItems, icon: "compass" });
-
- !Doc.UserDoc().noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
- !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" });
- this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" });
-
- if (!this.Document.annotationOn) {
- const options = cm.findByDescription("Options...");
- const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
- !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" });
-
- onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
- onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" });
- !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" });
- onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" });
- onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" });
- onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" });
- !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" });
- } else if (DocListCast(this.Document.links).length) {
- onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" });
- onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" });
- onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" });
- !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" });
- }
+ // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
+ if (e) {
+ if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) {
+ e.preventDefault();
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ e.persist();
+
+ if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
+ return;
+ }
}
- const funcs: ContextMenuProps[] = [];
- if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) {
- funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
- funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
- funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined });
- cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" });
+ const cm = ContextMenu.Instance;
+ if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
+
+ if (e && !(e.nativeEvent as any).dash) {
+ const onDisplay = () => setTimeout(() => {
+ DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ setTimeout(() => {
+ const ele = document.elementFromPoint(e.clientX, e.clientY);
+ simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY);
+ });
+ });
+ if (navigator.userAgent.includes("Macintosh")) {
+ cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay);
+ }
+ else {
+ onDisplay();
+ }
+ return;
}
- const more = cm.findByDescription("More...");
- const moreItems = more && "subitems" in more ? more.subitems : [];
- if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" });
- if (!Doc.UserDoc().noviceMode) {
- moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
- moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" });
-
- if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
- moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
- moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
- moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
- }
- moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" });
- }
+ const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []);
+ StrListCast(this.Document.contextMenuLabels).forEach((label, i) =>
+ cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" }));
+ this.props.contextMenuItems?.().forEach(item =>
+ item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp }));
+
+ if (!this.props.Document.isFolder) {
+ const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+ const appearance = cm.findByDescription("UI Controls...");
+ const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : [];
+ !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" });
+ !Doc.UserDoc().noviceMode && appearanceItems.push({
+ description: "Add a Field", event: () => {
+ const alias = Doc.MakeAlias(this.rootDoc);
+ alias.layout = FormattedTextBox.LayoutString("newfield");
+ alias.title = "newfield";
+ alias._yMargin = 10;
+ alias._height = 35;
+ alias._width = 100;
+ alias.syncLayoutFieldWithTitle = true;
+ alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
+ alias.y = NumCast(this.rootDoc.y);
+ this.props.addDocument?.(alias);
+ }, icon: "eye"
+ });
+ DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" });
+ !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" });
+
+ if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
+ !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" });
+ const existingOnClick = cm.findByDescription("OnClick...");
+ const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
+
+ const zorders = cm.findByDescription("ZOrder...");
+ const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : [];
+ if (this.props.bringToFront !== emptyFunction) {
+ zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" });
+ zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" });
+ zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" });
+ }
+ !zorders && cm.addItem({ description: "ZOrder...", noexpand: true, subitems: zorderItems, icon: "compass" });
+
+ !Doc.UserDoc().noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
+ !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" });
+ this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+
+ if (!this.Document.annotationOn) {
+ const options = cm.findByDescription("Options...");
+ const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
+ !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" });
+
+ onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+ onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" });
+ !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" });
+ onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" });
+ onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" });
+ onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" });
+ !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" });
+ } else if (DocListCast(this.Document.links).length) {
+ onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" });
+ onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" });
+ onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" });
+ !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" });
+ }
+ }
+
+ const funcs: ContextMenuProps[] = [];
+ if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) {
+ funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
+ funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
+ funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined });
+ cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" });
+ }
+
+ const more = cm.findByDescription("More...");
+ const moreItems = more && "subitems" in more ? more.subitems : [];
+ if (!Doc.IsSystem(this.rootDoc)) {
+ (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" });
+ if (!Doc.UserDoc().noviceMode) {
+ moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+ moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" });
+
+ if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
+ moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
+ moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
+ moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
+ }
+ moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" });
+ }
+ }
+
+ if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
+ moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" });
+ }
+ !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" });
+
+ const help = cm.findByDescription("Help...");
+ const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
+ !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
+ helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
+ !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
+ !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" });
+ cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
}
- if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
- moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" });
- }
- !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" });
-
- const help = cm.findByDescription("Help...");
- const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : [];
- !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
- helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" });
- !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" });
- !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" });
- cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" });
- }
-
- if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
- cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
- }
-
- collectionFilters = () => StrListCast(this.props.Document._docFilters);
- collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
- @computed get showFilterIcon() {
- return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" :
- this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined;
- }
- rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
- panelHeight = () => this.props.PanelHeight() - this.headerMargin;
- screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
- contentScaling = () => this.ContentScale;
- onClickFunc = () => this.onClickHandler;
- setHeight = (height: number) => {
- if (this.props.renderDepth !== -1) {
- this.layoutDoc._height = height;
- }
- }
- setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
- isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() === false ? false : (
- CurrentUserUtils.SelectedTool !== InkTool.None ||
- SnappingManager.GetIsDragging() ||
- this.rootSelected() ||
- this.props.Document.forceActive ||
- this.props.isSelected(outsideReaction) ||
- this._componentView?.isAnyChildContentActive?.() ||
- this.props.isContentActive()) ? true : undefined;
- }
- @observable _retryThumb = 1;
- thumbShown = () => !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb &&
- !this._componentView?.isAnyChildContentActive?.() ? true : false
- @computed get contents() {
- TraceMobx();
- const audioView = !this.layoutDoc._showAudio ? (null) :
- <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter} >
- <FontAwesomeIcon className="documentView-audioFont"
- style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }}
- icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" />
+ if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
+ cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
+ }
+
+ collectionFilters = () => StrListCast(this.props.Document._docFilters);
+ collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
+ @computed get showFilterIcon() {
+ return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" :
+ this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined;
+ }
+ rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
+ panelHeight = () => this.props.PanelHeight() - this.headerMargin;
+ screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
+ contentScaling = () => this.ContentScale;
+ onClickFunc = () => this.onClickHandler;
+ setHeight = (height: number) => this.layoutDoc._height = height;
+ setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
+ isContentActive = (outsideReaction?: boolean) => {
+ return this.props.isContentActive() === false ? false : (
+ CurrentUserUtils.SelectedTool !== InkTool.None ||
+ SnappingManager.GetIsDragging() ||
+ this.rootSelected() ||
+ this.props.Document.forceActive ||
+ this.props.isSelected(outsideReaction) ||
+ this._componentView?.isAnyChildContentActive?.() ||
+ this.props.isContentActive()) ? true : undefined;
+ }
+ @observable _retryThumb = 1;
+ thumbShown = () => !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb &&
+ !this._componentView?.isAnyChildContentActive?.() ? true : false
+ @computed get contents() {
+ TraceMobx();
+ const audioView = !this.layoutDoc._showAudio ? (null) :
+ <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter} >
+ <FontAwesomeIcon className="documentView-audioFont"
+ style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._mediaState] }}
+ icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]).length ? "microphone" : "file-audio"} size="sm" />
+ </div>;
+
+ return <div className="documentView-contentsView"
+ style={{
+ pointerEvents: this.props.pointerEvents as any ?? this.rootDoc.layoutKey === "layout_icon" ? "none" : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"),
+ height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+ }}>
+ {!this._retryThumb || !this.thumbShown() ? (null) :
+ <img style={{ background: "white", top: 0, position: "absolute" }} src={this.thumb} // + '?d=' + (new Date()).getTime()}
+ width={this.props.PanelWidth()} height={this.props.PanelHeight()}
+ onError={(e: any) => {
+ setTimeout(action(() => this._retryThumb = 0), 0);
+ setTimeout(action(() => this._retryThumb = 1), 150);
+ }} />}
+ <DocumentContentsView key={1}
+ {...this.props}
+ docViewPath={this.props.viewPath}
+ thumbShown={this.thumbShown}
+ isHovering={this.isHovering}
+ setContentView={this.setContentView}
+ scaling={this.contentScaling}
+ PanelHeight={this.panelHeight}
+ setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
+ isContentActive={this.isContentActive}
+ ScreenToLocalTransform={this.screenToLocal}
+ rootSelected={this.rootSelected}
+ onClick={this.onClickFunc}
+ focus={this.focus}
+ layoutKey={this.finalLayoutKey} />
+ {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
+ {(!this.props.isSelected() && !this._isHovering) || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) :
+ <DocumentLinksButton View={this.props.DocumentView()}
+ ContentScaling={this.props.ContentScaling}
+ Offset={[this.topMost ? 0 : !this.props.isSelected() ? - 15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? - 15 : -30]} />
+ }
+ {audioView}
</div>;
- return <div className="documentView-contentsView"
- style={{
- pointerEvents: this.props.pointerEvents as any ? this.props.pointerEvents as any : (this.rootDoc.type !== DocumentType.INK && ((this.props.contentPointerEvents as any) || (this.isContentActive())) ? "all" : "none"),
- height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
- }}>
- {!this._retryThumb || !this.thumbShown() ? (null) :
- <img style={{ background: "white", top: 0, position: "absolute" }} src={this.thumb} // + '?d=' + (new Date()).getTime()}
- width={this.props.PanelWidth()} height={this.props.PanelHeight()}
- onError={(e: any) => {
- setTimeout(action(() => this._retryThumb = 0), 0);
- setTimeout(action(() => this._retryThumb = 1), 150);
- }} />}
- <DocumentContentsView key={1}
- {...this.props}
- docViewPath={this.props.viewPath}
- thumbShown={this.thumbShown}
- isHovering={this.isHovering}
- setContentView={this.setContentView}
- scaling={this.contentScaling}
- PanelHeight={this.panelHeight}
- setHeight={this.setHeight}
- isContentActive={this.isContentActive}
- ScreenToLocalTransform={this.screenToLocal}
- rootSelected={this.rootSelected}
- onClick={this.onClickFunc}
- focus={this.focus}
- layoutKey={this.finalLayoutKey} />
- {this.layoutDoc.hideAllLinks ? (null) : this.allLinkEndpoints}
- {!this.props.isSelected() || this.hideLinkButton || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() ? (null) :
- <DocumentLinksButton View={this.props.DocumentView()}
- ContentScaling={this.props.ContentScaling}
- Offset={[this.topMost ? 0 : -15, undefined, undefined, this.topMost ? 10 : -20]} />
+ }
+
+ get indicatorIcon() {
+ if (this.props.Document["acl-Public"] !== SharingPermissions.None) return "globe-americas";
+ else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return "users";
+ else return "user";
+ }
+
+ @undoBatch
+ hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true)
+ anchorPanelWidth = () => this.props.PanelWidth() || 1;
+ anchorPanelHeight = () => this.props.PanelHeight() || 1;
+ anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
+ switch (property) {
+ case StyleProp.ShowTitle: return "";
+ case StyleProp.PointerEvents: return "none";
+ case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox
+ default: return this.props.styleProvider?.(doc, props, property);
}
- {audioView}
- </div>;
- }
-
- get indicatorIcon() {
- if (this.props.Document["acl-Public"] !== SharingPermissions.None) return "globe-americas";
- else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return "users";
- else return "user";
- }
-
- @undoBatch
- hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true)
- anchorPanelWidth = () => this.props.PanelWidth() || 1;
- anchorPanelHeight = () => this.props.PanelHeight() || 1;
- anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
- switch (property) {
- case StyleProp.ShowTitle: return "";
- case StyleProp.PointerEvents: return "none";
- case StyleProp.LinkSource: return this.props.Document;// pass the LinkSource to the LinkAnchorBox
- default: return this.props.styleProvider?.(doc, props, property);
- }
- }
- // We need to use allrelatedLinks to get not just links to the document as a whole, but links to
- // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g.,
- // - PDF text regions are rendered as an Annotations without generating a DocumentView, '
- // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
- // - and links to PDF/Web docs at a certain scroll location never create an explicit view.
- // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
- @computed get directLinks() {
- TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(link =>
- Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) ||
- Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) ||
- ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) ||
- ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc))
- );
- }
- @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); }
- @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links
- TraceMobx();
- if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
- if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null);
- const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
- return filtered.map((link, i) =>
- <div className="documentView-anchorCont" key={link[Id]}>
- <DocumentView {...this.props}
- Document={link}
- PanelWidth={this.anchorPanelWidth}
- PanelHeight={this.anchorPanelHeight}
- dontRegisterView={false}
- showTitle={returnEmptyString}
- hideCaptions={true}
- fitWidth={returnTrue}
- styleProvider={this.anchorStyleProvider}
- removeDocument={this.hideLinkAnchor}
- LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} />
- </div >);
- }
-
- @action
- onPointerEnter = () => {
- const self = this;
- const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]);
- if (audioAnnos && audioAnnos.length && this._mediaState === 0) {
- const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
- anno.data instanceof AudioField && new Howl({
- src: [anno.data.url.href],
- format: ["mp3"],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => self._mediaState = 0);
- }
- });
- this._mediaState = 1;
- }
- }
- recordAudioAnnotation = () => {
- let gumStream: any;
- let recorder: any;
- const self = this;
- navigator.mediaDevices.getUserMedia({
- audio: true
- }).then(function (stream) {
- gumStream = stream;
- recorder = new MediaRecorder(stream);
- recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(e.data);
- if (!(result instanceof Error)) {
- const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 });
- audioDoc.treeViewExpandedView = "layout";
- const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc));
- if (audioAnnos === undefined) {
- self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]);
- } else {
- audioAnnos.push(audioDoc);
- }
- }
- };
- runInAction(() => self._mediaState = 2);
- recorder.start();
- setTimeout(() => {
- recorder.stop();
- runInAction(() => self._mediaState = 0);
- gumStream.getAudioTracks()[0].stop();
- }, 5000);
- });
- }
-
- captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption");
- @computed get innards() {
- TraceMobx();
- const ffscale = () => (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1);
- const showTitle = this.ShowTitle?.split(":")[0];
- const showTitleHover = this.ShowTitle?.includes(":hover");
- const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
- const captionView = !showCaption ? (null) :
- <div className="documentView-captionWrapper"
- style={{
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
- minWidth: 50 * ffscale(),
- maxHeight: `max(100%, ${20 * ffscale()}px)`
- }}>
- <FormattedTextBox {...OmitKeys(this.props, ['children']).omit}
- yPadding={10}
- xPadding={10}
- fieldKey={showCaption}
- fontSize={12 * Math.max(1, 2 * ffscale() / 3)}
- styleProvider={this.captionStyleProvider}
- dontRegisterView={true}
- noSidebar={true}
- dontScale={true}
- isContentActive={this.isContentActive}
- onClick={this.onClickFunc}
- />
- </div>;
- const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc);
- const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)");
- const titleView = !showTitle ? (null) :
- <div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} key="title" style={{
- position: this.headerMargin ? "relative" : "absolute",
- height: this.titleHeight,
- color: lightOrDark(background),
- background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
- }}>
- <EditableView ref={this._titleRef}
- contents={showTitle.split(";").map(field => field.trim()).map(field => targetDoc[field]?.toString()).join("\\")}
- display={"block"}
- 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 {
- 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);
+ }
+ // We need to use allrelatedLinks to get not just links to the document as a whole, but links to
+ // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g.,
+ // - PDF text regions are rendered as an Annotations without generating a DocumentView, '
+ // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
+ // - and links to PDF/Web docs at a certain scroll location never create an explicit view.
+ // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
+ @computed get directLinks() {
+ TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(link =>
+ Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) ||
+ Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) ||
+ ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) ||
+ ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc))
+ );
+ }
+ @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); }
+ @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links
+ TraceMobx();
+ if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
+ if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null);
+ const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
+ return filtered.map((link, i) =>
+ <div className="documentView-anchorCont" key={link[Id]}>
+ <DocumentView {...this.props}
+ Document={link}
+ PanelWidth={this.anchorPanelWidth}
+ PanelHeight={this.anchorPanelHeight}
+ dontRegisterView={false}
+ showTitle={returnEmptyString}
+ hideCaptions={true}
+ fitWidth={returnTrue}
+ styleProvider={this.anchorStyleProvider}
+ removeDocument={this.hideLinkAnchor}
+ LayoutTemplate={undefined}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} />
+ </div >);
+ }
+
+ @action
+ onPointerEnter = () => {
+ const self = this;
+ const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]);
+ if (audioAnnos && audioAnnos.length && this._mediaState === 0) {
+ const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
+ anno.data instanceof AudioField && new Howl({
+ src: [anno.data.url.href],
+ format: ["mp3"],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: function () {
+ runInAction(() => self._mediaState = 0);
}
- return true;
- })}
- />
- </div>;
- return this.props.hideTitle || (!showTitle && !showCaption) ?
- this.contents :
- <div className="documentView-styleWrapper" >
- {!this.headerMargin ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>}
- {captionView}
- </div>;
- }
- isHovering = () => this._isHovering;
- @observable _isHovering = false;
- @observable _: string = "";
- @computed get renderDoc() {
- TraceMobx();
- const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png");
- const isButton = this.props.Document.type === DocumentType.FONTICON;
- if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
- return this.docContents ??
- <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
- id={this.props.Document[Id]}
- onPointerEnter={action(() => this._isHovering = true)}
- onPointerLeave={action(() => this._isHovering = false)}
- style={{
- background: isButton || thumb ? undefined : this.backgroundColor,
- opacity: this.opacity,
- color: StrCast(this.layoutDoc.color, "inherit"),
- fontFamily: StrCast(this.Document._fontFamily, "inherit"),
- fontSize: Cast(this.Document._fontSize, "string", null),
- transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`,
- }}>
-
- {this.innards}
- {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : (null)}
- {this.widgetDecorations ?? null}
- </div>;
- }
- render() {
- TraceMobx();
- const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
- const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "orange"][highlightIndex];
- const highlightStyle = ["solid", "dashed", "solid", "solid"][highlightIndex];
- const excludeTypes = !this.props.treeViewDoc && highlightIndex < 3 ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
- let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
- highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
-
- const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
- const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
- const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
- this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
-
- // Return surrounding highlight
- return <div className={DocumentView.ROOT_DIV} ref={this._mainCont}
- onContextMenu={this.onContextMenu}
- onKeyDown={this.onKeyDown}
- onPointerDown={this.onPointerDown}
- onClick={this.onClick}
- onPointerEnter={e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document)}
- onPointerLeave={e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document)}
- style={{
- borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents,
- outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px",
- border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
- boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined
- }}>
- {!borderPath.path ? internal :
- <>
- {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
+ });
+ this._mediaState = 1;
+ }
+ }
+ recordAudioAnnotation = () => {
+ let gumStream: any;
+ let recorder: any;
+ const self = this;
+ navigator.mediaDevices.getUserMedia({
+ audio: true
+ }).then(function (stream) {
+ gumStream = stream;
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer(e.data);
+ if (!(result instanceof Error)) {
+ const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 });
+ audioDoc.treeViewExpandedView = "layout";
+ const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc));
+ if (audioAnnos === undefined) {
+ self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]);
+ } else {
+ audioAnnos.push(audioDoc);
+ }
+ }
+ };
+ runInAction(() => self._mediaState = 2);
+ recorder.start();
+ setTimeout(() => {
+ recorder.stop();
+ runInAction(() => self._mediaState = 0);
+ gumStream.getAudioTracks()[0].stop();
+ }, 5000);
+ });
+ }
+
+ captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption");
+ @computed get innards() {
+ TraceMobx();
+ const ffscale = () => (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1);
+ const showTitle = this.ShowTitle?.split(":")[0];
+ const showTitleHover = this.ShowTitle?.includes(":hover");
+ const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
+ const captionView = !showCaption ? (null) :
+ <div className="documentView-captionWrapper"
+ style={{
+ pointerEvents: this.onClickHandler || this.Document.ignoreClick ? "none" : this.isContentActive() || this.props.isDocumentActive?.() ? "all" : undefined,
+ minWidth: 50 * ffscale(),
+ maxHeight: `max(100%, ${20 * ffscale()}px)`
+ }}>
+ <FormattedTextBox {...OmitKeys(this.props, ['children']).omit}
+ yPadding={10}
+ xPadding={10}
+ fieldKey={showCaption}
+ fontSize={12 * Math.max(1, 2 * ffscale() / 3)}
+ styleProvider={this.captionStyleProvider}
+ dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
+ isContentActive={this.isContentActive}
+ onClick={this.onClickFunc}
+ />
+ </div>;
+ const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc);
+ const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
+ 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,
+ 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 {
+ var value = input.replace(new RegExp(showTitle + "="), "") as string | number;
+ if (showTitle !== "title" && Number(value).toString() === value) value = Number(value);
+ if (showTitle.includes("Date") || showTitle === "author") return true;
+ Doc.SetInPlace(targetDoc, showTitle, value, true);
+ }
+ return true;
+ })}
+ />
+ </div>;
+ return this.props.hideTitle || (!showTitle && !showCaption) ?
+ this.contents :
+ <div className="documentView-styleWrapper" >
+ {!this.headerMargin ? <> {this.contents} {titleView} </> : <> {titleView} {this.contents} </>}
+ {captionView}
+ </div>;
+ }
+ isHovering = () => this._isHovering;
+ @observable _isHovering = false;
+ @observable _: string = "";
+ @computed get renderDoc() {
+ TraceMobx();
+ const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png");
+ const isButton = this.props.Document.type === DocumentType.FONTICON;
+ if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null;
+ return this.docContents ??
+ <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
+ id={this.props.Document[Id]}
+ onPointerEnter={action(() => this._isHovering = true)}
+ onPointerLeave={action(() => this._isHovering = false)}
+ style={{
+ background: isButton || thumb ? undefined : this.backgroundColor,
+ opacity: this.opacity,
+ color: StrCast(this.layoutDoc.color, "inherit"),
+ fontFamily: StrCast(this.Document._fontFamily, "inherit"),
+ fontSize: Cast(this.Document._fontSize, "string", null),
+ transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
+ transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`,
+ }}>
+
+ {this.innards}
+ {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : (null)}
+ {this.widgetDecorations ?? null}
+ </div>;
+ }
+ render() {
+ TraceMobx();
+ const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
+ const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "orange"][highlightIndex];
+ const highlightStyle = ["solid", "dashed", "solid", "solid"][highlightIndex];
+ const excludeTypes = !this.props.treeViewDoc && highlightIndex < 3 ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
+ let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear;
+ highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
+
+ const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined };
+ const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc;
+ const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` :
+ this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined);
+
+ // Return surrounding highlight
+ return <div className={`${DocumentView.ROOT_DIV} docView-hack`} ref={this._mainCont}
+ onContextMenu={this.onContextMenu}
+ onKeyDown={this.onKeyDown}
+ onPointerDown={this.onPointerDown}
+ onClick={this.onClick}
+ onPointerEnter={e => !SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document)}
+ onPointerLeave={e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document)}
+ style={{
+ display: this.hidden ? "inline" : undefined,
+ borderRadius: this.borderRounding,
+ pointerEvents: this.pointerEvents,
+ outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px",
+ border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined,
+ boxShadow,
+ clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined
+ }}>
+ {!borderPath.path ? internal :
+ <>
+ {/* <div style={{ clipPath: `path('${borderPath.fill}')` }}>
{internal}
</div> */}
- {internal}
- <div key="border2" className="documentView-customBorder" style={{ pointerEvents: "none" }} >
- <svg style={{ overflow: "visible" }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
- <path d={borderPath.path} style={{ stroke: "black", fill: "transparent", strokeWidth: borderPath.width }} />
- </svg>
- </div>
- </>
- }
- {this.showFilterIcon ?
- <FontAwesomeIcon icon={"filter"} size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
- onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
- />
- : (null)}
- </div>;
- }
+ {internal}
+ <div key="border2" className="documentView-customBorder" style={{ pointerEvents: "none" }} >
+ <svg style={{ overflow: "visible" }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
+ <path d={borderPath.path} style={{ stroke: "black", fill: "transparent", strokeWidth: borderPath.width }} />
+ </svg>
+ </div>
+ </>
+ }
+ {this.showFilterIcon ?
+ <FontAwesomeIcon icon={"filter"} size="lg"
+ style={{ position: 'absolute', top: '1%', right: '1%', cursor: "pointer", padding: 1, color: this.showFilterIcon === "hasFilter" ? '#18c718bd' : "orange", zIndex: 1 }}
+ onPointerDown={action(e => { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })}
+ />
+ : (null)}
+ </div>;
+ }
}
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
- public static ROOT_DIV = "documentView-effectsWrapper";
- public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
- public ContentRef = React.createRef<HTMLDivElement>();
- private _disposers: { [name: string]: IReactionDisposer } = {};
-
- public static showBackLinks(linkSource: Doc) {
- const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + "-pivotish";
- DocServer.GetRefField(docid).then(docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(linkSource);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- const linkCollection = ((docx instanceof Doc) && docx) || Docs.Create.StackingDocument([/*rootAlias()*/], { title: linkSource.title + "-pivot", _width: 500, _height: 500, }, docid);
- linkCollection.linkSource = linkSource;
- if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript("updateLinkCollection(self)");
- LightboxView.SetLightboxDoc(linkCollection);
- });
- }
-
- @observable public docView: DocumentViewInternal | undefined | null;
-
- get Document() { return this.props.Document; }
- get topMost() { return this.props.renderDepth === 0; }
- get rootDoc() { return this.docView?.rootDoc || this.Document; }
- get dataDoc() { return this.docView?.dataDoc || this.Document; }
- get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; }
- get ContentDiv() { return this.docView?.ContentDiv; }
- get ComponentView() { return this.docView?._componentView; }
- get allLinks() { return this.docView?.allLinks || []; }
- get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; }
- get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; }
-
- @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; }
- @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); }
- @computed get nativeWidth() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
- returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
- }
- @computed get nativeHeight() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
- returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
- }
- @computed get shouldNotScale() { return (this.fitWidth && !this.nativeWidth) || this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); }
- @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); }
- @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); }
- @computed get nativeScaling() {
- if (this.shouldNotScale) return 1;
- const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
- if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
- return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth
- }
- return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
- }
-
- @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); }
- @computed get panelHeight() {
- if (this.effectiveNativeHeight) {
- return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling);
- }
- return this.props.PanelHeight();
- }
- @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; }
- @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; }
- @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; }
- @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
-
- toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
- focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options);
- getBounds = () => {
- if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
- return undefined;
- }
- const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse();
- const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
- if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
- const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
- if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
- }
- return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
- }
-
- public iconify() {
- const layoutKey = Cast(this.Document.layoutKey, "string", null);
- if (layoutKey !== "layout_icon") {
- this.switchViews(true, "icon");
- if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", "");
- } else {
- const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null);
- this.switchViews(deiconifyLayout ? true : false, deiconifyLayout);
- this.Document.deiconifyLayout = undefined;
- }
- }
- @undoBatch
- @action
- setCustomView = (custom: boolean, layout: string): void => {
- Doc.setNativeView(this.props.Document);
- custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
- }
- switchViews = action((custom: boolean, view: string) => {
- this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
- setTimeout(action(() => {
- this.setCustomView(custom, view);
- this.docView && (this.docView._animateScalingTo = 1); // expand it
- setTimeout(action(() => this.docView && (this.docView._animateScalingTo = 0)), this.docView!._animateScaleTime - 10);
- }), this.docView!._animateScaleTime - 10);
- });
-
- startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
-
- docViewPathFunc = () => this.docViewPath;
- isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
- select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
- NativeWidth = () => this.effectiveNativeWidth;
- NativeHeight = () => this.effectiveNativeHeight;
- PanelWidth = () => this.panelWidth;
- PanelHeight = () => this.panelHeight;
- ContentScale = () => this.nativeScaling;
- selfView = () => this;
- screenToLocalTransform = () => {
- return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
- }
- componentDidMount() {
- this._disposers.reactionScript = reaction(
- () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
- output => output
- );
- this._disposers.height = reaction(
- () => NumCast(this.layoutDoc._height),
- action(height => {
- const docMax = NumCast(this.layoutDoc.docMaxAutoHeight);
- if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height;
- })
- );
- !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this);
- }
- componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
- !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
- }
-
- render() {
- TraceMobx();
- const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
- const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
- const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
- const isOverlay: boolean = CurrentUserUtils.OverlayDocs.includes(this.props.Document);
- return (<div className="contentFittingDocumentView">
- {!this.props.Document || !this.props.PanelWidth() ? (null) : (
- <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
- style={{
- transition: this.props.dataTransition,
- position: this.props.Document.isInkMask ? "absolute" : undefined,
- transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- width: isOverlay ? undefined : isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
- height: isOverlay ? undefined : (!this.props.ignoreAutoHeight && this.layoutDoc.autoHeight && this.layoutDoc.type === DocumentType.RTF) || isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
- `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
- }}>
- <DocumentViewInternal {...this.props}
- DocumentView={this.selfView}
- viewPath={this.docViewPathFunc}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- NativeWidth={this.NativeWidth}
- NativeHeight={this.NativeHeight}
- isSelected={this.isSelected}
- select={this.select}
- ContentScaling={this.ContentScale}
- ScreenToLocalTransform={this.screenToLocalTransform}
- focus={this.props.focus || emptyFunction}
- bringToFront={emptyFunction}
- ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} />
- </div>)}
- </div>);
- }
+ public static ROOT_DIV = "documentView-effectsWrapper";
+ public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
+ public ContentRef = React.createRef<HTMLDivElement>();
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+
+ public static showBackLinks(linkSource: Doc) {
+ const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + "-pivotish";
+ DocServer.GetRefField(docid).then(docx => {
+ const rootAlias = () => {
+ const rootAlias = Doc.MakeAlias(linkSource);
+ rootAlias.x = rootAlias.y = 0;
+ return rootAlias;
+ };
+ const linkCollection = ((docx instanceof Doc) && docx) || Docs.Create.StackingDocument([/*rootAlias()*/], { title: linkSource.title + "-pivot", _width: 500, _height: 500, }, docid);
+ linkCollection.linkSource = linkSource;
+ if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript("updateLinkCollection(self)");
+ LightboxView.SetLightboxDoc(linkCollection);
+ });
+ }
+
+ @observable public docView: DocumentViewInternal | undefined | null;
+
+ get Document() { return this.props.Document; }
+ get topMost() { return this.props.renderDepth === 0; }
+ get rootDoc() { return this.docView?.rootDoc || this.Document; }
+ get dataDoc() { return this.docView?.dataDoc || this.Document; }
+ get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; }
+ get ContentDiv() { return this.docView?.ContentDiv; }
+ get ComponentView() { return this.docView?._componentView; }
+ get allLinks() { return this.docView?.allLinks || []; }
+ get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; }
+ get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; }
+
+ @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; }
+ @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); }
+ @computed get nativeWidth() {
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
+ returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
+ }
+ @computed get nativeHeight() {
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 :
+ returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions));
+ }
+ @computed get shouldNotScale() {
+ return (this.fitWidth && !this.nativeWidth) ||
+ this.props.ContainingCollectionView?.collectionViewType === CollectionViewType.Time ||
+ this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+ }
+ @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); }
+ @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); }
+ @computed get nativeScaling() {
+ if (this.shouldNotScale) return 1;
+ const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
+ if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
+ return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth
+ }
+ return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
+ }
+
+ @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); }
+ @computed get panelHeight() {
+ if (this.effectiveNativeHeight) {
+ return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling);
+ }
+ return this.props.PanelHeight();
+ }
+ @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; }
+ @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; }
+ @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; }
+ @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; }
+
+ toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight());
+ focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options);
+ getBounds = () => {
+ if (!this.docView || !this.docView.ContentDiv || this.docView.props.renderDepth === 0 || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+ return undefined;
+ }
+ const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse();
+ const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
+ if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
+ const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
+ if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
+ }
+ return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
+ }
+
+ public iconify(finished?: () => void) {
+ this.ComponentView?.updateIcon?.();
+ const layoutKey = Cast(this.Document.layoutKey, "string", null);
+ if (layoutKey !== "layout_icon") {
+ this.switchViews(true, "icon", finished);
+ if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", "");
+ } else {
+ const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null);
+ this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
+ this.Document.deiconifyLayout = undefined;
+ this.props.bringToFront(this.rootDoc);
+ }
+ }
+ @undoBatch
+ @action
+ setCustomView = (custom: boolean, layout: string): void => {
+ Doc.setNativeView(this.props.Document);
+ custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
+ }
+ switchViews = action((custom: boolean, view: string, finished?: () => void) => {
+ this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
+ setTimeout(action(() => {
+ this.setCustomView(custom, view);
+ this.docView && (this.docView._animateScalingTo = 1); // expand it
+ setTimeout(action(() => {
+ this.docView && (this.docView._animateScalingTo = 0);
+ finished?.();
+ }), this.docView!._animateScaleTime - 10);
+ }), this.docView!._animateScaleTime - 10);
+ });
+
+ startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+
+ docViewPathFunc = () => this.docViewPath;
+ isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
+ select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
+ NativeWidth = () => this.effectiveNativeWidth;
+ NativeHeight = () => this.effectiveNativeHeight;
+ PanelWidth = () => this.panelWidth;
+ PanelHeight = () => this.panelHeight;
+ ContentScale = () => this.nativeScaling;
+ selfView = () => this;
+ screenToLocalTransform = () => {
+ return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling);
+ }
+ componentDidMount() {
+ this._disposers.reactionScript = reaction(
+ () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
+ output => output
+ );
+ this._disposers.height = reaction(
+ () => NumCast(this.layoutDoc._height),
+ action(height => {
+ const docMax = NumCast(this.layoutDoc.docMaxAutoHeight);
+ if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height;
+ })
+ );
+ !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this);
+ }
+ componentWillUnmount() {
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
+ }
+
+ render() {
+ TraceMobx();
+ const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined);
+ const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined);
+ const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES;
+ const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
+ // const isOverlay: boolean = CurrentUserUtils.OverlayDocs.includes(this.props.Document);
+ return (<div className="contentFittingDocumentView">
+ {!this.props.Document || !this.props.PanelWidth() ? (null) : (
+ <div className="contentFittingDocumentView-previewDoc" ref={this.ContentRef}
+ style={{
+ transition: this.props.dataTransition,
+ position: this.props.Document.isInkMask ? "absolute" : undefined,
+ transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: isButton || isPresTreeElement ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
+ height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+ `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
+ }}>
+ <DocumentViewInternal {...this.props}
+ DocumentView={this.selfView}
+ viewPath={this.docViewPathFunc}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ NativeWidth={this.NativeWidth}
+ NativeHeight={this.NativeHeight}
+ isSelected={this.isSelected}
+ select={this.select}
+ ContentScaling={this.ContentScale}
+ ScreenToLocalTransform={this.screenToLocalTransform}
+ focus={this.props.focus || emptyFunction}
+ ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))} />
+ </div>)}
+ </div>);
+ }
}
export function deiconifyViewFunc(documentView: DocumentView) {
- documentView.iconify();
- //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
+ documentView.iconify();
+ //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
}
ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
- documentView.iconify();
-}
-);
+ documentView.iconify();
+ documentView.select(false);
+});
ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
- if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout");
- else dv.switchViews(true, detailLayoutKeySuffix);
+ if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout");
+ else dv.switchViews(true, detailLayoutKeySuffix);
});
ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) {
- const linkSource = Cast(linkCollection.linkSource, Doc, null);
- const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
- let wid = linkSource[WidthSym]();
- const links = DocListCast(linkSource.links);
- links.forEach(link => {
- const other = LinkManager.getOppositeAnchor(link, linkSource);
- const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
- if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- const alias = Doc.MakeAlias(otherdoc);
- alias.x = wid;
- alias.y = 0;
- alias._lockedPosition = false;
- wid += otherdoc[WidthSym]();
- Doc.AddDocToList(Doc.GetProto(linkCollection), "data", alias);
- }
- });
- return links;
+ const linkSource = Cast(linkCollection.linkSource, Doc, null);
+ const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
+ let wid = linkSource[WidthSym]();
+ const links = DocListCast(linkSource.links);
+ links.forEach(link => {
+ const other = LinkManager.getOppositeAnchor(link, linkSource);
+ const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
+ if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
+ const alias = Doc.MakeAlias(otherdoc);
+ alias.x = wid;
+ alias.y = 0;
+ alias._lockedPosition = false;
+ wid += otherdoc[WidthSym]();
+ Doc.AddDocToList(Doc.GetProto(linkCollection), "data", alias);
+ }
+ });
+ return links;
}); \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 1fd665bad..bd52eb018 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -23,7 +23,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
isDocumentActive?: () => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
scaling?: () => number;
- setHeight: (height: number) => void;
+ setHeight?: (height: number) => void;
// properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React)
pointerEvents?: string;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 17f95c1cc..5982d4d66 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -14,6 +14,8 @@ import { TraceMobx } from '../../../fields/util';
import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DragManager } from '../../util/DragManager';
@@ -73,7 +75,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0],
selected: this.props.isSelected()
}),
- ({ forceFull, scrSize, selected }) => this._curSuffix = forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o",
+ ({ forceFull, scrSize, selected }) => this._curSuffix = this.fieldKey === "icon" ? "_m" : forceFull ? "_o" : scrSize < 100 ? "_s" : scrSize < 400 ? "_m" : scrSize < 800 || !selected ? "_l" : "_o",
{ fireImmediately: true, delay: 1000 });
this._disposers.path = reaction(() => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
({ nativeSize, width }) => {
@@ -127,6 +129,48 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.layoutDoc._height = w;
});
+ crop = (region: Doc | undefined, addCrop?: boolean) => {
+ if (!region) return;
+ const cropping = Doc.MakeCopy(region, true);
+ Doc.GetProto(region).lockedPosition = true;
+ Doc.GetProto(region).title = "region:" + this.rootDoc.title;
+ Doc.GetProto(region).isPushpin = true;
+ this.addDocument(region);
+ const anchx = NumCast(cropping.x);
+ const anchy = NumCast(cropping.y);
+ const anchw = NumCast(cropping._width);
+ const anchh = NumCast(cropping._height);
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + "-nativeWidth"]) / anchw;
+ cropping.title = "crop: " + this.rootDoc.title;
+ cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
+ cropping.y = NumCast(this.rootDoc.y);
+ cropping._width = anchw * (this.props.scaling?.() || 1);
+ cropping._height = anchh * (this.props.scaling?.() || 1);
+ cropping.isLinkButton = undefined;
+ const croppingProto = Doc.GetProto(cropping);
+ croppingProto.annotationOn = undefined;
+ croppingProto.isPrototype = true;
+ croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
+ croppingProto.type = DocumentType.IMG;
+ croppingProto.layout = ImageBox.LayoutString("data");
+ croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
+ croppingProto["data-nativeWidth"] = anchw;
+ croppingProto["data-nativeHeight"] = anchh;
+ croppingProto.viewScale = viewScale;
+ croppingProto.viewScaleMin = viewScale;
+ croppingProto.panX = anchx / viewScale;
+ croppingProto.panY = anchy / viewScale;
+ croppingProto.panXMin = (anchx) / viewScale;
+ croppingProto.panXMax = (anchw) / viewScale;
+ croppingProto.panYMin = (anchy) / viewScale;
+ croppingProto.panYMax = (anchh) / viewScale;
+ if (addCrop) {
+ DocUtils.MakeLink({ doc: region }, { doc: cropping }, "cropped image", "");
+ }
+ this.props.bringToFront(cropping);
+ return cropping;
+ }
+
specificContextMenu = (e: React.MouseEvent): void => {
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
@@ -245,8 +289,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get nativeSize() {
TraceMobx();
- const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], 500);
- const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1);
+ const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], NumCast(this.layoutDoc[this.fieldKey + "-nativeWidth"], 500));
+ const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], NumCast(this.layoutDoc[this.fieldKey + "-nativeHeight"], 1));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + "-nativeOrientation"], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
@@ -300,17 +344,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>;
}
- // adjust y position to center image in panel aspect is bigger than image aspect.
- // bcz :note, this is broken for rotated images
- get ycenter() {
- const { nativeWidth, nativeHeight } = this.nativeSize;
- const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]);
- const aspect = (rotation % 180) ? nativeWidth / nativeHeight : nativeHeight / nativeWidth;
- return this.props.PanelHeight() / this.props.PanelWidth() > aspect ?
- (this.props.PanelHeight() - this.props.PanelWidth() * aspect) / 2 : 0;
- }
-
- screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, -this.ycenter);
+ screenToLocalTransform = this.props.ScreenToLocalTransform;
contentFunc = () => [this.content];
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -377,6 +411,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
savedAnnotations={this._savedAnnotations}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
+ anchorMenuCrop={this.crop}
/>}
</div >);
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index c44c8c828..4b1fbaf7d 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -67,7 +67,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
const { script, type, onDelegate } = kvpScript;
//const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
- const target = forceOnDelegate || onDelegate ? doc : doc.proto || doc;
+ const target = forceOnDelegate || onDelegate || key.startsWith("_") ? doc : doc.proto || doc;
let field: Field;
if (type === "computed") {
field = new ComputedField(script);
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
index db43551d8..02c36c4bc 100644
--- a/src/client/views/nodes/LabelBigText.js
+++ b/src/client/views/nodes/LabelBigText.js
@@ -5,14 +5,14 @@ And from Jetroid/bigtext.js v1.0.0, September 2016
Usage:
BigText("#myElement",{
- rotateText: {Number}, (null)
- fontSizeFactor: {Number}, (0.8)
- maximumFontSize: {Number}, (null)
- limitingDimension: {String}, ("both")
- horizontalAlign: {String}, ("center")
- verticalAlign: {String}, ("center")
- textAlign: {String}, ("center")
- whiteSpace: {String}, ("nowrap")
+ rotateText: {Number}, (null)
+ fontSizeFactor: {Number}, (0.8)
+ maximumFontSize: {Number}, (null)
+ limitingDimension: {String}, ("both")
+ horizontalAlign: {String}, ("center")
+ verticalAlign: {String}, ("center")
+ textAlign: {String}, ("center")
+ whiteSpace: {String}, ("nowrap")
});
@@ -28,6 +28,8 @@ fontSizeFactor: This option is used to give some vertical spacing for letters th
maximumFontSize: maximum font size to use.
+minimumFontSize: minimum font size to use. if font is calculated smaller than this, text will be rendered at this size and wrapped
+
limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
@@ -154,6 +156,7 @@ export default function BigText(element, options) {
width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
height: element.offsetHeight,
};
+ if (!box.width || !box.height) return element;
if (options.rotateText !== null) {
@@ -187,7 +190,20 @@ export default function BigText(element, options) {
lineHeight = Math.floor(heightFactor * 1000);
var fontSize = lineHeight * options.fontSizeFactor;
- if (options.maximumFontSize !== null && fontSize > options.maximumFontSize) {
+ if (fontSize < options.minimumFontSize) {
+ parentStyle.display = "flex";
+ parentStyle.alignItems = "center";
+ style.whiteSpace = "pre-wrap";
+ style.textAlign = "center";
+ style.visibility = "";
+ style.fontSize = "18px";
+ style.lineHeight = "20px";
+ style.top = "";
+ style.left = "";
+ style.margin = "";
+ return element;
+ }
+ if (options.maximumFontSize && fontSize > options.maximumFontSize) {
fontSize = options.maximumFontSize;
lineHeight = fontSize / options.fontSizeFactor;
}
diff --git a/src/client/views/nodes/LabelBox.scss b/src/client/views/nodes/LabelBox.scss
index 6a0d651d2..42e158584 100644
--- a/src/client/views/nodes/LabelBox.scss
+++ b/src/client/views/nodes/LabelBox.scss
@@ -4,7 +4,7 @@
border-radius: inherit;
display: flex;
flex-direction: column;
- position: absolute;
+ position: relative;
}
.labelBox-mainButton {
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 0015f0b71..d539ca9b8 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Cast, StrCast, NumCast } from '../../../fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
@@ -73,6 +73,19 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
@observable _mouseOver = false;
@computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; }
+ fitTextToBox = (r: any) => {
+ BigText(r, {
+ rotateText: null,
+ fontSizeFactor: 1,
+ minimumFontSize: NumCast(this.layoutDoc._minFontSize),
+ maximumFontSize: NumCast(this.layoutDoc._maxFontSize),
+ limitingDimension: "both",
+ horizontalAlign: "center",
+ verticalAlign: "center",
+ textAlign: "center",
+ whiteSpace: "nowrap"
+ });
+ }
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
@@ -97,16 +110,8 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
}} >
<span ref={r => {
if (r) {
- BigText(r, {
- rotateText: null,
- fontSizeFactor: 1,
- maximumFontSize: null,
- limitingDimension: "both",
- horizontalAlign: "center",
- verticalAlign: "center",
- textAlign: "center",
- whiteSpace: "nowrap"
- });
+ if (!r.offsetWidth || !r.offsetHeight) setTimeout(() => this.fitTextToBox(r));
+ else this.fitTextToBox(r);
}
}}>{label.startsWith("#") ? (null) : label}</span>
</div>
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index d5f3f3b4e..375434933 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -15,6 +15,7 @@ import { DocumentView, DocumentViewSharedProps } from "./DocumentView";
import './LinkDocPreview.scss';
import React = require("react");
import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager } from '../../util/DragManager';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -84,7 +85,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => {
if (anchor instanceof Doc && DocListCast(anchor.links).length) {
this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0];
- const automaticLink = this._linkDoc.linkRelationship === "automatic";
+ const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords;
if (automaticLink) { // automatic links specify the target in the link info, not the source
const linkTarget = anchor;
this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
@@ -109,7 +110,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, action(() => {
this._linkDoc = undefined;
this._hrefInd = (this._hrefInd + 1) % (this.props.hrefs?.length || 1);
- }));
+ }), true);
}
followLink = (e: React.PointerEvent) => {
@@ -137,7 +138,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@computed get previewHeader() {
return !this._linkDoc || !this._targetDoc || !this._linkSrc ? (null) :
<div className="linkDocPreview-info" ref={this._infoRef}>
- <div className="linkDocPreview-title">
+ <div className="linkDocPreview-title" style={{ pointerEvents: "all" }}
+ onPointerDown={e => {
+ DragManager.StartDocumentDrag([this._infoRef.current!],
+ new DragManager.DocumentDragData([this._targetDoc!]), e.pageX, e.pageY);
+ e.stopPropagation();
+ LinkDocPreview.Clear();
+ }}>
{StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + "..." : this._targetDoc.title}
<p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
</div>
@@ -188,6 +195,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
ContainingCollectionDoc={undefined}
ContainingCollectionView={undefined}
renderDepth={-1}
+ suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
focus={DocUtils.DefaultFocus}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index a2e7d2aa3..668c6c8fc 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -2,10 +2,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
+import { CreateImage } from "../nodes/WebBoxRenderer";
import "pdfjs-dist/web/pdf_viewer.css";
-import { Doc, DocListCast, Opt, WidthSym } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../fields/Doc";
import { Cast, NumCast, StrCast, ImageCast } from '../../../fields/Types';
-import { PdfField } from "../../../fields/URLField";
+import { ImageField, PdfField } from "../../../fields/URLField";
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -21,6 +22,8 @@ import { SidebarAnnos } from '../SidebarAnnos';
import { FieldView, FieldViewProps } from './FieldView';
import "./PDFBox.scss";
import React = require("react");
+import { Id } from '../../../fields/FieldSymbols';
+import { VideoBox } from './VideoBox';
@observer
export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -52,6 +55,56 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
+ replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => {
+ if (oldDiv.childNodes) {
+ for (let i = 0; i < oldDiv.childNodes.length; i++) {
+ this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement);
+ }
+ }
+ if (oldDiv instanceof HTMLCanvasElement) {
+ const canvas = oldDiv;
+ const img = document.createElement('img'); // create a Image Element
+ img.src = canvas.toDataURL(); //image source
+ img.style.width = canvas.style.width;
+ img.style.height = canvas.style.height;
+ const newCan = newDiv as HTMLCanvasElement;
+ const parEle = newCan.parentElement as HTMLElement;
+ parEle.removeChild(newCan);
+ parEle.appendChild(img);
+ }
+ }
+
+ updateIcon = () => {
+ const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
+ const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
+ newDiv.style.width = (this.layoutDoc[WidthSym]()).toString();
+ newDiv.style.height = (this.layoutDoc[HeightSym]()).toString();
+ this.replaceCanvases(docViewContent, newDiv);
+ const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
+ const nativeWidth = this.layoutDoc[WidthSym]();
+ const nativeHeight = this.layoutDoc[HeightSym]();
+
+ CreateImage(
+ "",
+ document.styleSheets,
+ htmlString?.replace(/docView-hack/g, 'documentView-hack'),
+ nativeWidth,
+ nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(),
+ NumCast(this.layoutDoc._scrollTop) * this.props.PanelHeight() / NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"])
+ ).then
+ ((data_url: any) => {
+ VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then(
+ returnedfilename => setTimeout(action(() => {
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = nativeWidth;
+ this.dataDoc["icon-nativeHeight"] = nativeHeight;
+ }), 500));
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
+ }
+
componentWillUnmount() { this._selectReactionDisposer?.(); }
componentDidMount() {
this.props.setContentView?.(this);
@@ -104,8 +157,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
});
public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
- public backPage = () => { this.Document._curPage = (NumCast(this.Document._curPage) || 1) - 1; return true; };
- public forwardPage = () => { this.Document._curPage = (NumCast(this.Document._curPage) || 1) + 1; return true; };
+ public backPage = () => {
+ this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1);
+ return true;
+ }
+ public forwardPage = () => {
+ this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + "-numPages"]), (NumCast(this.Document._curPage) || 1) + 1);
+ return true;
+ }
public gotoPage = (p: number) => this.Document._curPage = p;
@undoBatch
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 6d444d324..86358e838 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,24 +1,54 @@
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
+import { AudioField, VideoField } from "../../../../fields/URLField";
+import { Upload } from "../../../../server/SharedMediaTypes";
import { ViewBoxBaseComponent } from "../../DocComponent";
import { FieldView } from "../FieldView";
+import { VideoBox } from "../VideoBox";
import { RecordingView } from './RecordingView';
+import { DocumentType } from "../../../documents/DocumentTypes";
@observer
-export class RecordingBox extends ViewBoxBaseComponent(){
+export class RecordingBox extends ViewBoxBaseComponent() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
- private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
- super(props);
- }
+ constructor(props: any) {
+ super(props);
+ }
- render() {
- return <div className="recordingBox" ref={this._ref}>
- <RecordingView/>
- </div>;
- }
+ @observable result: Upload.FileInformation | undefined = undefined
+ @observable videoDuration: number | undefined = undefined
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration
+ }
+
+ @action
+ setResult = (info: Upload.FileInformation) => {
+ console.log("Setting result to " + info)
+ this.result = info
+ console.log(this.result.accessPaths.agnostic.client)
+ this.dataDoc.type = DocumentType.VID;
+ console.log(this.videoDuration)
+ this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
+
+ this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ // this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+ // this.layoutDoc._fitWidth = undefined;
+ this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client);
+ }
+
+ render() {
+ return <div className="recordingBox" ref={this._ref}>
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} />}
+ {/* {!this.result ? <RecordingView setResult={this.setResult} /> :
+ <p>video box</p>} */}
+ </div>;
+ }
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.scss b/src/client/views/nodes/RecordingBox/RecordingView.scss
index e4d971d51..1fea231b7 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.scss
+++ b/src/client/views/nodes/RecordingBox/RecordingView.scss
@@ -15,6 +15,7 @@ button {
height: 100%;
width: 100%;
display: flex;
+ pointer-events: all;
}
.video-wrapper {
@@ -157,8 +158,8 @@ button {
background-color: red;
border: 0px;
border-radius: 10%;
- height: 80%;
- width: 80%;
+ height: 70%;
+ width: 70%;
align-self: center;
margin: 0;
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index fab7bc990..d2adff95a 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -5,6 +5,8 @@ import { ProgressBar } from "./ProgressBar"
import { MdBackspace } from 'react-icons/md';
import { FaCheckCircle } from 'react-icons/fa';
import { IconContext } from "react-icons";
+import { Networking } from '../../../Network';
+import { Upload } from '../../../../server/SharedMediaTypes';
import { RecordingApi } from '../../../apis/recording/RecordingApi';
@@ -15,14 +17,19 @@ enum RecordingStatus {
Paused
}
-interface VideoSegment {
- chunks: any[],
+interface MediaSegment {
+ videoChunks: any[],
endTime: number
}
+interface IRecordingViewProps {
+ setResult: (info: Upload.FileInformation) => void
+ setDuration: (seconds: number) => void
+}
+
const MAXTIME = 1000;
-export function RecordingView() {
+export function RecordingView(props: IRecordingViewProps) {
const [recording, setRecording] = useState(false);
const recordingTimerRef = useRef<number>(0);
@@ -32,10 +39,10 @@ export function RecordingView() {
const [speed, setSpeed] = useState(1);
const [muted, setMuted] = useState(false);
- const [videos, setVideos] = useState<VideoSegment[]>([]);
- // const [videos, setVideos] = useState<string[]>([]);
+ const [videos, setVideos] = useState<MediaSegment[]>([]);
const [currentVid, setCurrentVid] = useState<number>(0);
- const recorder = useRef<MediaRecorder | null>(null);
+ const videoRecorder = useRef<MediaRecorder | null>(null);
+ const audioRecorder = useRef<MediaRecorder | null>(null);
const videoElementRef = useRef<HTMLVideoElement | null>(null);
const [finished, setFinished] = useState<Boolean>(false)
@@ -43,49 +50,57 @@ export function RecordingView() {
const DEFAULT_MEDIA_CONSTRAINTS = {
+ // video: true,
+ // audio: true
video: {
width: 1280,
height: 720,
},
- audio: {
- echoCancellation: true,
- noiseSuppression: true,
- sampleRate: 44100
- }
+ // audio: true,
+ // audio: {
+ // echoCancellation: true,
+ // noiseSuppression: true,
+ // sampleRate: 44100
+ // }
}
useEffect(() => {
if (finished) {
+ props.setDuration(recordingTimer * 100)
let allVideoChunks: any = []
console.log(videos)
videos.forEach((vid) => {
- console.log(vid.chunks)
- allVideoChunks = allVideoChunks.concat(vid.chunks)
+ console.log(vid.videoChunks)
+ allVideoChunks = allVideoChunks.concat(vid.videoChunks)
})
console.log(allVideoChunks)
-
- const blob = new Blob(allVideoChunks, {
- type: 'video/webm'
- })
- const blobUrl = URL.createObjectURL(blob)
-
- videoElementRef.current!.srcObject = null
- videoElementRef.current!.src = blobUrl
- videoElementRef.current!.muted = false
-
- // clear the recording api
- const presentation = RecordingApi.finish()
- RecordingApi.clear()
-
- // upload data
- // const [{ result }] = await Networking.UploadFilesToServer(e.data);
- // console.log("Data result", result);
- // if (!(result instanceof Error)) {
- // this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
+ const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() });
+
+ // const uploadVideo = async () => {
+ // const [{ result }] = await Networking.UploadFilesToServer(videoFile);
+ // console.log("upload result", result);
+ // if (!(result instanceof Error)) {
+ // setResult(result)
+ // }
// }
+ Networking.UploadFilesToServer(videoFile)
+ .then((data) => {
+ const result = data[0].result
+ if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+ props.setResult(result)
+ } else {
+ alert("video conversion failed");
+ }
+ })
+ // uploadVideo()
+
+ // this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
+
+
+
// change to one recording box
}
@@ -102,21 +117,9 @@ export function RecordingView() {
useEffect(() => {
// get access to the video element on every render
- // videoElement = document.getElementById('video') as HTMLVideoElement;
videoElementRef.current = document.getElementById('video') as HTMLVideoElement;
})
- // useEffect(() => {
- // if (playing) {
- // videoElement!.srcObject = null
- // // videoElement!.src = videos[currentVid].url
- // videoElement!.muted = false
- // videoElement!.play()
- // } else {
- // videoElement!.pause();
- // }
- // }, [playing, videoElement]);
-
useEffect(() => {
let interval: any = null;
if (recording) {
@@ -150,82 +153,86 @@ export function RecordingView() {
const record = async () => {
const stream = await startShowingStream();
- recorder.current = new MediaRecorder(stream)
+ videoRecorder.current = new MediaRecorder(stream)
+ // audioRecorder.current = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
// temporary chunks of video
- let chunks: any = []
- recorder.current.ondataavailable = (event: any) => {
- // store the video chunks as it is recording
- console.log("data available")
+ let videoChunks: any = []
+ // let audioChunks: any = []
+
+ videoRecorder.current.ondataavailable = (event: any) => {
if (event.data.size > 0) {
- chunks.push(event.data)
+ videoChunks.push(event.data)
}
}
- recorder.current.onstart = (event: any) => {
- console.log("on start")
+ // audioRecorder.current.ondataavailable = (event: any) => {
+ // if (event.data.size > 0) {
+ // audioChunks.push(event.data)
+ // }
+ // }
+
+ videoRecorder.current.onstart = (event: any) => {
setRecording(true);
}
- recorder.current.onstop = () => {
+ videoRecorder.current.onstop = () => {
// if we have a last portion
- if (chunks.length > 1) {
+ if (videoChunks.length > 1) {
// append the current portion to the video pieces
- setVideos(videos => [...videos, { chunks: chunks, endTime: recordingTimerRef.current }])
+ setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
}
// reset the temporary chunks
- chunks = []
+ videoChunks = []
setRecording(false);
setFinished(true);
}
// recording paused
- recorder.current.onpause = (event: any) => {
+ videoRecorder.current.onpause = (event: any) => {
// append the current portion to the video pieces
- console.log(chunks)
- setVideos(videos => [...videos, { chunks: chunks, endTime: recordingTimerRef.current }])
+ console.log(videoChunks)
+ setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }])
// reset the temporary chunks
- chunks = []
+ videoChunks = []
setRecording(false);
}
- recorder.current.onresume = async (event: any) => {
+ videoRecorder.current.onresume = async (event: any) => {
console.log(event)
await startShowingStream();
setRecording(true);
}
- recorder.current.start(200)
+ videoRecorder.current.start(200)
}
const stop = () => {
- if (recorder.current) {
- if (recorder.current.state !== "inactive") {
- recorder.current.stop();
+ if (videoRecorder.current) {
+ if (videoRecorder.current.state !== "inactive") {
+ videoRecorder.current.stop();
// recorder.current.stream.getTracks().forEach((track: any) => track.stop())
}
}
}
const pause = () => {
- if (recorder.current) {
- if (recorder.current.state === "recording") {
- recorder.current.pause();
- const err = RecordingApi.pause()
+ if (videoRecorder.current) {
+ if (videoRecorder.current.state === "recording") {
+ videoRecorder.current.pause();
}
}
}
const startOrResume = () => {
console.log('[RecordingView.tsx] startOrResume')
- if (!recorder.current || recorder.current.state === "inactive") {
- record();
- const err = RecordingApi.initAndStart()
- } else if (recorder.current.state === "paused") {
- recorder.current.resume();
+ if (!videoRecorder.current || videoRecorder.current.state === "inactive") {
+ record();
+ } else if (videoRecorder.current.state === "paused") {
+ videoRecorder.current.resume();
}
}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index dbb567d3a..0c7d7dc31 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -26,7 +26,7 @@ import { FormattedTextBox } from "./formattedText/FormattedTextBox";
import "./ScreenshotBox.scss";
import { VideoBox } from "./VideoBox";
declare class MediaRecorder {
- constructor(e: any, options?: any); // whatever MediaRecorder has
+ constructor(e: any, options?: any); // whatever MediaRecorder has
}
// interface VideoTileProps {
@@ -107,225 +107,226 @@ declare class MediaRecorder {
@observer
export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
- private _audioRec: any;
- private _videoRec: any;
- @observable private _videoRef: HTMLVideoElement | null = null;
- @observable _screenCapture = false;
- @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+ private _audioRec: any;
+ private _videoRec: any;
+ @observable private _videoRef: HTMLVideoElement | null = null;
+ @observable _screenCapture = false;
+ @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
- constructor(props: any) {
- super(props);
- if (this.rootDoc.videoWall) {
- this.rootDoc.nativeWidth = undefined;
- this.rootDoc.nativeHeight = undefined;
- this.layoutDoc.popOff = 0;
- this.layoutDoc.popOut = 1;
- } else {
- this.setupDictation();
- }
- }
- getAnchor = () => {
- const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
- startTime, startTime === undefined ? undefined : startTime + 3)
- || this.rootDoc;
- }
+ constructor(props: any) {
+ super(props);
+ if (this.rootDoc.videoWall) {
+ this.rootDoc.nativeWidth = undefined;
+ this.rootDoc.nativeHeight = undefined;
+ this.layoutDoc.popOff = 0;
+ this.layoutDoc.popOut = 1;
+ } else {
+ this.setupDictation();
+ }
+ }
+ getAnchor = () => {
+ const startTime = Cast(this.layoutDoc._currentTimecode, "number", null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, "_timecodeToShow", "_timecodeToHide",
+ startTime, startTime === undefined ? undefined : startTime + 3)
+ || this.rootDoc;
+ }
- videoLoad = () => {
- const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
- const nativeWidth = Doc.NativeWidth(this.layoutDoc);
- const nativeHeight = Doc.NativeHeight(this.layoutDoc);
- if (!nativeWidth || !nativeHeight) {
- if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
- Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
- this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
- }
- }
+ videoLoad = () => {
+ const aspect = this._videoRef!.videoWidth / this._videoRef!.videoHeight;
+ const nativeWidth = Doc.NativeWidth(this.layoutDoc);
+ const nativeHeight = Doc.NativeHeight(this.layoutDoc);
+ if (!nativeWidth || !nativeHeight) {
+ if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
+ Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
+ this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
+ }
+ }
- componentDidMount() {
- this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
- this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
- // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
- // ({ width, height }) => {
- // if (this._camera) {
- // const angle = -Math.abs(1 - width / height);
- // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
- // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
- // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
- // (this._camera as any).updateProjectionMatrix();
- // }
- // });
- }
- componentWillUnmount() {
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
- }
+ componentDidMount() {
+ this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0;
+ this.props.setContentView?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
+ // this.rootDoc.videoWall && reaction(() => ({ width: this.props.PanelWidth(), height: this.props.PanelHeight() }),
+ // ({ width, height }) => {
+ // if (this._camera) {
+ // const angle = -Math.abs(1 - width / height);
+ // const xz = [0, (this._numScreens - 2) / Math.abs(1 + angle)];
+ // this._camera.position.set(this._numScreens / 2 + xz[1] * Math.sin(angle), this._numScreens / 2, xz[1] * Math.cos(angle));
+ // this._camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+ // (this._camera as any).updateProjectionMatrix();
+ // }
+ // });
+ }
+ componentWillUnmount() {
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ }
- specificContextMenu = (e: React.MouseEvent): void => {
- const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
- ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
- }
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const subitems = [{ description: "Screen Capture", event: this.toggleRecording, icon: "expand-arrows-alt" as any }];
+ ContextMenu.Instance.addItem({ description: "Options...", subitems, icon: "video" });
+ }
- @computed get content() {
- if (this.rootDoc.videoWall) return (null);
- return <video className={"videoBox-content"} key="video"
- ref={r => {
- this._videoRef = r;
- setTimeout(() => {
- if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) {
- this.toggleRecording();
- }
- }, 100);
- }}
- autoPlay={this._screenCapture}
- style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
- onCanPlay={this.videoLoad}
- controls={true}
- onClick={e => e.preventDefault()}>
- <source type="video/mp4" />
- Not supported.
- </video>;
- }
+ @computed get content() {
+ if (this.rootDoc.videoWall) return (null);
+ return <video className={"videoBox-content"} key="video"
+ ref={r => {
+ this._videoRef = r;
+ setTimeout(() => {
+ if (this.rootDoc.mediaState === "pendingRecording" && this._videoRef) {
+ this.toggleRecording();
+ }
+ }, 100);
+ }}
+ autoPlay={this._screenCapture}
+ style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+ onCanPlay={this.videoLoad}
+ controls={true}
+ onClick={e => e.preventDefault()}>
+ <source type="video/mp4" />
+ Not supported.
+ </video>;
+ }
- // _numScreens = 5;
- // _camera: Camera | undefined;
- // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
- // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
- @computed get threed() {
- // if (this.rootDoc.videoWall) {
- // const screens: any[] = [];
- // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
- // let count = 0;
- // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
- // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
- // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
- // this._camera = props.camera;
- // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
- // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
- // }}>
- // {/* <ambientLight />*/}
- // <pointLight position={[10, 10, 10]} intensity={1} />
- // {screens}
- // </ Canvas>;
- // }
- return (null);
- }
- toggleRecording = async () => {
- if (!this._screenCapture) {
- this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
- const aud_chunks: any = [];
- this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
- this._audioRec.onstop = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
- if (!(result instanceof Error)) {
- this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
- }
- };
- this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
- this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
- const vid_chunks: any = [];
- this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
- this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
- this._videoRec.onstop = async (e: any) => {
- const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
- const [{ result }] = await Networking.UploadFilesToServer(file);
- this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
- if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
- this.dataDoc.type = DocumentType.VID;
- this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
- this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
- this.layoutDoc._fitWidth = undefined;
- this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
- } else alert("video conversion failed");
- };
- this._audioRec.start();
- this._videoRec.start();
- runInAction(() => {
- this._screenCapture = true;
- this.dataDoc.mediaState = "recording";
- });
- DocUtils.ActiveRecordings.push(this);
- } else {
- this._audioRec?.stop();
- this._videoRec?.stop();
- runInAction(() => {
- this._screenCapture = false;
- this.dataDoc.mediaState = "paused";
- });
- const ind = DocUtils.ActiveRecordings.indexOf(this);
- ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
+ // _numScreens = 5;
+ // _camera: Camera | undefined;
+ // @observable _raised = [] as { coord: Vector2, off: Vector3 }[];
+ // @action setRaised = (r: { coord: Vector2, off: Vector3 }[]) => this._raised = r;
+ @computed get threed() {
+ // if (this.rootDoc.videoWall) {
+ // const screens: any[] = [];
+ // const colors = ["yellow", "red", "orange", "brown", "maroon", "gray"];
+ // let count = 0;
+ // numberRange(this._numScreens).forEach(x => numberRange(this._numScreens).forEach(y => screens.push(
+ // <VideoTile rootDoc={this.rootDoc} color={colors[count++ % colors.length]} x={x} y={y} raised={this._raised} setRaised={this.setRaised} />)));
+ // return <Canvas key="canvas" id="CANCAN" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }} gl={{ antialias: false }} colorManagement={false} onCreated={props => {
+ // this._camera = props.camera;
+ // props.camera.position.set(this._numScreens / 2, this._numScreens / 2, this._numScreens - 2);
+ // props.camera.lookAt(this._numScreens / 2, this._numScreens / 2, 0);
+ // }}>
+ // {/* <ambientLight />*/}
+ // <pointLight position={[10, 10, 10]} intensity={1} />
+ // {screens}
+ // </ Canvas>;
+ // }
+ return (null);
+ }
+ toggleRecording = async () => {
+ if (!this._screenCapture) {
+ this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
+ const aud_chunks: any = [];
+ this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
+ this._audioRec.onstop = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
+ if (!(result instanceof Error)) {
+ this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client);
+ }
+ };
+ this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
+ const vid_chunks: any = [];
+ this._videoRec.onstart = () => this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
+ this._videoRec.onstop = async (e: any) => {
+ console.log("screenshotbox: upload")
+ const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
+ const [{ result }] = await Networking.UploadFilesToServer(file);
+ this.dataDoc[this.fieldKey + "-duration"] = (new Date().getTime() - this.recordingStart!) / 1000;
+ if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox
+ this.dataDoc.type = DocumentType.VID;
+ this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
+ this.layoutDoc._fitWidth = undefined;
+ this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
+ } else alert("video conversion failed");
+ };
+ this._audioRec.start();
+ this._videoRec.start();
+ runInAction(() => {
+ this._screenCapture = true;
+ this.dataDoc.mediaState = "recording";
+ });
+ DocUtils.ActiveRecordings.push(this);
+ } else {
+ this._audioRec?.stop();
+ this._videoRec?.stop();
+ runInAction(() => {
+ this._screenCapture = false;
+ this.dataDoc.mediaState = "paused";
+ });
+ const ind = DocUtils.ActiveRecordings.indexOf(this);
+ ind !== -1 && (DocUtils.ActiveRecordings.splice(ind, 1));
- CaptureManager.Instance.open(this.rootDoc);
- }
- }
+ CaptureManager.Instance.open(this.rootDoc);
+ }
+ }
- setupDictation = () => {
- if (this.dataDoc[this.fieldKey + "-dictation"]) return;
- const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
- NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
- NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
- dictationText._autoHeight = false;
- const dictationTextProto = Doc.GetProto(dictationText);
- dictationTextProto.recordingSource = this.dataDoc;
- dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
- dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
- this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
- }
- contentFunc = () => [this.threed, this.content];
- videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
- formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
- render() {
- TraceMobx();
- return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
- <div className="videoBox-viewer" >
- <div style={{ position: "relative", height: this.videoPanelHeight() }}>
- <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- PanelHeight={this.videoPanelHeight}
- PanelWidth={this.props.PanelWidth}
- focus={this.props.focus}
- isSelected={this.props.isSelected}
- isAnnotationOverlay={true}
- select={emptyFunction}
- isContentActive={returnFalse}
- scaling={returnOne}
- whenChildContentsActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- {this.contentFunc}
- </CollectionFreeFormView></div>
- <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
- {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) :
- <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
- Document={this.dataDoc[this.fieldKey + "-dictation"]}
- fieldKey={"text"}
- PanelHeight={this.formattedPanelHeight}
- isAnnotationOverlay={true}
- select={emptyFunction}
- isContentActive={emptyFunction}
- scaling={returnOne}
- xPadding={25}
- yPadding={10}
- whenChildContentsActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- </FormattedTextBox>}
- </div>
- </div>
- {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
- <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
- <FontAwesomeIcon icon="file" size="lg" />
- </div>
- </div>}
- </div >;
- }
+ setupDictation = () => {
+ if (this.dataDoc[this.fieldKey + "-dictation"]) return;
+ const dictationText = CurrentUserUtils.GetNewTextDoc("dictation",
+ NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10,
+ NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
+ dictationText._autoHeight = false;
+ const dictationTextProto = Doc.GetProto(dictationText);
+ dictationTextProto.recordingSource = this.dataDoc;
+ dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
+ dictationTextProto.mediaState = ComputedField.MakeFunction("self.recordingSource.mediaState");
+ this.dataDoc[this.fieldKey + "-dictation"] = dictationText;
+ }
+ contentFunc = () => [this.threed, this.content];
+ videoPanelHeight = () => NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], this.layoutDoc[WidthSym]()) * this.props.PanelWidth();
+ formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
+ render() {
+ TraceMobx();
+ return <div className="videoBox" onContextMenu={this.specificContextMenu} style={{ width: "100%", height: "100%" }} >
+ <div className="videoBox-viewer" >
+ <div style={{ position: "relative", height: this.videoPanelHeight() }}>
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ PanelHeight={this.videoPanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ isContentActive={returnFalse}
+ scaling={returnOne}
+ whenChildContentsActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ {this.contentFunc}
+ </CollectionFreeFormView></div>
+ <div style={{ position: "relative", height: this.formattedPanelHeight() }}>
+ {!(this.dataDoc[this.fieldKey + "-dictation"] instanceof Doc) ? (null) :
+ <FormattedTextBox {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ Document={this.dataDoc[this.fieldKey + "-dictation"]}
+ fieldKey={"text"}
+ PanelHeight={this.formattedPanelHeight}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ isContentActive={emptyFunction}
+ scaling={returnOne}
+ xPadding={25}
+ yPadding={10}
+ whenChildContentsActiveChanged={emptyFunction}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ </FormattedTextBox>}
+ </div>
+ </div>
+ {!this.props.isSelected() ? (null) : <div className="screenshotBox-uiButtons">
+ <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
+ <FontAwesomeIcon icon="file" size="lg" />
+ </div>
+ </div>}
+ </div >;
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 3cf10a033..f47a71469 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -81,6 +81,7 @@
align-items: center;
justify-content: center;
display: flex;
+ width: 100%;
visibility: none;
opacity: 0;
background-color: $dark-gray;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index ca8dc8515..e57cb1abe 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -4,10 +4,10 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio
import { observer } from "mobx-react";
import { basename } from "path";
import * as rp from 'request-promise';
-import { Doc, DocListCast } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym } from "../../../fields/Doc";
import { InkTool } from "../../../fields/InkField";
import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { AudioField, VideoField } from "../../../fields/URLField";
+import { AudioField, ImageField, VideoField } from "../../../fields/URLField";
import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from "../../documents/DocumentTypes";
@@ -53,14 +53,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
* @param returnedFilename the base filename to store the image on the server
* @param nosuffix optionally suppress creating multiple resolution images
*/
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false) {
+ public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
try {
const posting = Utils.prepend("/uploadURI");
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
name: returnedFilename,
- nosuffix
+ nosuffix,
+ replaceRootFilename
},
json: true,
});
@@ -220,16 +221,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// creates and links snapshot photo of current video frame
- @action public Snapshot(downX?: number, downY?: number) {
+ @action public Snapshot = (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
const width = NumCast(this.layoutDoc._width);
const canvas = document.createElement('canvas');
canvas.width = 640;
canvas.height = 640 * Doc.NativeHeight(this.layoutDoc) / (Doc.NativeWidth(this.layoutDoc) || 1);
const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
if (ctx) {
- // ctx.rect(0, 0, canvas.width, canvas.height);
- // ctx.fillStyle = "blue";
- // ctx.fill();
this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
}
@@ -259,10 +257,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const encodedFilename = encodeURIComponent("snapshot" + retitled + "_" + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, "_"));
const filename = basename(encodedFilename);
VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) =>
- returnedFilename && this.createRealSummaryLink(returnedFilename, downX, downY));
+ returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
}
}
+ updateIcon = () => {
+ const makeIcon = (returnedfilename: string) => {
+ this.dataDoc.icon = new ImageField(returnedfilename);
+ this.dataDoc["icon-nativeWidth"] = this.layoutDoc[WidthSym]();
+ this.dataDoc["icon-nativeHeight"] = this.layoutDoc[HeightSym]();
+ };
+ this.Snapshot(undefined, undefined, makeIcon);
+ }
+
// creates link for snapshot
createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c12059943..5a39123b9 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -130,8 +130,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.setScrollPos(this._initialScroll);
}
}));
- } else if (!this.props.isContentActive() &&
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && /// don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
+ } else if ((!this.props.isContentActive() || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
+ !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
LightboxView.LightboxDoc !== this.rootDoc) { // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
const imageBitmap = ImageCast(this.layoutDoc["thumb-frozen"])?.url.href;
if (this._iframe && !imageBitmap) {
@@ -150,7 +150,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
NumCast(this.layoutDoc._scrollTop)
).then
((data_url: any) => {
- VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-thumb" + (new Date()).getTime(), true).then(
+ VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then(
returnedfilename => setTimeout(action(() => this.layoutDoc.thumb = new ImageField(returnedfilename)), 500));
})
.catch(function (error: any) {
@@ -164,7 +164,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
autoHeight => {
if (autoHeight) {
this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]);
- this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
}
});
@@ -243,7 +243,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
if (doc !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1);
+ const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * .1, this.getScrollHeight());
if (scrollTo !== undefined) {
const focusSpeed = smooth ? 500 : 0;
this._initialScroll !== undefined && (this._initialScroll = scrollTo);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ec88b8d1d..29117794e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,25 +1,25 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap, selectAll } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
import { Fragment, Mark, Node, Slice } from "prosemirror-model";
-import { ReplaceStep } from 'prosemirror-transform';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc";
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc";
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from "../../../../fields/RichTextField";
import { RichTextUtils } from '../../../../fields/RichTextUtils';
-import { Cast, DateCast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
+import { Cast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../../DocServer";
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -29,6 +29,7 @@ import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from "../../../util/DragManager";
import { makeTemplate } from '../../../util/DropConverter';
+import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from '../../../util/SnappingManager';
import { undoBatch, UndoManager } from "../../../util/UndoManager";
@@ -38,10 +39,11 @@ import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
import { ViewBoxAnnotatableComponent } from "../../DocComponent";
import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
-import { AudioBox } from '../AudioBox';
import { FieldView, FieldViewProps } from "../FieldView";
import { LinkDocPreview } from '../LinkDocPreview';
import { DashDocCommentView } from "./DashDocCommentView";
@@ -60,10 +62,6 @@ import { schema } from "./schema_rts";
import { SummaryView } from "./SummaryView";
import applyDevTools = require("prosemirror-dev-tools");
import React = require("react");
-import { SidebarAnnos } from '../../SidebarAnnos';
-import { Colors } from '../../global/globalEnums';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { LinkManager } from '../../../util/LinkManager';
const translateGoogleApi = require("translate-google-api");
export interface FormattedTextBoxProps {
@@ -221,6 +219,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
return undefined;
});
AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
+ AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
* This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -350,7 +349,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
autoLink = () => {
if (this._editorView) {
const newAutoLinks = new Set<Doc>();
- const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === "automatic");
+ const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
const f = this._editorView.state.selection.from;
const t = this._editorView.state.selection.to;
var tr = this._editorView.state.tr as any;
@@ -364,7 +363,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
updateTitle = () => {
- const title = StrCast(this.dataDoc.title)
+ const title = StrCast(this.dataDoc.title);
if (!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
(title.startsWith("-") || title.startsWith("@")) && this._editorView && !this.dataDoc["title-custom"] &&
(Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === "text")) {
@@ -383,14 +382,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
const editorView = this._editorView;
if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) {
- const flattened1 = this.findInNode(editorView, editorView.state.doc, StrCast(target.title).replace(/^@/, ""));
+ const autoLinkTerm = StrCast(target.title).replace(/^@/, "");
+ const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
var alink: Doc | undefined;
flattened1.forEach((flat, i) => {
- const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, StrCast(target.title).replace(/^@/, ""));
+ const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm);
this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
alink = alink ?? (DocListCast(this.Document.links).find(link =>
Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) &&
- Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, "automatic")!);
+ Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target },
+ LinkManager.AutoKeywords)!);
newAutoLinks.add(alink);
const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
const sel = flattened[i];
@@ -758,7 +759,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
- const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: "#" + this._editorView?.state.doc.textBetween(sel.from, sel.to), unrendered: true });
+ const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: "#" + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -814,6 +815,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
};
let start = 0;
+ this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
if (this._editorView && textAnchorId) {
const editor = this._editorView;
const ret = findAnchorFrag(editor.state.doc.content, editor);
@@ -833,7 +835,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
}
- return this._focusSpeed;
+ return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed
}
// if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
@@ -859,7 +861,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight));
}, { fireImmediately: true });
- this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+ this._disposers.links = reaction(() => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
this._cachedLinks = newLinks;
@@ -1129,6 +1131,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
}
});
}
+ _didScroll = false;
setupEditor(config: any, fieldKey: string) {
const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]);
const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
@@ -1143,7 +1146,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const scrollRef = self._scrollRef.current;
const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined;
const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined;
- if ((topOff || botOff) && scrollRef) {
+ if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) {
const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale;
if (this._focusSpeed !== undefined) {
@@ -1151,6 +1154,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
} else {
scrollRef.scrollTo({ top: scrollPos });
}
+ this._didScroll = true;
}
return true;
},
@@ -1526,7 +1530,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
if (children) {
const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins);
const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
- if (scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ if (this.props.setHeight && scrollHeight && this.props.renderDepth && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => this.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight;
if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 00c03875b..427e05edb 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -275,7 +275,7 @@ export class RichTextRules {
this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
}
const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ""), _width: 500, _height: 500, }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to", undefined);
+ DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, "portal to:portal from", undefined);
const fstate = this.TextBox.EditorView?.state;
if (fstate && selection) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2e312ee51..30ad43562 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -96,7 +96,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable private openMovementDropdown: boolean = false;
@observable private openEffectDropdown: boolean = false;
@observable private presentTools: boolean = false;
- @computed get childDocs() { return DocListCast(this.dataDoc[this.fieldKey]); }
+ @computed get childDocs() { return DocListCast(this.rootDoc[this.fieldKey]); }
@computed get tagDocs() {
const tagDocs: Doc[] = [];
for (const doc of this.childDocs) {
@@ -122,7 +122,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (Doc.UserDoc().activePresentation = this.rootDoc) runInAction(() => PresBox.Instance = this);
if (!this.presElement) { // create exactly one presElmentBox template to use by any and all presentations.
Doc.UserDoc().presElement = new PrefetchProxy(Docs.Create.PresElementBoxDocument({
- title: "pres element template", type: DocumentType.PRESELEMENT, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"
+ title: "pres element template", type: DocumentType.PRESELEMENT, _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: "data"
}));
// this script will be called by each presElement to get rendering-specific info that the PresBox knows about but which isn't written to the PresElement
// this is a design choice -- we could write this data to the presElements which would require a reaction to keep it up to date, and it would prevent
@@ -308,7 +308,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (!group) this._selectedArray.clear();
this.childDocs[index] && this._selectedArray.set(this.childDocs[index], undefined); //Update selected array
- if (this.layoutDoc._viewType === "stacking" && !group) this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list
+ if ([CollectionViewType.Stacking, CollectionViewType.Tree].includes(this.layoutDoc._viewType as any) && !group) this.navigateToElement(this.childDocs[index]); //Handles movement to element only when presTrail is list
this.onHideDocument(); //Handles hide after/before
}
});
@@ -620,9 +620,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//@ts-ignore
const viewType = e.target.selectedOptions[0].value as CollectionViewType;
// pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- viewType === CollectionViewType.Stacking && (this.rootDoc._pivotField = undefined);
+ [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
this.rootDoc._viewType = viewType;
- if (viewType === CollectionViewType.Stacking) this.layoutDoc._gridGap = 0;
+ if ([CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType)) this.layoutDoc._gridGap = 0;
});
/**
@@ -696,8 +696,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
return true;
}
- childLayoutTemplate = () => this.rootDoc._viewType !== CollectionViewType.Stacking ? undefined : this.presElement;
- removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.dataDoc, this.fieldKey, doc);
+ childLayoutTemplate = () => ![CollectionViewType.Stacking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any) ? undefined : this.presElement;
+ removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
isContentActive = (outsideReaction?: boolean) => ((CurrentUserUtils.SelectedTool === InkTool.None && this.props.layerProvider?.(this.layoutDoc) !== false) &&
@@ -2256,13 +2256,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const isMini: boolean = this.toolbarWidth <= 100;
return (
<div className="presBox-buttons" style={{ display: !this.rootDoc._chromeHidden ? "none" : undefined }}>
- {isMini || Doc.UserDoc().noviceMode ? (null) : <select className="presBox-viewPicker"
+ {isMini ? (null) : <select className="presBox-viewPicker"
style={{ display: this.layoutDoc.presStatus === "edit" ? "block" : "none" }}
onPointerDown={e => e.stopPropagation()}
onChange={this.viewChanged}
value={mode}>
<option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
- <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option>
+ <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Tree}>Tree</option>
+ {Doc.UserDoc().noviceMode ? (null) : <option onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel3D}>3D Carousel</option>}
</select>}
<div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}>
<span className={`presBox-button ${this.layoutDoc.presStatus === "edit" ? "present" : ""}`}>
@@ -2459,7 +2460,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
ScriptingGlobals.add(function lookupPresBoxField(container: Doc, field: string, data: Doc) {
if (field === 'indexInPres') return DocListCast(container[StrCast(container.presentationFieldKey)]).indexOf(data);
- if (field === 'presCollapsedHeight') return container._viewType === CollectionViewType.Stacking ? 35 : 31;
+ if (field === 'presCollapsedHeight') return [CollectionViewType.Tree || CollectionViewType.Stacking].includes(container._viewType as any) ? 35 : 31;
if (field === 'presStatus') return container.presStatus;
if (field === '_itemIndex') return container._itemIndex;
if (field === 'presBox') return container;
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 1ad4b820e..a178be910 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -42,7 +42,7 @@ $slide-active: #5B9FDD;
background-color: #d5dce2;
border-radius: 5px;
height: calc(100% - 7px);
- width: calc(100% - 15px);
+ width: 100%;
display: grid;
grid-template-rows: 16px 10px auto;
grid-template-columns: max-content max-content max-content max-content auto;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 766676e95..f4f33482e 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { DataSym, Doc, Opt } from "../../../../fields/Doc";
+import { DataSym, Doc, DocListCast, Opt } from "../../../../fields/Doc";
import { Id } from "../../../../fields/FieldSymbols";
import { Cast, NumCast, StrCast } from "../../../../fields/Types";
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils";
@@ -23,6 +23,7 @@ import { PresBox } from "./PresBox";
import "./PresElementBox.scss";
import { PresMovement } from "./PresEnums";
import React = require("react");
+import { CollectionViewType } from "../../collections/CollectionView";
import { List } from "../../../../fields/List";
/**
* This class models the view a document added to presentation will have in the presentation.
@@ -30,376 +31,388 @@ import { List } from "../../../../fields/List";
*/
@observer
export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
- _heightDisposer: IReactionDisposer | undefined;
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
+ _heightDisposer: IReactionDisposer | undefined;
- @observable _dragging = false;
- // these fields are conditionally computed fields on the layout document that take this document as a parameter
- @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
- @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
- @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
- @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
- @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
- @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
+ @observable _dragging = false;
+ // these fields are conditionally computed fields on the layout document that take this document as a parameter
+ @computed get indexInPres() { return Number(this.lookupField("indexInPres")); } // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
+ @computed get collapsedHeight() { return Number(this.lookupField("presCollapsedHeight")); } // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
+ @computed get presStatus() { return StrCast(this.lookupField("presStatus")); }
+ @computed get itemIndex() { return NumCast(this.lookupField("_itemIndex")); }
+ @computed get presBox() { return Cast(this.lookupField("presBox"), Doc, null); }
+ @computed get targetDoc() { return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc; }
- componentDidMount() {
- this.layoutDoc.hideLinkButton = true;
- this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
- params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
- }
- componentWillUnmount() {
- this._heightDisposer?.();
- }
+ @observable isShowingVideo = false;
+ @action setIsShowingVideo(shown: boolean) {
+ this.isShowingVideo = shown
+ }
- /**
- * Returns a local transformed coordinate array for given coordinates.
- */
- ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
+ componentDidMount() {
+ this.layoutDoc.hideLinkButton = true;
+ this._heightDisposer = reaction(() => [this.rootDoc.presExpandInlineButton, this.collapsedHeight],
+ params => this.layoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._heightDisposer?.();
+ }
- @action
- presExpandDocumentClick = () => {
- this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton;
- }
+ /**
+ * Returns a local transformed coordinate array for given coordinates.
+ */
+ ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
- embedHeight = (): number => 97;
- // embedWidth = () => this.props.PanelWidth();
- // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
- embedWidth = (): number => this.props.PanelWidth() - 35;
- styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
- if (property === StyleProp.Opacity) return 1;
- return this.props.styleProvider?.(doc, props, property);
- }
- /**
- * The function that is responsible for rendering a preview or not for this
- * presentation element.
- */
- @computed get renderEmbeddedInline() {
- return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
- <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
- <DocumentView
- Document={this.targetDoc}
- DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
- styleProvider={this.styleProvider}
- layerProvider={this.props.layerProvider}
- docViewPath={returnEmptyDoclist}
- rootSelected={returnTrue}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- isContentActive={this.props.isContentActive}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- PanelWidth={this.embedWidth}
- PanelHeight={this.embedHeight}
- ScreenToLocalTransform={Transform.Identity}
- moveDocument={this.props.moveDocument!}
- renderDepth={this.props.renderDepth + 1}
- focus={DocUtils.DefaultFocus}
- whenChildContentsActiveChanged={returnFalse}
- bringToFront={returnFalse}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
- searchFilterDocs={this.props.searchFilterDocs}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- hideLinkButton={true}
- />
- <div className="presItem-embeddedMask" />
- </div>;
- }
+ embedHeight = (): number => 97;
+ // embedWidth = () => this.props.PanelWidth();
+ // embedHeight = () => Math.min(this.props.PanelWidth() - 20, this.props.PanelHeight() - this.collapsedHeight);
+ embedWidth = (): number => this.props.PanelWidth() - 35;
+ styleProvider = (doc: (Doc | undefined), props: Opt<DocumentViewProps>, property: string): any => {
+ if (property === StyleProp.Opacity) return 1;
+ return this.props.styleProvider?.(doc, props, property);
+ }
+ /**
+ * The function that is responsible for rendering a preview or not for this
+ * presentation element.
+ */
+ @computed get renderEmbeddedInline() {
+ return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? (null) :
+ <div className="presItem-embedded" style={{ height: this.embedHeight(), width: this.embedWidth() }}>
+ <DocumentView
+ Document={this.targetDoc}
+ DataDoc={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
+ styleProvider={this.styleProvider}
+ layerProvider={this.props.layerProvider}
+ docViewPath={returnEmptyDoclist}
+ rootSelected={returnTrue}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ isContentActive={this.props.isContentActive}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ fitContentsToDoc={returnTrue}
+ PanelWidth={this.embedWidth}
+ PanelHeight={this.embedHeight}
+ ScreenToLocalTransform={Transform.Identity}
+ moveDocument={this.props.moveDocument!}
+ renderDepth={this.props.renderDepth + 1}
+ focus={DocUtils.DefaultFocus}
+ whenChildContentsActiveChanged={returnFalse}
+ bringToFront={returnFalse}
+ docFilters={this.props.docFilters}
+ docRangeFilters={this.props.docRangeFilters}
+ searchFilterDocs={this.props.searchFilterDocs}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ hideLinkButton={true}
+ />
+ <div className="presItem-embeddedMask" />
+ </div>;
+ }
+ @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 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;
+ if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
+ else transitionInS = 0.5;
+ return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
+ }
- @computed get transition() {
- let transitionInS: number;
- if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
- else transitionInS = 0.5;
- return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? (null) : "M: " + transitionInS + "s";
- }
+ private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _titleRef: React.RefObject<EditableView> = React.createRef();
- private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _titleRef: React.RefObject<EditableView> = React.createRef();
+ @action
+ headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ const element = e.target as any;
+ e.stopPropagation();
+ e.preventDefault();
+ if (element && !(e.ctrlKey || e.metaKey)) {
+ if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
+ PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+ setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
+ } else {
+ setupMoveUpEvents(this, e, ((e: PointerEvent) => {
+ PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
+ return this.startDrag(e);
+ }), emptyFunction, emptyFunction);
+ }
+ }
+ }
- @action
- headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
- const element = e.target as any;
- e.stopPropagation();
- e.preventDefault();
- if (element && !(e.ctrlKey || e.metaKey)) {
- if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
- PresBox.Instance._selectedArray.size === 1 && PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
- setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
- } else {
- setupMoveUpEvents(this, e, ((e: PointerEvent) => {
- PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
- return this.startDrag(e);
- }), emptyFunction, emptyFunction);
+ headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ /**
+ * Function to drag and drop the pres element to a diferent location
+ */
+ startDrag = (e: PointerEvent) => {
+ const miniView: boolean = this.toolbarWidth <= 100;
+ const activeItem = this.rootDoc;
+ const dragArray = PresBox.Instance._dragArray;
+ const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
+ if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
+ dragData.dropAction = "move";
+ dragData.treeViewDoc = this.props.docViewPath().lastElement()?.props.treeViewDoc;
+ dragData.moveDocument = this.props.docViewPath().lastElement()?.props.moveDocument;
+ const dragItem: HTMLElement[] = [];
+ if (dragArray.length === 1) {
+ const doc = dragArray[0];
+ doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
+ dragItem.push(doc);
+ } else if (dragArray.length >= 1) {
+ const doc = document.createElement('div');
+ doc.className = "presItem-multiDrag";
+ doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
+ doc.style.position = 'absolute';
+ doc.style.top = (e.clientY) + 'px';
+ doc.style.left = (e.clientX - 50) + 'px';
+ dragItem.push(doc);
}
- }
- }
- headerUp = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
- }
+ // const dropEvent = () => runInAction(() => this._dragging = false);
+ if (activeItem) {
+ DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
+ // runInAction(() => this._dragging = true);
+ return true;
+ }
+ return false;
+ }
- /**
- * Function to drag and drop the pres element to a diferent location
- */
- startDrag = (e: PointerEvent) => {
- const miniView: boolean = this.toolbarWidth <= 100;
- const activeItem = this.rootDoc;
- const dragArray = PresBox.Instance._dragArray;
- const dragData = new DragManager.DocumentDragData(PresBox.Instance.sortArray());
- const dragItem: HTMLElement[] = [];
- if (dragArray.length === 1) {
- const doc = dragArray[0];
- doc.className = miniView ? "presItem-miniSlide" : "presItem-slide";
- dragItem.push(doc);
- } else if (dragArray.length >= 1) {
- const doc = document.createElement('div');
- doc.className = "presItem-multiDrag";
- doc.innerText = "Move " + PresBox.Instance._selectedArray.size + " slides";
- doc.style.position = 'absolute';
- doc.style.top = (e.clientY) + 'px';
- doc.style.left = (e.clientX - 50) + 'px';
- dragItem.push(doc);
- }
+ onPointerOver = (e: any) => {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointermove", this.onPointerMove);
+ }
- // const dropEvent = () => runInAction(() => this._dragging = false);
- if (activeItem) {
- DragManager.StartDocumentDrag(dragItem.map(ele => ele), dragData, e.clientX, e.clientY, undefined);
- // runInAction(() => this._dragging = true);
- return true;
- }
- return false;
- }
+ onPointerMove = (e: PointerEvent) => {
+ const slide = this._itemRef.current!;
+ let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
+ for (const doc of DragManager.docsBeingDragged) {
+ if (!doc.presentationTargetDoc) dragIsPresItem = false;
+ }
+ if (slide && dragIsPresItem) {
+ const rect = slide.getBoundingClientRect();
+ const y = e.clientY - rect.top; //y position within the element.
+ const height = slide.clientHeight;
+ const halfLine = height / 2;
+ if (y <= halfLine) {
+ slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ slide.style.borderBottom = "0px";
+ } else if (y > halfLine) {
+ slide.style.borderTop = "0px";
+ slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ }
+ }
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
- onPointerOver = (e: any) => {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- }
+ onPointerLeave = (e: any) => {
+ this._itemRef.current!.style.borderTop = "0px";
+ this._itemRef.current!.style.borderBottom = "0px";
+ document.removeEventListener("pointermove", this.onPointerMove);
+ }
- onPointerMove = (e: PointerEvent) => {
- const slide = this._itemRef.current!;
- let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false;
- for (const doc of DragManager.docsBeingDragged) {
- if (!doc.presentationTargetDoc) dragIsPresItem = false;
- }
- if (slide && dragIsPresItem) {
- const rect = slide.getBoundingClientRect();
- const y = e.clientY - rect.top; //y position within the element.
- const height = slide.clientHeight;
- const halfLine = height / 2;
- if (y <= halfLine) {
- slide.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
- slide.style.borderBottom = "0px";
- } else if (y > halfLine) {
- slide.style.borderTop = "0px";
- slide.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ @action
+ toggleProperties = () => {
+ if (CurrentUserUtils.propertiesWidth < 5) {
+ action(() => (CurrentUserUtils.propertiesWidth = 250));
}
- }
- document.removeEventListener("pointermove", this.onPointerMove);
- }
+ }
- onPointerLeave = (e: any) => {
- this._itemRef.current!.style.borderTop = "0px";
- this._itemRef.current!.style.borderBottom = "0px";
- document.removeEventListener("pointermove", this.onPointerMove);
- }
+ @undoBatch
+ removeItem = action((e: React.MouseEvent) => {
+ this.props.removeDocument?.(this.rootDoc);
+ if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
+ PresBox.Instance._selectedArray.delete(this.rootDoc);
+ }
+ e.stopPropagation();
+ });
- @action
- toggleProperties = () => {
- if (CurrentUserUtils.propertiesWidth < 5) {
- action(() => (CurrentUserUtils.propertiesWidth = 250));
- }
- }
+ // set the value/title of the individual pres element
+ @undoBatch
+ @action
+ onSetValue = (value: string) => {
+ this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
+ return true;
+ }
- @undoBatch
- removeItem = action((e: React.MouseEvent) => {
- this.props.removeDocument?.(this.rootDoc);
- if (PresBox.Instance._selectedArray.has(this.rootDoc)) {
- PresBox.Instance._selectedArray.delete(this.rootDoc);
- }
- e.stopPropagation();
- });
+ /**
+ * Method called for updating the view of the currently selected document
+ *
+ * @param targetDoc
+ * @param activeItem
+ */
+ @undoBatch
+ @action
+ updateView = (targetDoc: Doc, activeItem: Doc) => {
+ if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
+ const scroll = targetDoc._scrollTop;
+ activeItem.presPinViewScroll = scroll;
+ } else if (targetDoc.type === DocumentType.VID) {
+ activeItem.presPinTimecode = targetDoc._currentTimecode;
+ } else if (targetDoc.type === DocumentType.COMPARISON) {
+ const clipWidth = targetDoc._clipWidth;
+ activeItem.presPinClipWidth = clipWidth;
+ } else {
+ const x = targetDoc._panX;
+ const y = targetDoc._panY;
+ const scale = targetDoc._viewScale;
+ activeItem.presPinViewX = x;
+ activeItem.presPinViewY = y;
+ activeItem.presPinViewScale = scale;
+ }
+ }
- // set the value/title of the individual pres element
- @undoBatch
- @action
- onSetValue = (value: string) => {
- this.rootDoc.title = !value.trim().length ? "-untitled-" : value;
- return true;
- }
+ @undoBatch
+ @action
+ startRecording = (targetDoc: Doc, activeItem: Doc) => {
- /**
- * Method called for updating the view of the currently selected document
- *
- * @param targetDoc
- * @param activeItem
- */
- @undoBatch
- @action
- updateView = (targetDoc: Doc, activeItem: Doc) => {
- if (targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF) {
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- } else if (targetDoc.type === DocumentType.VID) {
- activeItem.presPinTimecode = targetDoc._currentTimecode;
- } else if (targetDoc.type === DocumentType.COMPARISON) {
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- } else {
- const x = targetDoc._panX;
- const y = targetDoc._panY;
- const scale = targetDoc._viewScale;
- activeItem.presPinViewX = x;
- activeItem.presPinViewY = y;
- activeItem.presPinViewScale = scale;
- }
- }
+ // Remove every recording that already exists in overlay view
+ DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => {
+ if (doc.slides !== null) {
+ Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc);
+ }
+ })
- @undoBatch
- @action
- startRecording = (targetDoc: Doc, activeItem: Doc) => {
- // If we already have an existing recording
- // TODO: Remove everything that already exists
- if (activeItem.recording) {
- Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
- // Recording has not yet been created
- } else {
- console.log(targetDoc.title, activeItem.title);
- const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
- activeItem.recording = recording
- // TODO: Figure out exactly where we want the video to appear
- const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- console.log("x: ", pt[0], "y: ", pt[1]);
- recording.x = pt[0];
- recording.y = pt[1];
- Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
- }
-
- }
+ if (activeItem.recording) {
+ // if we already have an existing recording
+ Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null));
- @computed
- get toolbarWidth(): number {
- const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
- let width: number = NumCast(this.presBox._width);
- if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
- if (width === 0) width = 300;
- return width;
- }
+ } else {
+ // if we dont have any recording
+ const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ // attach the recording to the slide, and attach the slide to the recording
+ recording.slides = activeItem
+ activeItem.recording = recording
+ // TODO: Figure out exactly where we want the video to appear
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ recording.x = pt[0];
+ recording.y = pt[1];
+ Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording);
+ }
- @computed get mainItem() {
- const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
- const toolbarWidth: number = this.toolbarWidth;
- const showMore: boolean = this.toolbarWidth >= 300;
- const miniView: boolean = this.toolbarWidth <= 110;
- const presBox: Doc = this.presBox; //presBox
- const presBoxColor: string = StrCast(presBox._backgroundColor);
- const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
- const targetDoc: Doc = this.targetDoc;
- const activeItem: Doc = this.rootDoc;
- return (
- <div className={`presItem-container`}
- key={this.props.Document[Id] + this.indexInPres}
- ref={this._itemRef}
- style={{
- backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
- opacity: this._dragging ? 0.3 : 1
- }}
- onClick={e => {
- e.stopPropagation();
- e.preventDefault();
- PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
- }}
- onDoubleClick={action(e => {
- this.toggleProperties();
- PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
- })}
- onPointerOver={this.onPointerOver}
- onPointerLeave={this.onPointerLeave}
- onPointerDown={this.headerDown}
- onPointerUp={this.headerUp}
- >
- {miniView ?
- // when width is LESS than 110 px
- <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
- {`${this.indexInPres + 1}.`}
- </div>
- :
- // when width is MORE than 110 px
- <div className="presItem-number">
- {`${this.indexInPres + 1}.`}
- </div>}
- {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
- style={{
- backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
- }}>
- <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
- <EditableView
- ref={this._titleRef}
- editing={!isSelected ? false : undefined}
- contents={activeItem.title}
- overflow={'ellipsis'}
- GetValue={() => StrCast(activeItem.title)}
- SetValue={this.onSetValue}
- />
- </div>
- <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
- <div className={"presItem-slideButtons"}>
- <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
- <div className="slideButton"
- onClick={() => this.updateView(targetDoc, activeItem)}
- style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
- </Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
- <div className="slideButton"
- onClick={() => this.startRecording(targetDoc, activeItem)}
- style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
- <div className="slideButton"
- onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
- style={{
- zIndex: 1000 - this.indexInPres,
- fontWeight: 700,
- backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
- height: activeItem.groupWithUp ? 53 : 18,
- transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
- }}>
- <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
- <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
- </div>
- </div>
- </Tooltip>}
- <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
- <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
- className={"slideButton"}
- onClick={this.removeItem}>
- <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
- </div></Tooltip>
- </div>
- <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
- {this.renderEmbeddedInline}
- </div>}
- </div >);
- }
+ }
+
+ @computed
+ get toolbarWidth(): number {
+ const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
+ let width: number = NumCast(this.presBox._width);
+ if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
+ if (width === 0) width = 300;
+ return width;
+ }
+
+ @computed get mainItem() {
+ const isSelected: boolean = PresBox.Instance._selectedArray.has(this.rootDoc);
+ const toolbarWidth: number = this.toolbarWidth;
+ const showMore: boolean = this.toolbarWidth >= 300;
+ const miniView: boolean = this.toolbarWidth <= 110;
+ const presBox: Doc = this.presBox; //presBox
+ const presBoxColor: string = StrCast(presBox._backgroundColor);
+ const presColorBool: boolean = presBoxColor ? (presBoxColor !== Colors.WHITE && presBoxColor !== "transparent") : false;
+ const targetDoc: Doc = this.targetDoc;
+ const activeItem: Doc = this.rootDoc;
+ return (
+ <div className={`presItem-container`}
+ key={this.props.Document[Id] + this.indexInPres}
+ ref={this._itemRef}
+ style={{
+ backgroundColor: presColorBool ? isSelected ? "rgba(250,250,250,0.3)" : "transparent" : isSelected ? Colors.LIGHT_BLUE : "transparent",
+ opacity: this._dragging ? 0.3 : 1
+ }}
+ onClick={e => {
+ e.stopPropagation();
+ e.preventDefault();
+ PresBox.Instance.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+ }}
+ onDoubleClick={action(e => {
+ this.toggleProperties();
+ PresBox.Instance.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+ })}
+ onPointerOver={this.onPointerOver}
+ onPointerLeave={this.onPointerLeave}
+ onPointerDown={this.headerDown}
+ onPointerUp={this.headerUp}
+ >
+ {miniView ?
+ // when width is LESS than 110 px
+ <div className={`presItem-miniSlide ${isSelected ? "active" : ""}`} ref={miniView ? this._dragRef : null}>
+ {`${this.indexInPres + 1}.`}
+ </div>
+ :
+ // when width is MORE than 110 px
+ <div className="presItem-number">
+ {`${this.indexInPres + 1}.`}
+ </div>}
+ {miniView ? (null) : <div ref={miniView ? null : this._dragRef} className={`presItem-slide ${isSelected ? "active" : ""}`}
+ style={{
+ backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+ boxShadow: presBoxColor && presBoxColor !== "white" && presBoxColor !== "transparent" ? isSelected ? "0 0 0px 1.5px" + presBoxColor : undefined : undefined
+ }}>
+ <div className="presItem-name" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105, cursor: isSelected ? 'text' : 'grab' }}>
+ <EditableView
+ ref={this._titleRef}
+ editing={!isSelected ? false : undefined}
+ contents={activeItem.title}
+ overflow={'ellipsis'}
+ GetValue={() => StrCast(activeItem.title)}
+ SetValue={this.onSetValue}
+ />
+ </div>
+ <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip>
+ <div className={"presItem-slideButtons"}>
+ <Tooltip title={<><div className="dash-tooltip">{"Update view"}</div></>}>
+ <div className="slideButton"
+ onClick={() => this.updateView(targetDoc, activeItem)}
+ style={{ fontWeight: 700, display: activeItem.presPinView ? "flex" : "none" }}>V</div>
+ </Tooltip>
+ <Tooltip title={<><div className="dash-tooltip">{"Start recording"}</div></>}>
+ <div className="slideButton"
+ onClick={() => this.startRecording(targetDoc, activeItem)}
+ style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={"video"} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ {this.indexInPres === 0 ? (null) : <Tooltip title={<><div className="dash-tooltip">{activeItem.groupWithUp ? "Ungroup" : "Group with up"}</div></>}>
+ <div className="slideButton"
+ onClick={() => activeItem.groupWithUp = !activeItem.groupWithUp}
+ style={{
+ zIndex: 1000 - this.indexInPres,
+ fontWeight: 700,
+ backgroundColor: activeItem.groupWithUp ? presColorBool ? presBoxColor : Colors.MEDIUM_BLUE : undefined,
+ height: activeItem.groupWithUp ? 53 : 18,
+ transform: activeItem.groupWithUp ? "translate(0, -17px)" : undefined
+ }}>
+ <div style={{ transform: activeItem.groupWithUp ? "rotate(180deg) translate(0, -17.5px)" : "rotate(0deg)" }}>
+ <FontAwesomeIcon icon={"arrow-up"} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </div>
+ </Tooltip>}
+ {/* <Tooltip title={<><div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}</div></>}><div className={"slideButton"} onClick={e => { e.stopPropagation(); this.presExpandDocumentClick(); }}>
+ <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? "eye-slash" : "eye"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip> */}
+ <Tooltip title={<><div className="dash-tooltip">{"Remove from presentation"}</div></>}><div
+ className={"slideButton"}
+ onClick={this.removeItem}>
+ <FontAwesomeIcon icon={"trash"} onPointerDown={e => e.stopPropagation()} />
+ </div></Tooltip>
+ </div>
+ <div className="presItem-docName" style={{ maxWidth: showMore ? (toolbarWidth - 195) : toolbarWidth - 105 }}>{activeItem.presPinView ? (<><i>View of </i> {targetDoc.title}</>) : targetDoc.title}</div>
+ {this.renderEmbeddedInline}
+ </div>}
+ </div >);
+ }
- render() {
- return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
- }
+ render() {
+ return !(this.rootDoc instanceof Doc) || this.targetDoc instanceof Promise ? (null) : this.mainItem;
+ }
} \ No newline at end of file
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index ad3afb775..29d068817 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -46,8 +46,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
+ public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
public OnClick: (e: PointerEvent) => void = unimplementedFunction;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
+ public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
public Delete: () => void = unimplementedFunction;
@@ -79,6 +81,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
}, returnFalse, e => this.OnClick?.(e));
}
+ cropDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e: PointerEvent) => {
+ this.StartCropDrag(e, this._commentCont.current!);
+ return true;
+ }, returnFalse, e => this.OnCrop?.(e));
+ }
+
@action
highlightClicked = (e: React.MouseEvent) => {
if (!this.Highlight(this.highlightColor, false) && this.Pinned) {
@@ -161,7 +170,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
<FontAwesomeIcon style={{ position: "absolute", transform: "scale(0.5)", transformOrigin: "top left", top: 12, left: 12 }} icon={"link"} size="lg" />
</button>
</Tooltip>,
- <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />
+ <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />,
+ AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? <></> : <Tooltip key="crop" title={<div className="dash-tooltip">{"Click/Drag to create cropped image"}</div>}>
+ <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: "grab" }}>
+ <FontAwesomeIcon icon="image" size="lg" />
+ </button>
+ </Tooltip>,
] : [
<Tooltip key="trash" title={<div className="dash-tooltip">{"Remove Link Anchor"}</div>}>
<button className="antimodeMenu-button" onPointerDown={this.Delete}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 651b0401b..b55b2cf44 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -74,7 +74,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = "";
private _downX: number = 0;
private _downY: number = 0;
@@ -123,7 +123,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
autoHeight => {
if (autoHeight) {
this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]);
- this.props.setHeight(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + "-nativeHeight"]) * (this.props.scaling?.() || 1));
}
});
@@ -167,7 +167,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
});
if (i === this.props.pdf.numPages - 1) {
this.props.loaded?.(page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1],
- page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], i);
+ page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], this.props.pdf.numPages);
}
}))));
this.props.Document.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
@@ -181,7 +181,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
let focusSpeed: Opt<number>;
if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.scaling?.() || 1);
- const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight);
+ const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, .1 * windowHeight, NumCast(this.props.Document.scrollHeight));
if (scrollTo !== undefined) {
focusSpeed = 500;
@@ -299,9 +299,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
gotoPage = (p: number) => {
- if (this._pdfViewer?._setDocumentViewerElement?.offsetParent) {
- this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
- }
+ this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
@action
@@ -419,7 +417,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
const clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
const rect = clientRects.item(i);
- if (rect && rect.width !== this._mainCont.current.clientWidth) {
+ if (rect && rect.width !== this._mainCont.current.clientWidth && rect.width) {
const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
const annoBox = document.createElement("div");
annoBox.className = "marqueeAnnotator-annotationBox";
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 5fe2a5ab1..9257cb75e 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -112,7 +112,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
if (this.props.linkFrom) {
const linkFrom = this.props.linkFrom();
if (linkFrom) {
- DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }, "Link");
+ DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
}
}
});
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index d5254e315..0af7de6af 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -7,7 +7,7 @@ import { StrCast } from '../../../fields/Types';
import { Utils } from '../../../Utils';
import { CurrentUserUtils } from "../../util/CurrentUserUtils";
import { SettingsManager } from "../../util/SettingsManager";
-import { undoBatch } from "../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
import { Borders, Colors } from "../global/globalEnums";
import "./TopBar.scss";
@@ -33,30 +33,36 @@ export class TopBar extends React.Component {
</div>
<div className="topbar-center" >
<div className="topbar-lozenge-dashboard">
- <select className="topbar-dashSelect" onChange={e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)])}
+ <select className="topbar-dashSelect" onChange={undoBatch(e => CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)]))}
value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)}
style={{ color: Colors.WHITE }}>
{myDashboards.map((dash, i) => <option key={dash[Id]} value={i}> {StrCast(dash.title)} </option>)}
</select>
</div>
<div className="topbar-dashboards">
- <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.createNewDashboard(Doc.UserDoc()))}
- >
- {"New"}<FontAwesomeIcon icon="plus"></FontAwesomeIcon>
+ <div className="topbar-icon" onClick={async () => {
+ const batch = UndoManager.StartBatch("new dash");
+ await CurrentUserUtils.createNewDashboard(Doc.UserDoc());
+ batch.end();
+ }}>
+ {"New"}<FontAwesomeIcon icon="plus" />
</div>
- {Doc.UserDoc().noviceMode ? (null) : <div className="topbar-icon" onClick={undoBatch(() => CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))}
- >
- {"Snapshot"}<FontAwesomeIcon icon="camera"></FontAwesomeIcon>
+ {<div className="topbar-icon" onClick={async () => {
+ const batch = UndoManager.StartBatch("snapshot");
+ await CurrentUserUtils.snapshotDashboard(Doc.UserDoc());
+ batch.end();
+ }}>
+ {"Snapshot"}<FontAwesomeIcon icon="camera" />
</div>}
</div>
</div>
<div className="topbar-right" >
<div className="topbar-icon" onClick={() => window.open(
"https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
- {"Help"}<FontAwesomeIcon icon="question-circle"></FontAwesomeIcon>
+ {"Help"}<FontAwesomeIcon icon="question-circle" />
</div>
<div className="topbar-icon" onClick={() => SettingsManager.Instance.open()}>
- {"Settings"}<FontAwesomeIcon icon="cog"></FontAwesomeIcon>
+ {"Settings"}<FontAwesomeIcon icon="cog" />
</div>
</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ebdbae344..50e5fcbc4 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -11,7 +11,7 @@ import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGloba
import { SelectionManager } from "../client/util/SelectionManager";
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
import { UndoManager } from "../client/util/UndoManager";
-import { DashColor, intersectRect, Utils } from "../Utils";
+import { DashColor, incrementTitleCopy, intersectRect, Utils } from "../Utils";
import { DateField } from "./DateField";
import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
import { List } from "./List";
@@ -770,7 +770,7 @@ export namespace Doc {
return overwrite;
}
- export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
+ export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
const copy = new Doc(copyProtoId, true);
const exclude = Cast(doc.cloneFieldFilter, listSpec("string"), []);
Object.keys(doc).forEach(key => {
@@ -806,6 +806,9 @@ export namespace Doc {
}
copy.context = undefined;
Doc.UserDoc().defaultAclPrivate && (copy["acl-Public"] = "Not Shared");
+ if (retitle) {
+ copy.title = incrementTitleCopy(StrCast(copy.title));
+ }
return copy;
}
@@ -1108,7 +1111,7 @@ export namespace Doc {
if (typeof value === "string") {
value = value.replace(`,${Utils.noRecursionHack}`, "");
}
- const fieldVal = doc[key];
+ const fieldVal = key === "#" ? (StrCast(doc.tags).includes(":#" + value + ":") ? StrCast(doc.tags) : undefined) : doc[key];
if (Cast(fieldVal, listSpec("string"), []).length) {
const vals = Cast(fieldVal, listSpec("string"), []);
const docs = vals.some(v => (v as any) instanceof Doc);
@@ -1119,7 +1122,7 @@ export namespace Doc {
return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
- export function deiconifyView(doc: any) {
+ export function deiconifyView(doc: Doc) {
StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index bfa07d47a..634548154 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -3,7 +3,7 @@ import { Method, _success } from "../RouteManager";
import * as formidable from 'formidable';
import v4 = require('uuid/v4');
const AdmZip = require('adm-zip');
-import { extname, basename, dirname } from 'path';
+import { extname, basename, dirname, } from 'path';
import { createReadStream, createWriteStream, unlink, writeFile } from "fs";
import { publicDirectory, filesDirectory } from "..";
import { Database } from "../database";
@@ -13,294 +13,298 @@ import { AcceptableMedia, Upload } from "../SharedMediaTypes";
import { normalize } from "path";
import RouteSubscriber from "../RouteSubscriber";
const imageDataUri = require('image-data-uri');
-import { isWebUri } from "valid-url";
-import { Opt } from "../../fields/Doc";
import { SolrManager } from "./SearchManager";
-import { StringDecoder } from "string_decoder";
+const fs = require('fs');
export enum Directory {
- parsed_files = "parsed_files",
- images = "images",
- videos = "videos",
- pdfs = "pdfs",
- text = "text",
- pdf_thumbnails = "pdf_thumbnails",
- audio = "audio",
+ parsed_files = "parsed_files",
+ images = "images",
+ videos = "videos",
+ pdfs = "pdfs",
+ text = "text",
+ pdf_thumbnails = "pdf_thumbnails",
+ audio = "audio",
}
export function serverPathToFile(directory: Directory, filename: string) {
- return normalize(`${filesDirectory}/${directory}/${filename}`);
+ return normalize(`${filesDirectory}/${directory}/${filename}`);
}
export function pathToDirectory(directory: Directory) {
- return normalize(`${filesDirectory}/${directory}`);
+ return normalize(`${filesDirectory}/${directory}`);
}
export function clientPathToFile(directory: Directory, filename: string) {
- return `/files/${directory}/${filename}`;
+ return `/files/${directory}/${filename}`;
}
export default class UploadManager extends ApiManager {
- protected initialize(register: Registration): void {
+ protected initialize(register: Registration): void {
- register({
- method: Method.POST,
- subscription: "/uploadFormData",
- secureHandler: async ({ req, res }) => {
- const form = new formidable.IncomingForm();
- form.keepExtensions = true;
- form.uploadDir = pathToDirectory(Directory.parsed_files);
- return new Promise<void>(resolve => {
- form.parse(req, async (_err, _fields, files) => {
- const results: Upload.FileResponse[] = [];
- for (const key in files) {
- const f = files[key];
- if (!Array.isArray(f)) {
- const result = await DashUploadUtils.upload(f);
- result && !(result.result instanceof Error) && results.push(result);
- }
- }
- _success(res, results);
- resolve();
- });
- });
- }
- });
+ register({
+ method: Method.POST,
+ subscription: "/uploadFormData",
+ secureHandler: async ({ req, res }) => {
+ const form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ form.uploadDir = pathToDirectory(Directory.parsed_files);
+ return new Promise<void>(resolve => {
+ form.parse(req, async (_err, _fields, files) => {
+ const results: Upload.FileResponse[] = [];
+ for (const key in files) {
+ const f = files[key];
+ if (!Array.isArray(f)) {
+ const result = await DashUploadUtils.upload(f);
+ result && !(result.result instanceof Error) && results.push(result);
+ }
+ }
+ _success(res, results);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadYoutubeVideo",
- secureHandler: async ({ req, res }) => {
- //req.readableBuffer.head.data
- return new Promise<void>(async resolve => {
- req.addListener("data", async (args) => {
- console.log(args);
- const payload = String.fromCharCode.apply(String, args);
- const videoId = JSON.parse(payload).videoId;
- const results: Upload.FileResponse[] = [];
- const result = await DashUploadUtils.uploadYoutube(videoId);
- result && !(result.result instanceof Error) && results.push(result);
- _success(res, results);
- resolve();
- });
- });
- }
- });
+ register({
+ method: Method.POST,
+ subscription: "/uploadYoutubeVideo",
+ secureHandler: async ({ req, res }) => {
+ //req.readableBuffer.head.data
+ return new Promise<void>(async resolve => {
+ req.addListener("data", async (args) => {
+ console.log(args);
+ const payload = String.fromCharCode.apply(String, args);
+ const videoId = JSON.parse(payload).videoId;
+ const results: Upload.FileResponse[] = [];
+ const result = await DashUploadUtils.uploadYoutube(videoId);
+ result && !(result.result instanceof Error) && results.push(result);
+ _success(res, results);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: new RouteSubscriber("youtubeScreenshot"),
- secureHandler: async ({ req, res }) => {
- const { id, timecode } = req.body;
- const convert = (raw: string) => {
- const number = Math.floor(Number(raw));
- const seconds = number % 60;
- const minutes = (number - seconds) / 60;
- return `${minutes}m${seconds}s`;
- };
- const suffix = timecode ? `&t=${convert(timecode)}` : ``;
- const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
- const buffer = await captureYoutubeScreenshot(targetUrl);
- if (!buffer) {
- return res.send();
- }
- const resolvedName = `youtube_capture_${id}_${suffix}.png`;
- const resolvedPath = serverPathToFile(Directory.images, resolvedName);
- return new Promise<void>(resolve => {
- writeFile(resolvedPath, buffer, async error => {
- if (error) {
- return res.send();
+ register({
+ method: Method.POST,
+ subscription: new RouteSubscriber("youtubeScreenshot"),
+ secureHandler: async ({ req, res }) => {
+ const { id, timecode } = req.body;
+ const convert = (raw: string) => {
+ const number = Math.floor(Number(raw));
+ const seconds = number % 60;
+ const minutes = (number - seconds) / 60;
+ return `${minutes}m${seconds}s`;
+ };
+ const suffix = timecode ? `&t=${convert(timecode)}` : ``;
+ const targetUrl = `https://www.youtube.com/watch?v=${id}${suffix}`;
+ const buffer = await captureYoutubeScreenshot(targetUrl);
+ if (!buffer) {
+ return res.send();
}
- await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
- res.send({
- accessPaths: {
- agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
- }
- } as Upload.FileInformation);
- resolve();
- });
- });
- }
- });
+ const resolvedName = `youtube_capture_${id}_${suffix}.png`;
+ const resolvedPath = serverPathToFile(Directory.images, resolvedName);
+ return new Promise<void>(resolve => {
+ writeFile(resolvedPath, buffer, async error => {
+ if (error) {
+ return res.send();
+ }
+ await DashUploadUtils.outputResizedImages(() => createReadStream(resolvedPath), resolvedName, pathToDirectory(Directory.images));
+ res.send({
+ accessPaths: {
+ agnostic: DashUploadUtils.getAccessPaths(Directory.images, resolvedName)
+ }
+ } as Upload.FileInformation);
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadRemoteImage",
- secureHandler: async ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/uploadRemoteImage",
+ secureHandler: async ({ req, res }) => {
- const { sources } = req.body;
- if (Array.isArray(sources)) {
- const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
- return res.send(results);
- }
- res.send();
- }
- });
+ const { sources } = req.body;
+ if (Array.isArray(sources)) {
+ const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
+ return res.send(results);
+ }
+ res.send();
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadDoc",
- secureHandler: ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/uploadDoc",
+ secureHandler: ({ req, res }) => {
- const form = new formidable.IncomingForm();
- form.keepExtensions = true;
- // let path = req.body.path;
- const ids: { [id: string]: string } = {};
- let remap = true;
- const getId = (id: string): string => {
- if (!remap) return id;
- if (id.endsWith("Proto")) return id;
- if (id in ids) {
- return ids[id];
- } else {
- return ids[id] = v4();
- }
- };
- const mapFn = (doc: any) => {
- if (doc.id) {
- doc.id = getId(doc.id);
- }
- for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) { continue; }
- const field = doc.fields[key];
- if (field === undefined || field === null) { continue; }
+ const form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ // let path = req.body.path;
+ const ids: { [id: string]: string } = {};
+ let remap = true;
+ const getId = (id: string): string => {
+ if (!remap) return id;
+ if (id.endsWith("Proto")) return id;
+ if (id in ids) {
+ return ids[id];
+ } else {
+ return ids[id] = v4();
+ }
+ };
+ const mapFn = (doc: any) => {
+ if (doc.id) {
+ doc.id = getId(doc.id);
+ }
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) { continue; }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) { continue; }
- if (field.__type === "Doc") {
- mapFn(field);
- } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
- field.fieldId = getId(field.fieldId);
- } else if (field.__type === "script" || field.__type === "computed") {
- if (field.captures) {
- field.captures.fieldId = getId(field.captures.fieldId);
- }
- } else if (field.__type === "list") {
- mapFn(field);
- } else if (typeof field === "string") {
- const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
- doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- } else if (field.__type === "RichTextField") {
- const re = /("href"\s*:\s*")(.*?)"/g;
- field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- }
- }
- };
- return new Promise<void>(resolve => {
- form.parse(req, async (_err, fields, files) => {
- remap = fields.remap !== "false";
- let id: string = "";
- try {
- for (const name in files) {
- const f = files[name];
- const path_2 = Array.isArray(f) ? "" : f.path;
- const zip = new AdmZip(path_2);
- zip.getEntries().forEach((entry: any) => {
- if (!entry.entryName.startsWith("files/")) return;
- let directory = dirname(entry.entryName) + "/";
- const extension = extname(entry.entryName);
- const base = basename(entry.entryName).split(".")[0];
+ if (field.__type === "Doc") {
+ mapFn(field);
+ } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ field.fieldId = getId(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ field.captures.fieldId = getId(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ mapFn(field);
+ } else if (typeof field === "string") {
+ const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+ doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ } else if (field.__type === "RichTextField") {
+ const re = /("href"\s*:\s*")(.*?)"/g;
+ field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ }
+ }
+ };
+ return new Promise<void>(resolve => {
+ form.parse(req, async (_err, fields, files) => {
+ remap = fields.remap !== "false";
+ let id: string = "";
try {
- zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
- directory = "/" + directory;
+ for (const name in files) {
+ const f = files[name];
+ const path_2 = Array.isArray(f) ? "" : f.path;
+ const zip = new AdmZip(path_2);
+ zip.getEntries().forEach((entry: any) => {
+ if (!entry.entryName.startsWith("files/")) return;
+ let directory = dirname(entry.entryName) + "/";
+ const extension = extname(entry.entryName);
+ const base = basename(entry.entryName).split(".")[0];
+ try {
+ zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+ directory = "/" + directory;
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
- } catch (e) {
- console.log(e);
- }
- });
- const json = zip.getEntry("doc.json");
- try {
- const data = JSON.parse(json.getData().toString("utf8"));
- const datadocs = data.docs;
- id = getId(data.id);
- const docs = Object.keys(datadocs).map(key => datadocs[key]);
- docs.forEach(mapFn);
- await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
- Database.Instance.replace(doc.id, doc, (err, r) => {
- err && console.log(err);
- res();
- }, true);
- })));
- } catch (e) { console.log(e); }
- unlink(path_2, () => { });
- }
- SolrManager.update();
- res.send(JSON.stringify(id || "error"));
- } catch (e) { console.log(e); }
- resolve();
- });
- });
- }
- });
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
+ } catch (e) {
+ console.log(e);
+ }
+ });
+ const json = zip.getEntry("doc.json");
+ try {
+ const data = JSON.parse(json.getData().toString("utf8"));
+ const datadocs = data.docs;
+ id = getId(data.id);
+ const docs = Object.keys(datadocs).map(key => datadocs[key]);
+ docs.forEach(mapFn);
+ await Promise.all(docs.map((doc: any) => new Promise<void>(res => {
+ Database.Instance.replace(doc.id, doc, (err, r) => {
+ err && console.log(err);
+ res();
+ }, true);
+ })));
+ } catch (e) { console.log(e); }
+ unlink(path_2, () => { });
+ }
+ SolrManager.update();
+ res.send(JSON.stringify(id || "error"));
+ } catch (e) { console.log(e); }
+ resolve();
+ });
+ });
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/inspectImage",
- secureHandler: async ({ req, res }) => {
+ register({
+ method: Method.POST,
+ subscription: "/inspectImage",
+ secureHandler: async ({ req, res }) => {
- const { source } = req.body;
- if (typeof source === "string") {
- return res.send(await DashUploadUtils.InspectImage(source));
- }
- res.send({});
- }
- });
+ const { source } = req.body;
+ if (typeof source === "string") {
+ return res.send(await DashUploadUtils.InspectImage(source));
+ }
+ res.send({});
+ }
+ });
- register({
- method: Method.POST,
- subscription: "/uploadURI",
- secureHandler: ({ req, res }) => {
- const uri = req.body.uri;
- const filename = req.body.name;
- const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
- if (!uri || !filename) {
- res.status(401).send("incorrect parameters specified");
- return;
- }
- return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
- const ext = extname(savedName).toLowerCase();
- const { pngs, jpgs } = AcceptableMedia;
- const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
- ];
- let isImage = false;
- if (pngs.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.png();
- });
- isImage = true;
- } else if (jpgs.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.jpeg();
- });
- isImage = true;
- }
- if (isImage) {
- resizers.forEach(resizer => {
- const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
- createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
- });
+ register({
+ method: Method.POST,
+ subscription: "/uploadURI",
+ secureHandler: ({ req, res }) => {
+ const uri = req.body.uri;
+ const filename = req.body.name;
+ const origSuffix = req.body.nosuffix ? SizeSuffix.None : SizeSuffix.Original;
+ const deleteFiles = req.body.replaceRootFilename;
+ if (!uri || !filename) {
+ res.status(401).send("incorrect parameters specified");
+ return;
+ }
+ if (deleteFiles) {
+ const path = serverPathToFile(Directory.images, "");
+ const regex = new RegExp(`${deleteFiles}.*`);
+ fs.readdirSync(path).filter((f: any) => regex.test(f)).map((f: any) => fs.unlinkSync(path + f));
+ }
+ return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
+ const ext = extname(savedName).toLowerCase();
+ const { pngs, jpgs } = AcceptableMedia;
+ const resizers = !origSuffix ? [{ resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" }] : [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngs.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgs.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
+ createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
+ });
- }
- res.send(clientPathToFile(Directory.images, filename + ext));
- });
- }
- });
+ }
+ res.send(clientPathToFile(Directory.images, filename + ext));
+ });
+ }
+ });
- }
+ }
}
function delay(ms: number) {
- return new Promise(resolve => setTimeout(resolve, ms));
+ return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* On success, returns a buffer containing the bytes of a screenshot
@@ -309,25 +313,25 @@ function delay(ms: number) {
* On failure, returns undefined.
*/
async function captureYoutubeScreenshot(targetUrl: string) {
- // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
- // const page = await browser.newPage();
- // // await page.setViewport({ width: 1920, height: 1080 });
+ // const browser = await launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] });
+ // const page = await browser.newPage();
+ // // await page.setViewport({ width: 1920, height: 1080 });
- // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
+ // // await page.goto(targetUrl, { waitUntil: 'domcontentloaded' as any });
- // const videoPlayer = await page.$('.html5-video-player');
- // videoPlayer && await page.focus("video");
- // await delay(7000);
- // const ad = await page.$('.ytp-ad-skip-button-text');
- // await ad?.click();
- // await videoPlayer?.click();
- // await delay(1000);
- // // hide youtube player controls.
- // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
+ // const videoPlayer = await page.$('.html5-video-player');
+ // videoPlayer && await page.focus("video");
+ // await delay(7000);
+ // const ad = await page.$('.ytp-ad-skip-button-text');
+ // await ad?.click();
+ // await videoPlayer?.click();
+ // await delay(1000);
+ // // hide youtube player controls.
+ // await page.evaluate(() => (document.querySelector('.ytp-chrome-bottom') as HTMLElement).style.display = 'none');
- // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
- // await browser.close();
+ // const buffer = await videoPlayer?.screenshot({ encoding: "binary" });
+ // await browser.close();
- // return buffer;
- return null;
+ // return buffer;
+ return null;
} \ No newline at end of file
diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts
index 12064668b..8339a06dc 100644
--- a/src/server/DashSession/Session/agents/applied_session_agent.ts
+++ b/src/server/DashSession/Session/agents/applied_session_agent.ts
@@ -9,8 +9,8 @@ export abstract class AppliedSessionAgent {
// the following two methods allow the developer to create a custom
// session and use the built in customization options for each thread
- protected abstract async initializeMonitor(monitor: Monitor): Promise<string>;
- protected abstract async initializeServerWorker(): Promise<ServerWorker>;
+ protected abstract initializeMonitor(monitor: Monitor): Promise<string>;
+ protected abstract initializeServerWorker(): Promise<ServerWorker>;
private launched = false;
diff --git a/src/server/database.ts b/src/server/database.ts
index 2a55c14de..725b66836 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -34,7 +34,13 @@ export namespace Database {
console.log(`mongoose established default connection at ${url}`);
resolve();
});
- mongoose.connect(url, { useNewUrlParser: true, useUnifiedTopology: true, dbName: schema });
+ mongoose.connect(url, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ dbName: schema,
+ // reconnectTries: Number.MAX_VALUE,
+ // reconnectInterval: 1000,
+ });
});
}
} catch (e) {