aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json10
-rw-r--r--package.json2
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts20
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/util/LinkManager.ts6
-rw-r--r--src/client/util/SelectionManager.ts5
-rw-r--r--src/client/views/DocComponent.tsx18
-rw-r--r--src/client/views/DocumentDecorations.scss6
-rw-r--r--src/client/views/DocumentDecorations.tsx51
-rw-r--r--src/client/views/InkControlPtHandles.tsx26
-rw-r--r--src/client/views/InkStroke.scss31
-rw-r--r--src/client/views/InkStrokeProperties.ts150
-rw-r--r--src/client/views/InkTangentHandles.tsx6
-rw-r--r--src/client/views/InkingStroke.tsx90
-rw-r--r--src/client/views/MainView.tsx6
-rw-r--r--src/client/views/OverlayView.tsx2
-rw-r--r--src/client/views/Palette.tsx2
-rw-r--r--src/client/views/PropertiesView.tsx2
-rw-r--r--src/client/views/StyleProvider.tsx4
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx5
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx12
-rw-r--r--src/client/views/collections/CollectionTreeView.scss9
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx244
-rw-r--r--src/client/views/collections/CollectionView.tsx12
-rw-r--r--src/client/views/collections/TabDocView.tsx3
-rw-r--r--src/client/views/collections/TreeView.scss7
-rw-r--r--src/client/views/collections/TreeView.tsx57
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx40
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx22
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx2
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.scss2
-rw-r--r--src/client/views/nodes/DocumentView.tsx25
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/FilterBox.tsx2
-rw-r--r--src/client/views/nodes/LinkBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.scss10
-rw-r--r--src/client/views/nodes/VideoBox.tsx11
-rw-r--r--src/client/views/nodes/WebBox.tsx77
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx36
-rw-r--r--src/mobile/AudioUpload.tsx2
-rw-r--r--src/mobile/MobileInterface.tsx2
-rw-r--r--src/server/DashUploadUtils.ts7
-rw-r--r--src/server/SharedMediaTypes.ts2
-rw-r--r--src/server/server_Initialization.ts115
54 files changed, 730 insertions, 445 deletions
diff --git a/package-lock.json b/package-lock.json
index c1dd8506f..128d6fba8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6092,6 +6092,11 @@
}
}
},
+ "exifr": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
+ "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
+ },
"expand-brackets": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -9264,6 +9269,11 @@
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"optional": true
},
+ "memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI="
+ },
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
diff --git a/package.json b/package.json
index 5d10c0d54..4f12f01d6 100644
--- a/package.json
+++ b/package.json
@@ -164,6 +164,7 @@
"depcheck": "^0.9.2",
"equation-editor-react": "github:bobzel/equation-editor-react#useLocally",
"exif": "^0.6.0",
+ "exifr": "^7.1.3",
"express": "^4.16.4",
"express-flash": "0.0.2",
"express-session": "^1.17.0",
@@ -194,6 +195,7 @@
"libxmljs": "^0.19.7",
"lodash": "^4.17.15",
"material-ui": "^0.20.2",
+ "memorystream": "^0.3.1",
"mobile-detect": "^1.4.4",
"mobx": "^5.15.7",
"mobx-react": "^5.4.4",
diff --git a/src/Utils.ts b/src/Utils.ts
index 53182cc9c..ca1432de2 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -448,7 +448,7 @@ export function returnEmptyDoclist() { return [] as any[]; }
export let emptyPath = [];
-export function emptyFunction() { }
+export function emptyFunction() { return undefined; }
export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
@@ -599,7 +599,7 @@ export function getWordAtPoint(elem: any, x: number, y: number): string | undefi
range.selectNodeContents(elem);
var currentPos = 0;
const endPos = range.endOffset;
- while (currentPos + 1 < endPos) {
+ while (currentPos + 1 <= endPos) {
range.setStart(elem, currentPos);
range.setEnd(elem, currentPos + 1);
const rangeRect = range.getBoundingClientRect();
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index da5c8efa9..219f51a3a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -94,6 +94,7 @@ type DROPt = DAInfo | dropActionType;
export class DocumentOptions {
system?: BOOLt = new BoolInfo("is this a system created/owned doc");
_dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+ allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?");
childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? ");
color?: string; // foreground color data doc
@@ -289,6 +290,7 @@ export class DocumentOptions {
treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed
treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
treeViewTruncateTitleWidth?: number;
+ treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations
treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
sidebarColor?: string; // background color of text sidebar
sidebarViewType?: string; // collection type of text sidebar
@@ -821,7 +823,7 @@ export namespace Docs {
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 9d06ad8a3..c9bef5924 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -339,7 +339,7 @@ export class CurrentUserUtils {
const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
if (doc.templateDocs === undefined) {
doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
- title: "template layouts", _xPadding: 0, system: true,
+ title: "template layouts", _xMargin: 0, system: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
}));
}
@@ -421,7 +421,11 @@ export class CurrentUserUtils {
((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptySlide === undefined) {
- const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ 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"])
+ });
Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text');
FormattedTextBox.SelectOnLoad = textDoc[Id];
doc.emptySlide = textDoc;
@@ -811,7 +815,7 @@ export class CurrentUserUtils {
const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true,
explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
@@ -840,7 +844,7 @@ export class CurrentUserUtils {
const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Trails", _showTitle: "title", _height: 100,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
explainer: "All of the trails that you have created will appear here."
@@ -865,7 +869,7 @@ export class CurrentUserUtils {
});
doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], {
title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true,
@@ -884,7 +888,7 @@ export class CurrentUserUtils {
const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true });
doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -902,7 +906,7 @@ export class CurrentUserUtils {
if (doc.currentFilter === undefined) {
doc.currentFilter = Docs.Create.FilterDocument({
title: "Unnamed Filter", _height: 150,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
+ treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true
});
@@ -918,7 +922,7 @@ export class CurrentUserUtils {
doc.treeViewOpen = true;
doc.treeViewExpandedView = "fields";
doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
})) as any as Doc;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f5704d2bf..ae3fa3170 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -335,8 +335,10 @@ export namespace DragManager {
}
export let docsBeingDragged: Doc[] = [];
export let CanEmbed = false;
+ export let DocDragData: DocumentDragData | undefined;
export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
if (dragData.dropAction === "none") return;
+ DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
const batch = UndoManager.StartBatch("dragging");
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 90a8f2737..62b13e2c6 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -234,8 +234,10 @@ export class LinkManager {
setTimeout(LightboxView.Next);
finished?.();
} else {
- const containerDoc = Cast(target.annotationOn, Doc, null) || target;
- const targetContext = Cast(containerDoc?.context, Doc, null);
+ 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);
}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index e507ec3bf..2cba2c1f2 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -27,7 +27,7 @@ export namespace SelectionManager {
manager.SelectedViews.set(docView, docView.rootDoc);
docView.props.whenChildContentsActiveChanged(true);
- } else if (!ctrlPressed && Array.from(manager.SelectedViews.entries()).length > 1) {
+ } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) {
Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
manager.SelectedSchemaDocument = undefined;
manager.SelectedViews.clear();
@@ -57,7 +57,8 @@ export namespace SelectionManager {
export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
manager.SelectView(docView, ctrlPressed);
}
- export function SelectSchemaViewDoc(document: Opt<Doc>): void {
+ export function SelectSchemaViewDoc(document: Opt<Doc>, deselectAllFirst?: boolean): void {
+ if (deselectAllFirst) manager.DeselectAll();
manager.SelectSchemaViewDoc(document);
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 32c351bf5..b9772fd57 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -44,7 +44,7 @@ interface ViewBoxBaseProps {
fieldKey: string;
layerProvider?: (doc: Doc, assign?: boolean) => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
- isContentActive: () => boolean;
+ isContentActive: () => boolean | undefined;
renderDepth: number;
rootSelected: (outsideReaction?: boolean) => boolean;
}
@@ -65,10 +65,12 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
- isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None ||
- (this.props.isContentActive?.() || this.props.Document.forceActive ||
- this.props.isSelected(outsideReaction) ||
- this.props.rootSelected(outsideReaction)) ? true : false)
+ isContentActive = (outsideReaction?: boolean) => (
+ this.props.isContentActive?.() === false ? false :
+ (CurrentUserUtils.SelectedTool !== InkTool.None ||
+ (this.props.isContentActive?.() || this.props.Document.forceActive ||
+ this.props.isSelected(outsideReaction) ||
+ this.props.rootSelected(outsideReaction)) ? true : undefined))
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
@@ -82,7 +84,7 @@ export interface ViewBoxAnnotatableProps {
fieldKey: string;
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)
layerProvider?: (doc: Doc, assign?: boolean) => boolean;
- isContentActive: () => boolean;
+ isContentActive: () => boolean | undefined;
select: (isCtrlPressed: boolean) => void;
whenChildContentsActiveChanged: (isActive: boolean) => void;
isSelected: (outsideReaction?: boolean) => boolean;
@@ -165,13 +167,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// otherwise, the document being moved must be able to be removed from its container before
// moving it into the target.
@action.bound
- moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean => {
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
return true;
}
const first = doc instanceof Doc ? doc : doc[0];
if (!first?._stayInCollection && addDocument !== returnFalse) {
- return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc));
+ return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
}
return false;
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index d8ad47ecb..82dca1287 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -240,8 +240,10 @@ $linkGap: 3px;
text-align: center;
display: flex;
margin-left: 5px;
- height: 22px;
+ height: 20px;
position: absolute;
+ border-radius: 8px;
+ background: rgba(159,159,159,0.1);
.documentDecorations-titleSpan,
.documentDecorations-titleSpan-Dark {
@@ -288,7 +290,7 @@ $linkGap: 3px;
text-align: center;
display: flex;
margin-left: 5px;
- height: 22px;
+ height: 20px;
position: absolute;
.documentDecorations-titleSpan {
width: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 17a81149c..1c0b1b995 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from '@material-ui/core';
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { DateField } from '../../fields/DateField';
import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from "../../fields/Doc";
@@ -27,8 +27,6 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from "./nodes/DocumentView";
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import React = require("react");
-import { dark } from '@material-ui/core/styles/createPalette';
-import { color } from 'd3-color';
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> {
@@ -65,15 +63,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
@computed
get Bounds() {
- return SelectionManager.Views().map(dv => dv.getBounds()).reduce((bounds, rect) =>
+ const views = SelectionManager.Views();
+ return views.map(dv => dv.getBounds()).reduce((bounds, rect) =>
!rect ? bounds :
{
x: Math.min(rect.left, bounds.x),
y: Math.min(rect.top, bounds.y),
r: Math.max(rect.right, bounds.r),
- b: Math.max(rect.bottom, bounds.b)
+ b: Math.max(rect.bottom, bounds.b),
+ c: views.length === 1 ? rect.center : undefined
},
- { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) });
}
@action
@@ -195,11 +195,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
@action
onRotateDown = (e: React.PointerEvent): void => {
this._rotateUndo = UndoManager.StartBatch("rotatedown");
+ const pt = { x: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 };
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
- const movement = { X: delta[0], Y: e.clientY - down[1] };
- const angle = Math.max(1, Math.abs(movement.Y / 10));
- InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180));
+ const docView = SelectionManager.Views()[0];
+ const { left, top, right, bottom } = docView.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 };
+ const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 };
+ const previousPoint = { X: e.clientX, Y: e.clientY };
+ const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
+ const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint);
+ const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
+ angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt);
return false;
},
() => {
@@ -448,17 +454,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
</Tooltip>);
const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
- const titleArea = this._edtingTitle ?
- <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
- style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 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% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
- <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
- </div>;
+ const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} 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} >
+ <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
+ </div>;
let inMainMenuPanel = false;
for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) {
@@ -492,8 +499,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
}}>
{!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
- {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
-
+ {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) :
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 0644488b3..f24dab949 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -2,19 +2,21 @@ import React = require("react");
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../fields/Doc";
-import { ControlPoint, InkData, PointData } from "../../fields/InkField";
+import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkField";
+import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
+import { Cast, NumCast } from "../../fields/Types";
import { setupMoveUpEvents } from "../../Utils";
import { Transform } from "../util/Transform";
import { UndoManager } from "../util/UndoManager";
import { Colors } from "./global/globalEnums";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { List } from "../../fields/List";
import { InkingStroke } from "./InkingStroke";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { DocumentView } from "./nodes/DocumentView";
export interface InkControlProps {
inkDoc: Doc;
+ inkView: DocumentView;
inkCtrlPoints: InkData;
screenCtrlPoints: InkData;
screenSpaceLineWidth: number;
@@ -41,8 +43,8 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
*/
@action
onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
- if (InkStrokeProperties.Instance) {
- const screenScale = this.props.ScreenToLocalTransform().Scale;
+ const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
+ if (InkStrokeProperties.Instance && ptFromScreen) {
const order = controlIndex % 4;
const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
@@ -51,12 +53,14 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
setupMoveUpEvents(this, e,
action((e: PointerEvent, down: number[], delta: number[]) => {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
- InkStrokeProperties.Instance?.moveControlPtHandle(delta[0] * screenScale, delta[1] * screenScale, controlIndex);
+ const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
+ const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
+ InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
return false;
}),
action(() => {
if (this.controlUndo) {
- InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, controlIndex);
+ InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex);
}
this.controlUndo?.end();
this.controlUndo = undefined;
@@ -71,11 +75,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
} else {
if (brokenIndices?.includes(equivIndex)) {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB);
+ InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
}
if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+ InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
}
}
this.controlUndo?.end();
@@ -98,7 +102,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
@action
onDelete = (e: KeyboardEvent) => {
if (["-", "Backspace", "Delete"].includes(e.key)) {
- InkStrokeProperties.Instance?.deletePoints();
+ InkStrokeProperties.Instance?.deletePoints(this.props.inkView);
e.stopPropagation();
}
}
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 55e06c6ca..2127826b4 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -13,16 +13,25 @@
}
}
-.inkStroke {
- mix-blend-mode: multiply;
- stroke-linejoin: round;
- stroke-linecap: round;
- overflow: visible !important;
- transform-origin: top left;
- width: 100%;
- height: 100%;
+.inkStroke-wrapper {
+ .inkStroke {
+ mix-blend-mode: multiply;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+ overflow: visible !important;
+ transform-origin: top left;
+ width: 100%;
+ height: 100%;
- svg:not(:root) {
- overflow: visible !important;
- }
+ svg:not(:root) {
+ overflow: visible !important;
+ }
+ }
+
+ .inkStroke-text {
+ position: absolute;
+ &:hover {
+ background: #9f9f9f0a;
+ }
+ }
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index ee30caa3d..6687b2bc7 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,16 +1,16 @@
import { Bezier } from "bezier-js";
-import { action, computed, observable, reaction } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { Document } from "../../fields/documentSchemas";
+import { action, observable, reaction } from "mobx";
+import { Doc, Opt, DocListCast } from "../../fields/Doc";
import { InkData, InkField, InkTool, PointData } from "../../fields/InkField";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
import { DocumentType } from "../documents/DocumentTypes";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
-import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import { InkingStroke } from "./InkingStroke";
+import { DocumentView } from "./nodes/DocumentView";
+import { DocumentManager } from "../util/DocumentManager";
export class InkStrokeProperties {
static Instance: InkStrokeProperties | undefined;
@@ -25,21 +25,16 @@ export class InkStrokeProperties {
reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
}
- @computed get selectedInk() {
- const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
- return inks.length ? inks : undefined;
- }
-
/**
* Helper function that enables other functions to be applied to a particular ink instance.
* @param func The inputted function.
* @param requireCurrPoint Indicates whether the current selected point is needed.
*/
- applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
+ applyFunction = (strokes: Opt<DocumentView | DocumentView[]>, func: (view: DocumentView, ink: InkData, ptsXscale: number, ptsYscale: number, inkStrokeWidth: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => {
var appliedFunc = false;
- this.selectedInk?.forEach(action(inkView => {
- if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) {
- const doc = Document(inkView.rootDoc);
+ (strokes instanceof DocumentView ? [strokes] : strokes)?.forEach(action(inkView => {
+ if (!requireCurrPoint || this._currentPoint !== -1) {
+ const doc = inkView.rootDoc;
if (doc.type === DocumentType.INK && doc.width && doc.height) {
const ink = Cast(doc.data, InkField)?.inkData;
if (ink) {
@@ -47,7 +42,7 @@ export class InkStrokeProperties {
const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
const ptsXscale = ((NumCast(doc._width) - NumCast(doc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1;
const ptsYscale = ((NumCast(doc._height) - NumCast(doc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1;
- const newPoints = func(doc, ink, ptsXscale, ptsYscale);
+ const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.strokeWidth));
if (newPoints) {
const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X));
const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y));
@@ -73,8 +68,9 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- addPoints = (t: number, i: number, controls: { X: number, Y: number }[]) => {
- this.applyFunction((doc: Doc, ink: InkData) => {
+ addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number, Y: number }[]) => {
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
const newsegs = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).split(t);
const splicepts = [...newsegs.left.points, ...newsegs.right.points];
@@ -143,9 +139,10 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => {
+ deletePoints = (inkView: DocumentView) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
const newPoints: { X: number, Y: number }[] = [];
- const toRemove = Math.floor(((this._currentPoint + 2) / 4));
+ const toRemove = Math.floor((this._currentPoint + 2) / 4);
const last = this._currentPoint === ink.length - 1;
for (let i = 0; i < ink.length; i++) {
if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) {
@@ -159,24 +156,24 @@ export class InkStrokeProperties {
}, true)
/**
- * Rotates the entire selected ink instance.
+ * Rotates ink stroke(s) about a point
+ * @param inkStrokes set of ink documentViews to rotate
* @param angle The angle at which to rotate the ink in radians.
+ * @param scrpt The center point of the rotation in screen coordinates
*/
@undoBatch
@action
- rotateInk = (angle: number) => {
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
- const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
- const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
- const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 };
- const newPoints = ink.map(i => {
- const pt = { X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y };
- const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale;
- const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y;
- return { X: newX + centerPoint.X, Y: newY + centerPoint.Y };
- });
- doc.rotation = NumCast(doc.rotation) + angle;
- return newPoints;
+ rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => {
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+ view.rootDoc.rotation = NumCast(view.rootDoc.rotation) + angle;
+ const inkCenterPt = view.ComponentView?.ptFromScreen?.({ X: scrpt.x, Y: scrpt.y });
+ return !inkCenterPt ? ink :
+ ink.map(i => {
+ const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
+ const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * yScale / xScale;
+ const newY = Math.sin(angle) * pt.X * xScale / yScale + Math.cos(angle) * pt.Y;
+ return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
+ });
});
}
@@ -185,8 +182,8 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- moveControlPtHandle = (deltaX: number, deltaY: number, controlIndex: number) =>
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
+ moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) =>
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
const order = controlIndex % 4;
const closed = InkingStroke.IsClosed(ink);
@@ -202,7 +199,7 @@ export class InkStrokeProperties {
(order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
(order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) {
- return ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale });
+ return ({ X: pt.X + deltaX, Y: pt.Y + deltaY });
}
return pt;
});
@@ -210,7 +207,7 @@ export class InkStrokeProperties {
})
- public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) {
+ public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refInkSpacePt: { X: number, Y: number }, excludeSegs?: number[]) {
var distance = Number.MAX_SAFE_INTEGER;
var nearestT = -1;
var nearestSeg = -1;
@@ -218,9 +215,9 @@ export class InkStrokeProperties {
for (var i = 0; i < ctrlPoints.length - 3; i += 4) {
if (excludeSegs?.includes(i)) continue;
const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
- const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y });
+ const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refInkSpacePt.X, y: refInkSpacePt.Y });
if (point.t !== undefined) {
- const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y));
+ const dist = Math.sqrt((point.x - refInkSpacePt.X) * (point.x - refInkSpacePt.X) + (point.y - refInkSpacePt.Y) * (point.y - refInkSpacePt.Y));
if (dist < distance) {
distance = dist;
nearestT = point.t;
@@ -235,42 +232,64 @@ export class InkStrokeProperties {
/**
* Handles the movement/scaling of a control point.
*/
- snapControl = (inkDoc: Doc, controlIndex: number) => {
- const ink = Cast(inkDoc.data, InkField)?.inkData;
- if (ink) {
- const closed = InkingStroke.IsClosed(ink);
+ snapControl = (inkView: DocumentView, controlIndex: number) => {
+ const inkDoc = inkView.rootDoc;
+ const ink = Cast(inkDoc[Doc.LayoutFieldKey(inkDoc)], InkField)?.inkData;
- // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve)
- const thisseg = Math.floor(controlIndex / 4) * 4;
- const which = controlIndex % 4;
- const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1;
- const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1;
- const refPt = ink[controlIndex];
- const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]);
-
- // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates.
- // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself.
- const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
- const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
- const ptsXscale = ((NumCast(inkDoc._width) - NumCast(inkDoc.strokeWidth)) / ((oldXrange.max - oldXrange.min) || 1)) || 1;
- const ptsYscale = ((NumCast(inkDoc._height) - NumCast(inkDoc.strokeWidth)) / ((oldYrange.max - oldYrange.min) || 1)) || 1;
- const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale +
- (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
-
- if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) {
- return this.moveControlPtHandle((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex);
+ if (ink) {
+ const screenDragPt = inkView.ComponentView?.ptToScreen?.(ink[controlIndex]);
+ if (screenDragPt) {
+ const snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex);
+ if (snapData.distance < 10) {
+ const deltaX = (snapData.nearestPt.X - ink[controlIndex].X);
+ const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y);
+ const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
+ console.log("X= " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
+ return res;
+ }
}
}
return false;
}
+ excludeSelfSnapSegs = (ink: InkData, controlIndex: number) => {
+ const closed = InkingStroke.IsClosed(ink);
+
+ // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve)
+ const thisseg = Math.floor(controlIndex / 4) * 4;
+ const which = controlIndex % 4;
+ const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1;
+ const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1;
+ return [thisseg, prevseg, nextseg];
+ }
+
+ snapToAllCurves = (screenDragPt: { X: number, Y: number }, inkView: DocumentView, snapData: { nearestPt: { X: number, Y: number }, distance: number }, ink: InkData, controlIndex: number) => {
+ const containingCollection = inkView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ containingCollection?.childDocs
+ .filter(doc => doc.type === DocumentType.INK)
+ .forEach(doc => {
+ const testInkView = DocumentManager.Instance.getDocumentView(doc, containingCollection?.props.CollectionView);
+ const snapped = testInkView?.ComponentView?.snapPt?.(screenDragPt, doc === inkView.rootDoc ? this.excludeSelfSnapSegs(ink, controlIndex) : []);
+ if (snapped && snapped.distance < snapData.distance) {
+ const snappedInkPt = doc === inkView.rootDoc ? snapped.nearestPt :
+ inkView.ComponentView?.ptFromScreen?.(testInkView?.ComponentView?.ptToScreen?.(snapped.nearestPt) ?? { X: 0, Y: 0 }); // convert from snapped ink coordinate system to dragged ink coordinate system by converting to/from screen space
+
+ if (snappedInkPt) {
+ snapData = { nearestPt: snappedInkPt, distance: snapped.distance };
+ }
+ }
+ });
+ return snapData;
+ }
+
/**
* Snaps a control point with broken tangency back to synced rotation.
* @param handleIndexA The handle point that retains its current position.
* @param handleIndexB The handle point that is rotated to be 180 degrees from its opposite.
*/
- snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
- this.applyFunction((doc: Doc, ink: InkData) => {
+ snapHandleTangent = (inkView: DocumentView, controlIndex: number, handleIndexA: number, handleIndexB: number) => {
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const doc = view.rootDoc;
const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"), []);
const ind = brokenIndices.findIndex(value => value === controlIndex);
if (ind !== -1) {
@@ -330,8 +349,9 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- moveTangentHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
- this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
+ moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
+ const doc = view.rootDoc;
const closed = InkingStroke.IsClosed(ink);
const oldHandlePoint = ink[handleIndex];
const oppositeHandlePoint = ink[oppositeHandleIndex];
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index df5bebf31..f88a20448 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -10,11 +10,13 @@ import { emptyFunction, setupMoveUpEvents } from "../../Utils";
import { Transform } from "../util/Transform";
import { UndoManager } from "../util/UndoManager";
import { Colors } from "./global/globalEnums";
-import { InkStrokeProperties } from "./InkStrokeProperties";
import { InkingStroke } from "./InkingStroke";
+import { InkStrokeProperties } from "./InkStrokeProperties";
+import { DocumentView } from "./nodes/DocumentView";
export interface InkHandlesProps {
inkDoc: Doc;
+ inkView: DocumentView;
screenCtrlPoints: InkData;
screenSpaceLineWidth: number;
ScreenToLocalTransform: () => Transform;
@@ -37,7 +39,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
if (e.altKey) this.onBreakTangent(controlIndex);
- InkStrokeProperties.Instance?.moveTangentHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+ InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
return false;
}, () => {
controlUndo?.end();
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 752db1413..59efb36dd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,17 +1,17 @@
import React = require("react");
-import { Bezier } from "bezier-js";
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
+import { Doc, HeightSym, WidthSym } from "../../fields/Doc";
import { documentSchema } from "../../fields/documentSchemas";
import { InkData, InkField, InkTool } from "../../fields/InkField";
import { makeInterface } from "../../fields/Schema";
-import { Cast, NumCast, StrCast, BoolCast } from "../../fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
import { TraceMobx } from "../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
+import { emptyFunction, returnFalse, setupMoveUpEvents, OmitKeys } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
+import { SnappingManager } from "../util/SnappingManager";
import { ContextMenu } from "./ContextMenu";
import { ViewBoxBaseComponent } from "./DocComponent";
import { Colors } from "./global/globalEnums";
@@ -20,8 +20,9 @@ import "./InkStroke.scss";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { InkTangentHandles } from "./InkTangentHandles";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
-import { SnappingManager } from "../util/SnappingManager";
import Color = require("color");
+import { Transform } from "../util/Transform";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@@ -57,6 +58,22 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this._selDisposer?.();
}
+ getCenter = (xf: Transform) => {
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const angle = -NumCast(this.layoutDoc.rotation);
+ const newPoints = inkData.map(pt => {
+ const newX = Math.cos(angle) * pt.X - Math.sin(angle) * pt.Y * inkScaleY / inkScaleX;
+ const newY = Math.sin(angle) * pt.X * inkScaleX / inkScaleY + Math.cos(angle) * pt.Y;
+ return { X: newX, Y: newY };
+ });
+ const crx = (Math.max(...newPoints.map(np => np.X)) + Math.min(...newPoints.map(np => np.X))) / 2;
+ const cry = (Math.max(...newPoints.map(np => np.Y)) + Math.min(...newPoints.map(np => np.Y))) / 2;
+ const cx = Math.cos(-angle) * crx - Math.sin(-angle) * cry * inkScaleY / inkScaleX;
+ const cy = Math.sin(-angle) * crx * inkScaleX / inkScaleY + Math.cos(-angle) * cry;
+ const tc = xf.transformPoint((cx - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (cy - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2);
+ return { X: tc[0], Y: tc[1] };
+ }
+
analyzeStrokes() {
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
@@ -83,7 +100,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
} else if (this._properties?._controlButton) {
- this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+ this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
}
}), this._properties?._controlButton, this._properties?._controlButton
);
@@ -108,6 +125,32 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
}
}
+ ptFromScreen = (scrPt: { X: number, Y: number }) => {
+ const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const docPt = this.props.ScreenToLocalTransform().transformPoint(scrPt.X, scrPt.Y);
+ const inkPt = {
+ X: (docPt[0] - inkStrokeWidth / 2) / inkScaleX + inkStrokeWidth / 2 + inkLeft,
+ Y: (docPt[1] - inkStrokeWidth / 2) / inkScaleY + inkStrokeWidth / 2 + inkTop,
+ };
+ return inkPt;
+ }
+ ptToScreen = (inkPt: { X: number, Y: number }) => {
+ const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const docPt = {
+ X: (inkPt.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+ Y: (inkPt.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2
+ };
+ const scrPt = this.props.ScreenToLocalTransform().inverse().transformPoint(docPt.X, docPt.Y);
+ return { X: scrPt[0], Y: scrPt[1] };
+ }
+
+ snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
+ const { inkData } = this.inkScaledData();
+ const inkPt = this.ptFromScreen(scrPt);
+ const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+ return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
+ }
+
inkScaledData = () => {
const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -166,6 +209,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
"none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
<InkControlPtHandles
+ inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
inkCtrlPoints={inkData}
screenCtrlPoints={screenHdlPts}
@@ -173,6 +217,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
<InkTangentHandles
+ inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
screenCtrlPoints={screenHdlPts}
screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
@@ -196,25 +241,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
- // Thin blue line indicating that the current ink stroke is selected.
- // const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, strokeWidth / 6, StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"),
- // StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", 1.0, false);
- // Invisible polygonal line that enables the ink to be selected by the user.
const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
const highlightColor = !highlightIndex ?
StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
-
+ // Invisible polygonal line that enables the ink to be selected by the user.
const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
- inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+ inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
// Set of points rendered upon the ink that can be added if a user clicks on one.
- return (
+ return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
<svg className="inkStroke"
style={{
+ width: "100%",
+ height: "100%",
pointerEvents: "none",
transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
@@ -235,7 +278,26 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
{clickableLine}
{inkLine}
</svg>
- );
+ {!closed ? (null) :
+ <div className="inkStroke-text" style={{
+ color: StrCast(this.layoutDoc.textColor, "black"),
+ pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined,
+ width: this.layoutDoc[WidthSym](),
+ }}>
+ <FormattedTextBox
+ {...OmitKeys(this.props, ['children']).omit}
+ yPadding={10}
+ xPadding={10}
+ fieldKey={"text"}
+ fontSize={12}
+ dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
+ isContentActive={this.isContentActive}
+ />
+ </div>
+ }
+ </div>;
}
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9a885fbf8..546b0e360 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -511,7 +511,7 @@ export class MainView extends React.Component {
bringToFront={emptyFunction}
select={emptyFunction}
isAnyChildContentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isSelected={returnFalse}
docViewPath={returnEmptyDoclist}
moveDocument={this.moveButtonDoc}
@@ -592,7 +592,7 @@ export class MainView extends React.Component {
pinToPres={returnFalse}
ScreenToLocalTransform={Transform.Identity}
bringToFront={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
focus={returnFalse}
docViewPath={returnEmptyDoclist}
@@ -669,7 +669,7 @@ export class MainView extends React.Component {
pinToPres={returnFalse}
ScreenToLocalTransform={Transform.Identity}
bringToFront={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
focus={returnFalse}
PanelWidth={() => 500}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index af04b967a..7cf388872 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -191,7 +191,7 @@ export class OverlayView extends React.Component {
ScreenToLocalTransform={Transform.Identity}
renderDepth={1}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
focus={DocUtils.DefaultFocus}
styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 86ab881bb..529697f71 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -50,7 +50,7 @@ export default class Palette extends React.Component<PaletteProps> {
PanelHeight={() => window.screen.height}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={emptyFunction}
docViewPath={returnEmptyDoclist}
styleProvider={returnEmptyString}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ab9022a84..1083e0075 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
freezeDimensions={true}
dontCenter={"y"}
isDocumentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
PanelWidth={panelWidth}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3f120d385..8d8630907 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -46,6 +46,7 @@ export enum StyleProp {
JitterRotation = "jitterRotation", // whether documents should be randomly rotated
BorderPath = "customBorder", // border path for document view
FontSize = "fontSize", // size of text font
+ FontFamily = "fontFamily", // size of text font
}
function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; }
@@ -91,7 +92,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey";
case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
- case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"]);
+ case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
+ case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily)));
case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
StrCast(doc._showTitle,
props?.showTitle?.() ||
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 5325d5827..f543d924d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -44,6 +44,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
}
private _reactionDisposer?: IReactionDisposer;
+ private _lightboxReactionDisposer?: IReactionDisposer;
private _containerRef = React.createRef<HTMLDivElement>();
private _flush: UndoManager.Batch | undefined;
private _ignoreStateChange = "";
@@ -298,6 +299,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
componentDidMount: () => void = () => {
if (this._containerRef.current) {
+ this._lightboxReactionDisposer = reaction(() => LightboxView.LightboxDoc, doc => setTimeout(() => !doc && this.onResize(undefined)));
new _global.ResizeObserver(this.onResize).observe(this._containerRef.current);
this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig),
config => {
@@ -320,13 +322,14 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) {
window.removeEventListener('resize', this.onResize);
this._reactionDisposer?.();
+ this._lightboxReactionDisposer?.();
}
@action
onResize = (event: any) => {
const cur = this._containerRef.current;
// bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed
- cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
+ !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
}
@action
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 648ff5087..bffaf86b1 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -226,7 +226,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
layerProvider={this.props.layerProvider}
docViewPath={this.props.docViewPath}
fitWidth={this.props.childFitWidth}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e662f3ddf..fc1bcb8b9 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,7 +22,6 @@ import ReactLoading from 'react-loading';
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
- SetSubView?: (subView: any) => void;
isAnyChildContentActive: () => boolean;
}
@@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
- componentDidMount() {
- this.props.SetSubView?.(this);
- }
-
componentWillUnmount() {
this.gestureDisposer?.();
this._multiTouchDisposer?.();
@@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
if (movedDocs.length) {
- const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+ const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) ||
Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
} else {
@@ -378,7 +373,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// }
}
if (uriList) {
- console.log("Web URI = ", uriList);
// const existingWebDoc = await Hypothesis.findWebDoc(uriList);
// if (existingWebDoc) {
// const alias = Doc.MakeAlias(existingWebDoc);
@@ -390,7 +384,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
// addDocument(alias);
// } else
{
- console.log("Adding ...");
const newDoc = Docs.Create.WebDocument(uriList.split("#annotations:")[0], {// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig)
...options,
title: uriList.split("#annotations:")[0],
@@ -399,8 +392,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
_nativeWidth: 850,
useCors: true
});
- console.log(" ... " + newDoc.title);
- console.log(" ... " + addDocument(newDoc) + " " + newDoc.title);
+ addDocument(newDoc);
}
return;
}
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index d370d21ab..b664d9d82 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -71,6 +71,15 @@
display: none;
}
+.collectionTreeView-titleBar {
+ display: inline-block;
+ width: 100%;
+ height: max-content;
+ .contentFittingDocumentView {
+ display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason)
+ }
+}
+
.collectionTreeView-keyHeader:hover {
background: #797777;
cursor: pointer;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3852987b9..ea077ea40 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
@@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -22,11 +22,11 @@ import { EditableView } from "../EditableView";
import { DocumentView } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import { TreeView } from "./TreeView";
import React = require("react");
-import { Transform } from '../../util/Transform';
const _global = (window /* browser */ || global /* node */) as any;
export type collectionTreeViewProps = {
@@ -41,10 +41,14 @@ export type collectionTreeViewProps = {
@observer
export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) {
- private treedropDisposer?: DragManager.DragDropDisposer;
+ private _treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
+ private _titleRef?: HTMLDivElement | HTMLInputElement | null;
private _disposers: { [name: string]: IReactionDisposer } = {};
- MainEle = () => this._mainEle;
+ private _isDisposing = false; // notes that instance is in process of being disposed
+ private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
+ private observer: any; // observer for monitoring tree view items.
+ private static expandViewLabelSize = 20;
@computed get doc() { return this.props.Document; }
@computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -54,6 +58,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
@computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
@computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
+ @observable _explainerHeight = 0; // height of the description of the tree view
+
+ MainEle = () => this._mainEle;
+
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
@@ -62,11 +70,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
this.props.rootSelected(outsideReaction)) ? true : false)
- isDisposing = false;
componentWillUnmount() {
- this.isDisposing = true;
+ this._isDisposing = true;
super.componentWillUnmount();
- this.treedropDisposer?.();
+ this._treedropDisposer?.();
Object.values(this._disposers).forEach(disposer => disposer?.());
}
@@ -76,13 +83,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
{ fireImmediately: true });
}
- refList: Set<any> = new Set();
- observer: any;
computeHeight = () => {
- if (this.isDisposing) return;
- const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot());
- this.layoutDoc._autoHeightMargins = bodyHeight;
- this.props.setHeight(this.documentTitleHeight() + bodyHeight);
+ if (!this._isDisposing) {
+ const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
+ const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
+ this.layoutDoc._autoHeightMargins = bodyHeight;
+ this.props.setHeight(bodyHeight + titleHeight);
+ }
}
unobserveHeight = (ref: any) => {
this.refList.delete(ref);
@@ -101,8 +108,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
}
protected createTreeDropTarget = (ele: HTMLDivElement) => {
- this.treedropDisposer?.();
- if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
+ this._treedropDisposer?.();
+ if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
}
protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -165,60 +172,44 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
}
- editableTitle = (childDocs: Doc[]) => {
- return !this.dataDoc ? (null) :
- <EditableView
- contents={this.dataDoc.title}
- display={"block"}
- maxHeight={72}
- height={"auto"}
- GetValue={() => StrCast(this.dataDoc.title)}
- SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
- if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs);
- this.dataDoc.title = value;
- return true;
- })} />;
+ get editableTitle() {
+ return <EditableView
+ contents={this.dataDoc.title}
+ display={"block"}
+ maxHeight={72}
+ height={"auto"}
+ GetValue={() => StrCast(this.dataDoc.title)}
+ SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
+ if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
+ this.dataDoc.title = value;
+ return true;
+ })} />;
}
- documentTitle = (childDocs: Doc[]) => {
- return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]}
- onKeyDown={e => {
- e.stopPropagation();
- e.key === "Enter" && this.makeTextCollection(childDocs);
- }}>
- <DocumentView
- Document={this.doc}
- DataDoc={undefined}
- LayoutTemplateString={FormattedTextBox.LayoutString("text")}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActive}
- isDocumentActive={this.isContentActive}
- rootSelected={returnTrue}
- docViewPath={this.props.docViewPath}
- styleProvider={this.props.styleProvider}
- layerProvider={this.props.layerProvider}
- PanelWidth={this.documentTitleWidth}
- PanelHeight={this.documentTitleHeight}
- NativeWidth={this.documentTitleWidth}
- NativeHeight={this.documentTitleHeight}
- focus={this.props.focus}
- treeViewDoc={this.props.Document}
- ScreenToLocalTransform={this.titleTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.doc}
- ContainingCollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
- />
- </div>;
+ get documentTitle() {
+ return <FormattedTextBox
+ {...this.props}
+ fieldKey={"text"}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.isContentActive}
+ isDocumentActive={this.isContentActive}
+ rootSelected={returnTrue}
+ forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+ PanelWidth={this.documentTitleWidth}
+ PanelHeight={this.documentTitleHeight}
+ scaling={returnOne}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionDoc={this.doc}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ bringToFront={returnFalse}
+ />;
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -263,21 +254,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
);
}
@computed get titleBar() {
- const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
- return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren);
+ return this.dataDoc === null ? (null) :
+ <div className="collectionTreeView-titleBar" key={this.doc[Id]}
+ style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}
+ ref={r => this._titleRef = r}
+ onKeyDown={e => {
+ if (this.outlineMode) {
+ e.stopPropagation();
+ e.key === "Enter" && this.makeTextCollection(this.treeChildren);
+ }
+ }}>
+ {this.outlineMode ? this.documentTitle : this.editableTitle}
+ </div>;
+ }
+
+ @computed get noviceExplainer() {
+ return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) :
+ <div className="documentExplanation"> {this.rootDoc.explainer} </div>;
}
return35 = () => 35;
@computed get buttonMenu() {
- const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
// To create a multibutton menu add a CollectionLinearView
- if (menuDoc) {
-
- const width: number = NumCast(menuDoc._width, 30);
- const height: number = NumCast(menuDoc._height, 30);
- console.log(menuDoc.title, width, height);
- return (<div className="buttonMenu-docBtn"
- style={{ width: width, height: height }}>
+ return !menuDoc ? null :
+ (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
@@ -306,11 +307,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
ContainingCollectionDoc={undefined}
/>
</div>);
- }
}
- @observable _explainerHeight: number = 0;
-
@computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); }
@computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); }
@@ -321,47 +319,81 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale ? wscale : hscale;
}
- paddingX = () => NumCast(this.doc._xPadding, 15);
- paddingTop = () => NumCast(this.doc._yPadding, 20);
- paddingBot = () => NumCast(this.doc._yPadding, 20);
+ marginX = () => NumCast(this.doc._xMargin);
+ marginTop = () => NumCast(this.doc._yMargin);
+ marginBot = () => NumCast(this.doc._yMargin);
documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
- titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20));
truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
- panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1);
- render() {
- TraceMobx();
+ panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1);
+
+ addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
+ this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false
+
+ contentFunc = () => {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
- const buttonMenu = this.rootDoc.buttonMenu;
- const noviceExplainer = this.rootDoc.explainer;
-
- return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
- <>
- {this.titleBar}
+ const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
+ return [
+ <div className="collectionTreeView-contents" key="tree" style={{
+ ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+ overflow: "auto",
+ height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+ }} >
+ {titleBar}
<div className="collectionTreeView-container"
- style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}}
+ style={{
+ transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
+ paddingLeft: `${this.marginX()}px`,
+ height: "max-content",
+ width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
+ }}
onContextMenu={this.onContextMenu}>
- {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
- {buttonMenu ? this.buttonMenu : null}
- {Doc.UserDoc().noviceMode && noviceExplainer ?
- <div className="documentExplanation">
- {noviceExplainer}
- </div>
- : null
- }
- </div> : null}
+ {!this.buttonMenu && !this.noviceExplainer ? (null) :
+ <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
+ {this.buttonMenu}
+ {this.noviceExplainer}
+ </div>
+ }
<div className="collectionTreeView-dropTarget"
- style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }}
+ style={{
+ background: background(),
+ height: `calc(100% - ${this._explainerHeight}px)`,
+ pointerEvents: pointerEvents()
+ }}
onWheel={e => e.stopPropagation()}
onDrop={this.onTreeDrop}
- ref={this.createTreeDropTarget}>
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
<ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
{this.treeViewElements}
</ul>
</div >
</div>
- </>;
+ </div>
+ ];
+ }
+ render() {
+ TraceMobx();
+
+ return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
+ this.doc.treeViewHasOverlay ?
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ isAnnotationOverlay={true}
+ isAnnotationOverlayScrollable={true}
+ childDocumentsActive={this.props.isDocumentActive}
+ fieldKey={this.props.fieldKey + "-annotations"}
+ dropAction={"move"}
+ select={emptyFunction}
+ addDocument={this.addAnnotationDocument}
+ removeDocument={this.remAnnotationDocument}
+ moveDocument={this.moveAnnotationDocument}
+ bringToFront={emptyFunction}
+ renderDepth={this.props.renderDepth + 1} >
+ {this.contentFunc}
+ </CollectionFreeFormView> :
+ this.contentFunc();
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 38e027fb3..8e84b59de 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -63,6 +63,7 @@ export enum CollectionViewType {
}
export interface CollectionViewProps extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+ isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
layoutEngine?: () => string;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
@@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
- private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => {
+ private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
TraceMobx();
+ if (type === undefined) return null;
switch (type) {
default:
case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
@@ -246,17 +248,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
@computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
-
- @observable _subView: any = undefined;
-
isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() ? true : false;
+ return this.props.isContentActive();
}
render() {
TraceMobx();
const props: SubCollectionViewProps = {
...this.props,
- SetSubView: action((subView: any) => this._subView = subView),
addDocument: this.addDocument,
moveDocument: this.moveDocument,
removeDocument: this.removeDocument,
@@ -273,7 +271,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
return (<div className={"collectionView"} onContextMenu={this.onContextMenu}
style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
{this.showIsTagged()}
- {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ {this.renderSubView(this.collectionViewType, props)}
</div>);
}
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index eb95bb913..5ba019698 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
<div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
<CollectionFreeFormView
Document={this.props.document}
- SetSubView={() => this}
CollectionView={undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
@@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
noOverlay={true} // don't render overlay Docs since they won't scale
setHeight={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isAnyChildContentActive={returnFalse}
select={emptyFunction}
dropAction={undefined}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 1ebc5873e..2e33d3564 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,14 +53,11 @@
}
}
+.treeView-container-outline-active
.treeView-container-active {
z-index: 100;
position: relative;
-
- .formattedTextbox-sidebar {
- background-color: #ffff001f !important;
- height: 500px !important;
- }
+ pointer-events: all;
}
.treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 7f2128230..d8f984601 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> {
static _openLevelScript: Opt<ScriptField | undefined>;
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
- private _docRef: Opt<DocumentView>;
+ @observable _docRef: Opt<DocumentView>;
private _selDisposer: Opt<IReactionDisposer>;
private _editTitleScript: (() => ScriptField) | undefined;
private _openScript: (() => ScriptField) | undefined;
@@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get childLinks() { return this.childDocList("links"); }
@computed get childAliases() { return this.childDocList("aliases"); }
@computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); }
- @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
+ @computed get selected() { return SelectionManager.IsSelected(this._docRef); }
+ // SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
childDocList(field: string) {
const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> {
DocListCastOrNull(this.doc[field]); // otherwise use the document's data field
}
@undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
- return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc);
+ if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
+ if (this.props.removeDoc?.(doc) === true) {
+ return addDoc(doc);
+ }
+ }
+ return false;
}
@undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
this.props.treeView.props.select(false);
@@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this._editTitle = false;
}
else if (docView.isSelected()) {
+ const doc = docView.Document;
+ SelectionManager.SelectSchemaViewDoc(doc);
this._editTitle = true;
- this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined));
+ this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined));
} else {
docView.select(false);
}
@@ -213,16 +221,18 @@ export class TreeView extends React.Component<TreeViewProps> {
const before = pt[1] < rect.top + rect.height / 2;
const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
this._header.current!.className = "treeView-header";
- if (inside) this._header.current!.className += " treeView-header-inside";
- else if (before) this._header.current!.className += " treeView-header-above";
- else if (!before) this._header.current!.className += " treeView-header-below";
+ if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
+ if (inside) this._header.current!.className += " treeView-header-inside";
+ else if (before) this._header.current!.className += " treeView-header-above";
+ else if (!before) this._header.current!.className += " treeView-header-below";
+ }
e.stopPropagation();
}
public static makeTextBullet() {
const bullet = Docs.Create.TextDocument("-text-", {
layout: CollectionView.LayoutString("data"),
- title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform,
+ 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
@@ -266,23 +276,25 @@ export class TreeView extends React.Component<TreeViewProps> {
e.stopPropagation();
}
const docDragData = de.complete.docDragData;
- if (docDragData) {
- e.stopPropagation();
+ if (docDragData && pt[0] < rect.left + rect.width) {
if (docDragData.draggedDocuments[0] === this.doc) return true;
- this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+ if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+ e.stopPropagation();
+ }
}
}
dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
- const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+ const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd;
const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
const addDoc = !inside ? parentAddDoc :
(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) {
- 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;
}
refTransform = (ref: HTMLDivElement | undefined | null) => {
@@ -432,7 +444,7 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>
</ul>;
}
- return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout"
+ return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout"
}
get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@@ -581,6 +593,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
+ return18 = () => 18;
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -636,10 +649,10 @@ export class TreeView extends React.Component<TreeViewProps> {
moveDocument={this.move}
removeDocument={this.props.removeDoc}
ScreenToLocalTransform={this.getTransform}
- NativeHeight={() => 18}
+ NativeHeight={this.return18}
NativeWidth={this.titleWidth}
PanelWidth={this.titleWidth}
- PanelHeight={() => 18}
+ PanelHeight={this.return18}
contextMenuItems={this.contextMenuItems}
renderDepth={1}
isContentActive={this.props.isContentActive}
@@ -679,6 +692,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" }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -691,7 +705,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
- renderEmbeddedDocument = (asText: boolean) => {
+ renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
const layout = StrCast(Doc.LayoutField(this.layoutDoc));
const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
@@ -704,8 +718,8 @@ 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}
- isContentActive={asText ? this.props.isContentActive : returnFalse}
- isDocumentActive={asText ? this.props.isContentActive : returnFalse}
+ isContentActive={isActive}
+ isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
hideTitle={asText}
fitContentsToDoc={returnTrue}
@@ -749,7 +763,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderDocumentAsHeader() {
return <>
{this.renderBullet}
- {this.renderEmbeddedDocument(true)}
+ {this.renderEmbeddedDocument(true, this.props.isContentActive)}
</>;
}
@@ -776,13 +790,12 @@ export class TreeView extends React.Component<TreeViewProps> {
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}
-
onDrop={this.onTreeDrop}
//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 ?
- this.renderEmbeddedDocument(false) :
+ this.renderEmbeddedDocument(false, returnFalse) :
this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index bb4cae8c6..9cc887e3d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,9 +5,8 @@ import { Id } from "../../../../fields/FieldSymbols";
import { List } from "../../../../fields/List";
import { NumCast } from "../../../../fields/Types";
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
-import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
import { LinkManager } from "../../../util/LinkManager";
-import { ColorScheme } from "../../../util/SettingsManager";
+import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
@@ -154,28 +153,44 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const atop = this.visibleY(adiv);
const btop = this.visibleY(bdiv);
if (!a.width || !b.width) return undefined;
+ const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const acentX = (a.left + a.right) / 2;
+ const acentY = (a.top + a.bottom) / 2;
+ const bcentX = (b.left + b.right) / 2;
+ const bcentY = (b.top + b.bottom) / 2;
+ const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) ||
+ ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1);
+ const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) ||
+ ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1);
const atop2 = this.visibleY(adiv);
const btop2 = this.visibleY(bdiv);
const aleft = this.visibleX(adiv);
const bleft = this.visibleX(bdiv);
const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
- const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
- const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y);
- const pt1 = [apt.point.x, apt.point.y];
- const pt2 = [bpt.point.x, bpt.point.y];
- const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)];
- const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)];
+ const pt1 = [aleft + a.width / 2, atop + a.height / 2];
+ const pt2 = [bleft + b.width / 2, btop + b.width / 2];
+ const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2];
+ const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2];
const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1]));
const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1]));
const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
- const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen];
- const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen];
+ const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] :
+ Math.abs(acentY - aDocBounds.top) < 0.01 ||
+ Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0];
+ const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] :
+ Math.abs(bcentY - bDocBounds.top) < 0.01 ||
+ Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0];
+ const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
+ const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
+ const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
+ const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen];
const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);
const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
- return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 };
+ return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
}
render() {
@@ -199,7 +214,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px";
return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<>
- <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }}
+ <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, strokeDasharray: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? "2 2" : undefined, stroke, strokeWidth }}
+ onClick={() => SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true)}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} />
{textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} >
{Field.toString(this.props.LinkDocs[0].description as any as Field)}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7dcd63b80..190d0e76a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -75,7 +75,7 @@ export type collectionFreeformViewProps = {
scaleField?: string;
noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
engineProps?: any;
- dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are trnasparent or not.
+ dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
// However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
};
@@ -146,7 +146,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
}
@computed get cachedGetTransform(): Transform {
- return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+ return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
}
@action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set;
@@ -165,11 +165,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
- contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
getContainerTransform = () => this.cachedGetContainerTransform.copy();
- getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
isAnyChildContentActive = () => this.props.isAnyChildContentActive();
addLiveTextBox = (newBox: Doc) => {
@@ -221,7 +220,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
if (!super.onInternalDrop(e, de)) return false;
const refDoc = docDragData.droppedDocuments[0];
- const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y);
+ const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y);
const z = NumCast(refDoc.z);
const x = (z ? xpo : xp) - docDragData.offset[0];
const y = (z ? ypo : yp) - docDragData.offset[1];
@@ -803,7 +802,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
else if (this.props.isContentActive(true) && !this.Document._isGroup) {
e.stopPropagation();
e.preventDefault();
- this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
}
}
@@ -1010,13 +1009,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
rootSelected={childData ? this.rootSelected : returnFalse}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
- ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
+ ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform}
PanelWidth={childLayout[WidthSym]}
PanelHeight={childLayout[HeightSym]}
docFilters={this.childDocFilters}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
focus={this.focusDocument}
addDocTab={this.addDocTab}
@@ -1035,7 +1034,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
bringToFront={this.bringToFront}
showTitle={this.props.childShowTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
- pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
+ pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" :
(this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
//fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1403,7 +1402,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
onPointerOver = (e: React.PointerEvent) => {
- (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey);
e.stopPropagation();
}
@@ -1472,10 +1470,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
getContainerTransform={this.getContainerTransform}
getTransform={this.getTransform}
isAnnotationOverlay={this.isAnnotationOverlay}>
- <div ref={this._marqueeRef} style={{ display: this.props.dontRenderDocuments ? "none" : undefined }}>
+ <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
{this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
+ isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
zoomScaling={this.zoomScaling}
presPaths={BoolCast(this.Document.presPathView)}
@@ -1584,6 +1583,7 @@ interface CollectionFreeFormViewPannableContentsProps {
progressivize?: boolean;
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
+ isAnnotationOverlayScrollable: boolean | undefined;
}
@observer
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65c345547..ec1cbadd5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
LayoutTemplateString={this.props.childLayoutString}
freezeDimensions={this.props.childFreezeDimensions}
renderDepth={this.props.renderDepth + 1}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
PanelWidth={width}
PanelHeight={height}
rootSelected={this.rootSelected}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 30836854a..a2d51e2e7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
ScreenToLocalTransform={dxf}
focus={this.props.focus}
docFilters={this.childDocFilters}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 9fe18d118..273e609ca 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider";
interface ResizerProps {
width: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toLeft?: Doc;
toRight?: Doc;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 5478bf709..006ef4df6 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView";
interface ResizerProps {
height: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toTop?: Doc;
toBottom?: Doc;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index 1306b79cb..dc35b5749 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -225,7 +225,7 @@ export interface KeysDropdownProps {
fieldKey: string;
ContainingCollectionDoc: Doc | undefined;
ContainingCollectionView: Opt<CollectionView>;
- active?: (outsideReaction?: boolean) => boolean;
+ active?: (outsideReaction?: boolean) => boolean | undefined;
openHeader: (column: any, screenx: number, screeny: number) => void;
col: SchemaHeaderField;
icon: IconProp;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bc5a9559f..2219345f6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -68,7 +68,7 @@ export interface SchemaTableProps {
addDocument?: (document: Doc | Doc[]) => boolean;
moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
- active: (outsideReaction: boolean | undefined) => boolean;
+ active: (outsideReaction: boolean | undefined) => boolean | undefined;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 1ec7bf72a..9fcd45e72 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -175,7 +175,7 @@
position: absolute;
bottom: 0;
width: 100%;
- overflow-y: scroll;
+ overflow-y: auto;
transform-origin: bottom left;
opacity: 0.1;
transition: opacity 0.5s;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1ccf38de2..c8a32a911 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -97,6 +97,10 @@ export interface DocComponentView {
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;
}
export interface DocumentViewSharedProps {
@@ -134,6 +138,7 @@ export interface DocumentViewSharedProps {
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
@@ -220,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentWillUnmount() { this.cleanupHandlers(true); }
componentDidMount() { this.setupHandlers(); }
- componentDidUpdate() { this.setupHandlers(); }
+ //componentDidUpdate() { this.setupHandlers(); }
setupHandlers() {
this.cleanupHandlers(false);
if (this._mainCont.current) {
@@ -411,6 +416,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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);
}
}
@@ -813,16 +819,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
contentScaling = () => this.ContentScale;
onClickFunc = () => this.onClickHandler;
- setHeight = (height: number) => this.layoutDoc._height = height;
+ 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 CurrentUserUtils.SelectedTool !== InkTool.None ||
+ return this.props.isContentActive() === false ? false : (
+ CurrentUserUtils.SelectedTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.props.rootSelected() ||
this.props.Document.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
- this.props.isContentActive() ? true : false;
+ this.props.isContentActive()) ? true : undefined;
}
@computed get contents() {
TraceMobx();
@@ -1175,9 +1186,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
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();
+ if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined };
}
- return { left, top, right, bottom };
+ return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
}
public iconify() {
@@ -1247,7 +1258,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
position: this.props.Document.isInkMask ? "absolute" : undefined,
transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
- height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+ 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}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ee81e106a..943b9f153 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,7 +18,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further
select: (isCtrlPressed: boolean) => void;
- isContentActive: (outsideReaction?: boolean) => boolean;
+ isContentActive: (outsideReaction?: boolean) => boolean | undefined;
isDocumentActive?: () => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
scaling?: () => number;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 2041c7399..fb8e89da9 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -225,7 +225,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true
});
Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- newFacet._textBoxPadding = 4;
+ newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" });
} else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index b82d16677..879a63248 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -15,7 +15,7 @@ const LinkDocument = makeInterface(documentSchema);
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(LinkDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
- isContentActiveFunc = () => this.isContentActive() ? true : false;
+ isContentActiveFunc = () => this.isContentActive();
render() {
if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 424083dac..2e29c0656 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -166,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
docViewPath={returnEmptyDoclist}
ScreenToLocalTransform={Transform.Identity}
isDocumentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
addDocument={returnFalse}
removeDocument={returnFalse}
addDocTab={returnFalse}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 7ad96bf05..0c631e5f9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -313,7 +313,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
PanelHeight={this.formattedPanelHeight}
isAnnotationOverlay={true}
select={emptyFunction}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
scaling={returnOne}
xPadding={25}
yPadding={10}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 4871599b8..f0d7bd2f3 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -48,10 +48,18 @@
width: 100%;
z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
position: absolute;
+ video {
+ width: auto;
+ height: 100%;
+ display: flex;
+ margin: auto;
+ }
}
.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
- height: Auto;
+ width: 100%;
+ height: 100%;
+ left: 0px;
}
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 440ccf638..615d595c0 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -307,12 +307,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get content() {
const field = Cast(this.dataDoc[this.fieldKey], VideoField);
const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+ const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div key="loading">Loading</div> :
- <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
- <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+ <div className={classname}>
<video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
- style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -457,11 +456,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+ onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 37d716993..9956cc36b 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -58,8 +58,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _sidebarRef = React.createRef<SidebarAnnos>();
private _searchRef = React.createRef<HTMLInputElement>();
private _searchString = "";
+ @observable private _webUrl = ""; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
+ @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled
@observable private _searching: boolean = false;
- @observable _showSidebar = false;
+ @observable private _showSidebar = false;
@observable private _scrollTimer: any;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
@@ -79,6 +81,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
super(props);
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || 850);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || this.Document[HeightSym]() / this.Document[WidthSym]() * 850);
+ runInAction(() => this._webUrl = this._url); // setting the weburl will change the src parameter of the embedded iframe and force a navigation to it.
}
@action
@@ -239,7 +242,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
this._marqueeing = [e.clientX * scale + mainContBounds.translateX,
e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale];
- if (word) {
+ if (word || (e.target as any || "").className.includes("rangeslider") || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) {
setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it.
} else {
this._iframeClick = this._iframe ?? undefined;
@@ -269,9 +272,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
- this._iframe?.contentDocument?.addEventListener("pointerup", this.iframeUp);
+ let requrlraw = decodeURIComponent(iframe?.contentWindow?.location.href.replace(Utils.prepend("") + "/corsProxy/", "") ?? this._url.toString());
+ if (requrlraw !== this._url.toString()) {
+ if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) {
+ const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g);
+ const newsearch = matches?.lastElement()!;
+ if (matches) {
+ requrlraw = requrlraw.substring(0, requrlraw.indexOf(newsearch));
+ for (let i = 1; i < Array.from(matches)?.length; i++) {
+ requrlraw = requrlraw.replace(matches[i], "");
+ }
+ }
+ requrlraw = requrlraw.replace(/q=[^&]*/, newsearch.substring(1)).replace("search&", "search?").replace("?gbv=1", "");
+ }
+ this.submitURL(requrlraw, undefined, true);
+ }
if (iframe?.contentDocument) {
- iframe?.contentDocument.addEventListener("pointerdown", this.iframeDown);
+ iframe.contentDocument.addEventListener("pointerup", this.iframeUp);
+ iframe.contentDocument.addEventListener("pointerdown", this.iframeDown);
this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument.body.scrollHeight);
setTimeout(action(() => this._scrollHeight = Math.max(this.scrollHeight, iframe?.contentDocument?.body.scrollHeight || 0)), 5000);
const initialScroll = this._initialScroll;
@@ -281,13 +299,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._initialScroll = undefined;
}
iframe.setAttribute("enable-annotation", "true");
- iframe.contentDocument.addEventListener("click", undoBatch(action(e => {
+ iframe.contentDocument.addEventListener("click", undoBatch(action((e: MouseEvent) => {
let href = "";
- for (let ele = e.target; ele; ele = ele.parentElement) {
+ for (let ele = e.target as any; ele; ele = ele.parentElement) {
href = (typeof (ele.href) === "string" ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href;
}
- if (href && this.webField?.origin) {
- this.submitURL(href.replace(Utils.prepend(""), this.webField?.origin));
+ const origin = this.webField?.origin;
+ if (href && origin) {
+ e.stopPropagation();
+ setTimeout(() => this.submitURL(href.replace(Utils.prepend(""), origin)));
if (this._outerRef.current) {
this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop);
this._outerRef.current.scrollLeft = 0;
@@ -338,8 +358,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"), []);
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (future.length) {
+ const curUrl = this._url;
this.dataDoc[this.fieldKey + "-history"] = new List<string>([...history, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
+ if (this._webUrl === this._url) {
+ this._webUrl = curUrl;
+ setTimeout(action(() => this._webUrl = this._url));
+ } else {
+ this._webUrl = this._url;
+ }
return true;
}
return false;
@@ -350,10 +377,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const future = Cast(this.dataDoc[this.fieldKey + "-future"], listSpec("string"));
const history = Cast(this.dataDoc[this.fieldKey + "-history"], listSpec("string"), []);
if (history.length) {
+ const curUrl = this._url;
if (future === undefined) this.dataDoc[this.fieldKey + "-future"] = new List<string>([this._url]);
else this.dataDoc[this.fieldKey + "-future"] = new List<string>([...future, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
- console.log(this._urlHash);
+ if (this._webUrl === this._url) {
+ this._webUrl = curUrl;
+ setTimeout(action(() => this._webUrl = this._url));
+ } else {
+ this._webUrl = this._url;
+ }
return true;
}
return false;
@@ -362,9 +395,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
static urlHash = (s: string) => {
return Math.abs(s.split('').reduce((a: any, b: any) => { a = ((a << 5) - a) + b.charCodeAt(0); return a & a; }, 0));
}
-
@action
- submitURL = (newUrl?: string, preview?: boolean) => {
+ submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => {
if (!newUrl) return;
if (!newUrl.startsWith("http")) newUrl = "http://" + newUrl;
try {
@@ -376,7 +408,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.layoutDoc._scrollTop = 0;
future && (future.length = 0);
}
- if (!preview) this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ if (!preview) {
+ this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+ !dontUpdateIframe && (this._webUrl = this._url);
+ }
} catch (e) {
console.log("WebBox URL error:" + this._url);
}
@@ -430,6 +465,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (!cm.findByDescription("Options...")) {
!Doc.UserDoc().noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : "Use") + " Cors", event: () => this.layoutDoc.useCors = !this.layoutDoc.useCors, icon: "snowflake" });
funcs.push({
+ description: (this.layoutDoc.allowScripts ? "Prevent" : "Allow") + " Scripts", event: () => {
+ this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts;
+ if (this._iframe) {
+ runInAction(() => this._hackHide = true);
+ setTimeout(action(() => this._hackHide = false));
+ }
+ }, icon: "snowflake"
+ });
+ funcs.push({
description: (!this.layoutDoc.forceReflow ? "Force" : "Prevent") + " Reflow", event: () => {
const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.scaling?.() || 1);
this.layoutDoc.forceReflow = !nw;
@@ -468,18 +512,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
@computed get urlContent() {
+ if (this._hackHide) return (null);
const field = this.dataDoc[this.props.fieldKey];
let view;
if (field instanceof HtmlField) {
view = <span className="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._url) : this._url;
+ const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl;
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }}
ref={action((r: HTMLIFrameElement | null) => this._iframe = r)} src={url} onLoad={this.iframeLoaded}
// the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
// sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
- sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin"} />;
+ sandbox={`${this.layoutDoc.allowScripts ? "allow-scripts" : ""} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} />;
} else {
view = <iframe className="webBox-iframe" enable-annotation={"true"}
style={{ pointerEvents: this._scrollTimer ? "none" : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly
@@ -615,12 +660,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
moveDocument={this.moveDocument}
addDocument={this.sidebarAddDocument}
styleProvider={this.childStyleProvider}
- pointerEvents={CurrentUserUtils.SelectedTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ childPointerEvents={this.props.isContentActive() ? "all" : undefined}
+ pointerEvents={this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
return (
<div className="webBox" ref={this._mainCont}
style={{ pointerEvents: this.pointerEvents() }} >
<div className={`webBox-container`} style={{ pointerEvents }} onContextMenu={this.specificContextMenu}>
- <base target="_blank" />
<div className={"webBox-outerContent"} ref={this._outerRef}
style={{
width: `calc(${100 / scale}% - ${this.sidebarWidth() / scale * (this._previewWidth ? scale : 1)}px)`,
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index aa53f751d..9d0402075 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -123,7 +123,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
@computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
- @computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
+ @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; }
@computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
@computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); }
@computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); }
@@ -1140,10 +1140,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
- if (startupText) {
- const { state: { tr }, dispatch } = this._editorView;
- dispatch(tr.insertText(startupText));
+ const { state, dispatch } = this._editorView;
+ if (!rtfField) {
+ const startupText = Field.toString(this.dataDoc[fieldKey] as Field);
+ if (startupText) {
+ dispatch(state.tr.insertText(startupText));
+ } else if (!FormattedTextBox.LiveTextUndo) {
+ selectAll(this._editorView!.state, (tr) => {
+ this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
+ });
+ }
}
(this._editorView as any).TextView = this;
}
@@ -1174,8 +1180,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []),
...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []),
- ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []),
- ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []),
+ ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
+ ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])];
}
}
@@ -1565,7 +1571,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
setHeight={this.setSidebarHeight}
fitContentsToDoc={this.fitToBox}
noSidebar={true}
- fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : this.SidebarKey} />;
+ fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
};
return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
@@ -1581,10 +1587,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false);
if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
const minimal = this.props.ignoreAutoHeight;
- const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = Math.min(margins, 10);
- const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0);
- const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : "";
+ const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
+ const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
+ const selPad = ((selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0);
+ const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? "-selected" : "";
const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return (styleFromString?.height === "0px" ? (null) :
<div className="formattedTextBox-cont"
@@ -1628,7 +1634,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onScroll={this.onScroll} onDrop={this.ondrop} >
<div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
style={{
- padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`),
+ padding: StrCast(this.layoutDoc._textBoxPadding),
+ paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+ paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+ paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
+ paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined
}}
/>
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 71ddda866..88221732e 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -94,7 +94,7 @@ export class AudioUpload extends React.Component {
PanelHeight={() => 400}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={emptyFunction}
layerProvider={undefined}
styleProvider={() => "rgba(0,0,0,0)"}
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 652804126..d732a6e2f 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -211,7 +211,7 @@ export class MobileInterface extends React.Component {
PanelHeight={this.returnHeight}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={DocUtils.DefaultFocus}
styleProvider={this.whitebackground}
layerProvider={undefined}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 7b83d09ef..f13580865 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -3,6 +3,7 @@ import { ExifImage } from 'exif';
import { File } from 'formidable';
import { createWriteStream, existsSync, readFileSync, rename, unlinkSync, writeFile } from 'fs';
import * as path from 'path';
+import * as exifr from 'exifr';
import { basename } from "path";
import * as sharp from 'sharp';
import { Stream } from 'stream';
@@ -331,7 +332,7 @@ export namespace DashUploadUtils {
}
};
- const parseExifData = async (source: string): Promise<Upload.EnrichedExifData> => {
+ const parseExifData = async (source: string) => {
const image = await request.get(source, { encoding: null });
const { data, error } = await new Promise(resolve => {
new ExifImage({ image }, (error, data) => {
@@ -342,8 +343,8 @@ export namespace DashUploadUtils {
resolve({ data, error: reason });
});
});
- data && bufferConverterRec(data);
- return { data, error };
+ //data && bufferConverterRec(data);
+ return { data: await exifr.parse(image), error };
};
const { pngs, jpgs, webps, tiffs } = AcceptableMedia;
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index fdc65188f..cde95526f 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -44,7 +44,7 @@ export namespace Upload {
}
export interface EnrichedExifData {
- data: ExifData;
+ data: ExifData & ExifData["gps"];
error?: string;
}
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 00a801e03..de93b64c3 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -13,6 +13,7 @@ import * as request from 'request';
import * as webpack from 'webpack';
import * as wdm from 'webpack-dev-middleware';
import * as whm from 'webpack-hot-middleware';
+import * as zlib from 'zlib';
import { publicDirectory } from '.';
import { logPort } from './ActionUtilities';
import { SSL } from './apis/google/CredentialsLoader';
@@ -36,39 +37,30 @@ export let resolvedPorts: { server: number, socket: number } = { server: 1050, s
export let resolvedServerUrl: string;
export default async function InitializeServer(routeSetter: RouteSetter) {
+ const isRelease = determineEnvironment();
const app = buildWithMiddleware(express());
- // Root route of express app
- app.get("/", (req, res) => res.redirect("/home"));
- app.use(express.static(publicDirectory, {
- setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*")
- }));
- app.use("/images", express.static(publicDirectory));
+ // route table managed by express. routes are tested sequentially against each of these map rules. when a match is found, the handler is called to process the request
+ app.get(new RegExp(/^\/+$/), (req, res) => res.redirect(req.user ? "/home" : "/login")); // target urls that consist of one or more '/'s with nothing in between
+ app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*") })); //all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc)
app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
-
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
app.use(whm(compiler));
-
- registerAuthenticationRoutes(app);
- registerCorsProxy(app);
-
- const isRelease = determineEnvironment();
-
+ registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc)
+ registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies
isRelease && !SSL.Loaded && SSL.exit();
-
- routeSetter(new RouteManager(app, isRelease));
- registerRelativePath(app);
+ routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc)
+ registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content
let server: HttpServer | HttpsServer;
- const { serverPort, serverName } = process.env;
- isRelease && serverPort && (resolvedPorts.server = Number(serverPort));
+ isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort));
await new Promise<void>(resolve => server = isRelease ?
createServer(SSL.Credentials, app).listen(resolvedPorts.server, resolve) :
app.listen(resolvedPorts.server, resolve)
);
logPort("server", resolvedPorts.server);
- resolvedServerUrl = `${isRelease && serverName ? `https://${serverName}.com` : "http://localhost"}:${resolvedPorts.server}`;
+ resolvedServerUrl = `${isRelease && process.env.serverName ? `https://${process.env.serverName}.com` : "http://localhost"}:${resolvedPorts.server}`;
// initialize the web socket (bidirectional communication: if a user changes
// a field on one client, that change must be broadcast to all other clients)
@@ -141,54 +133,81 @@ function registerAuthenticationRoutes(server: express.Express) {
}
function registerCorsProxy(server: express.Express) {
- const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
server.use("/corsProxy", async (req, res) => {
-
const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : "";
- const requrlraw = decodeURIComponent(req.url.substring(1));
+ let requrlraw = decodeURIComponent(req.url.substring(1));
+ const qsplit = requrlraw.split("?q=");
+ const newqsplit = requrlraw.split("&q=");
+ if (qsplit.length > 1 && newqsplit.length > 1) {
+ const lastq = newqsplit[newqsplit.length - 1];
+ requrlraw = qsplit[0] + "?q=" + lastq.split("&")[0] + "&" + qsplit[1].split("&")[1];
+ }
const requrl = requrlraw.startsWith("/") ? referer + requrlraw : requrlraw;
- // cors weirdness here...
- // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
+ // cors weirdness here...
+ // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
// then we redirect again to the cors referer and just add the relative path.
if (!requrl.startsWith("http") && req.originalUrl.startsWith("/corsProxy") && referer?.includes("corsProxy")) {
res.redirect(referer + (referer.endsWith("/") ? "" : "/") + requrl);
} else {
- try {
- await new Promise<void>((resolve, reject) => {
- request(requrl).on("response", resolve).on("error", reject);
- });
- } catch {
- console.log(`Malformed CORS url: ${requrl}`);
- return res.send();
- }
- req.pipe(request(requrl)).on("response", res => {
- const headers = Object.keys(res.headers);
- headers.forEach(headerName => {
- const header = res.headers[headerName];
- if (Array.isArray(header)) {
- res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
- } else if (header) {
- if (headerCharRegex.test(header as any)) {
- delete res.headers[headerName];
- }
- }
- });
- }).on("error", () => console.log(`Malformed CORS url: ${requrl}`)).pipe(res);
+ proxyServe(req, requrl, res);
}
});
}
-function registerRelativePath(server: express.Express) {
+function proxyServe(req: any, requrl: string, response: any) {
+ const htmlBodyMemoryStream = new (require('memorystream'))();
+ req.headers.cookie = "";
+ req.pipe(request(requrl))
+ .on("error", (e: any) => console.log(`Malformed CORS url: ${requrl}`, e))
+ .on("end", () => {
+ var rewrittenHtmlBody: any = undefined;
+ req.pipe(request(requrl))
+ .on("response", (res: any) => {
+ const headers = Object.keys(res.headers);
+ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+ headers.forEach(headerName => {
+ const header = res.headers[headerName];
+ if (Array.isArray(header)) {
+ res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
+ } else if (headerCharRegex.test(header || "")) {
+ delete res.headers[headerName];
+ }
+ if (headerName === "content-encoding" && header.includes("gzip")) {
+ try {
+ const replacer = (match: any, href: string, offset: any, string: any) => {
+ return `href="${resolvedServerUrl + "/corsProxy/http" + href}"`;
+ };
+ const zipToStringDecoder = new (require('string_decoder').StringDecoder)('utf8');
+ const htmlText = zipToStringDecoder.write(zlib.gunzipSync(htmlBodyMemoryStream.read()).toString('utf8')
+ .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
+ .replace(/href="http([^"]*)"/g, replacer)
+ .replace(/target="_blank"/g, ""));
+ rewrittenHtmlBody = zlib.gzipSync(htmlText);
+ } catch (e) { console.log(e); }
+ }
+ });
+ })
+ .on('data', (e: any) => {
+ rewrittenHtmlBody && response.send(rewrittenHtmlBody);
+ rewrittenHtmlBody = undefined;
+ })
+ .pipe(response);
+ })
+ .pipe(htmlBodyMemoryStream);
+}
+
+function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
server.use("*", (req, res) => {
const relativeUrl = req.originalUrl;
- if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
+ if (!req.user) res.redirect("/home"); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
+ else if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:<port>/corsProxy/ )
const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart)
const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org)
const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e..g, http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
res.redirect(redirectedProxiedUrl);
- } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine
+ } else if (relativeUrl.startsWith("/search") && !req.headers.referer?.includes("corsProxy")) { // detect search query and use default search engine
res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl));
} else {
res.end();