aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-10-26 17:24:43 -0400
committerbobzel <zzzman@gmail.com>2021-10-26 17:24:43 -0400
commit92f34533d58291036ff6123c38502cbd4e22300d (patch)
tree3ea20f1fe213b994c6465be103f783fc0212a539 /src
parent2155d39fbc4a9bac3945e40ee10f95f41df466d3 (diff)
parent48d5e650ddc8caa8252561bbc91961f2f4677d6e (diff)
Merge branch 'master' into ink_v1
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts4
-rw-r--r--src/client/documents/Documents.ts3
-rw-r--r--src/client/goldenLayout.js12
-rw-r--r--src/client/util/CurrentUserUtils.ts3
-rw-r--r--src/client/util/DocumentManager.ts5
-rw-r--r--src/client/util/LinkManager.ts15
-rw-r--r--src/client/views/DocumentDecorations.tsx35
-rw-r--r--src/client/views/InkControlPtHandles.tsx16
-rw-r--r--src/client/views/InkStrokeProperties.ts80
-rw-r--r--src/client/views/InkTangentHandles.tsx6
-rw-r--r--src/client/views/InkingStroke.tsx12
-rw-r--r--src/client/views/LightboxView.tsx4
-rw-r--r--src/client/views/MainView.scss17
-rw-r--r--src/client/views/MainView.tsx1
-rw-r--r--src/client/views/collections/CollectionDockingView.scss23
-rw-r--r--src/client/views/collections/CollectionSubView.tsx5
-rw-r--r--src/client/views/collections/TreeView.tsx12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx17
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx6
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx87
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx6
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx18
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss7
-rw-r--r--src/client/views/linking/LinkEditor.tsx28
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx1
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx18
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx14
-rw-r--r--src/client/views/nodes/DocumentView.tsx6
-rw-r--r--src/client/views/nodes/WebBox.tsx77
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx39
-rw-r--r--src/fields/Doc.ts2
-rw-r--r--src/server/DashUploadUtils.ts4
-rw-r--r--src/server/server_Initialization.ts113
33 files changed, 496 insertions, 200 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 7ec4f69f3..bfb29fe8d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -575,7 +575,7 @@ export function simulateMouseClick(element: Element | null | undefined, x: numbe
export function DashColor(color: string) {
try {
- return Color(color.toLowerCase());
+ return color ? Color(color.toLowerCase()) : Color("transparent");
} catch (e) {
console.log("COLOR error:", e);
return Color("red");
@@ -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 e3623c069..da5c8efa9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -191,6 +191,7 @@ export class DocumentOptions {
opacity?: number;
defaultBackgroundColor?: string;
_isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked
+ _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint
isFolder?: boolean;
lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
activeFrame?: number; // the active frame of a document in a frame base collection
@@ -302,6 +303,7 @@ export class DocumentOptions {
border?: string; //for searchbox
hoverBackgroundColor?: string; // background color of a label when hovered
linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
+ linkRelationshipSizes?: List<number>; //stores number of links contained in each relationship
linkColorList?: List<string>; // colors of links corresponding to specific link relationships
}
export namespace Docs {
@@ -1105,6 +1107,7 @@ export namespace DocUtils {
"_acl-Public": SharingPermissions.Augment,
linkDisplay: true,
_hidden: true,
+ _linkAutoMove: true,
linkRelationship,
_showCaption: "description",
_showTitle: "linkRelationship",
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 238f1ac0a..896237e1d 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -1583,7 +1583,7 @@
close: 'close',
maximise: 'maximise',
minimise: 'minimise',
- popout: 'open in new window',
+ popout: 'new tab',
popin: 'pop in',
tabDropdown: 'additional tabs'
}
@@ -2355,6 +2355,7 @@
this.element.hide();
}
});
+
/**
* This class represents a header above a Stack ContentItem.
*
@@ -2362,6 +2363,7 @@
* @param {lm.item.AbstractContentItem} parent
*/
lm.controls.Header = function (layoutManager, parent) {
+
lm.utils.EventEmitter.call(this);
this.layoutManager = layoutManager;
@@ -4449,7 +4451,11 @@
lm.items.Stack = function (layoutManager, config, parent) {
lm.items.AbstractContentItem.call(this, layoutManager, config, parent);
- this.element = $('<div class="lm_item lm_stack"></div>');
+ this.element = $(
+ '<div class="lm_item lm_stack">'
+ + '<p class="empty-tabs-message">Click <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC"/> to create a new tab</p>'
+ + '</div>'
+ );
this._activeContentItem = null;
var cfg = layoutManager.config;
this._header = { // defaults' reconstruction from old configuration style
@@ -5029,7 +5035,7 @@
'close',
'maximise',
'minimise',
- 'open in new window'
+ 'new tab'
];
};
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 3c32c2359..9d06ad8a3 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -810,7 +810,7 @@ export class CurrentUserUtils {
const newDashboard = ScriptField.MakeScript(`createNewDashboard(Doc.UserDoc())`);
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,
+ title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _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,
@@ -1328,6 +1328,7 @@ export class CurrentUserUtils {
Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
doc.savedFilters = new List<Doc>();
doc.filterDocCount = 0;
+ doc.freezeChildren = "remove|add";
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupImportSidebar(doc); // sets up the import sidebar
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index b66befb08..66b6a1e44 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -161,7 +161,8 @@ export class DocumentManager {
originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
finished?: () => void,
originalTarget?: Doc,
- noSelect?: boolean
+ noSelect?: boolean,
+ presZoom?: number
): Promise<void> => {
originalTarget = originalTarget ?? targetDoc;
const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
@@ -194,7 +195,7 @@ export class DocumentManager {
if (focusView) {
!noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
focusView.focus(targetDoc, {
- originalTarget, willZoom, afterFocus: (didFocus: boolean) =>
+ originalTarget, willZoom, scale: presZoom, afterFocus: (didFocus: boolean) =>
new Promise<ViewAdjustment>(res => {
focusAndFinish(didFocus);
res();
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 53bd13fb3..62b13e2c6 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -104,12 +104,9 @@ export class LinkManager {
public createLinkrelationshipLists = () => {
//create new lists for link relations and their associated colors if the lists don't already exist
- if (!Doc.UserDoc().linkRelationshipList && !Doc.UserDoc().linkColorList) {
- const linkRelationshipList = new List<string>();
- const linkColorList = new List<string>();
- Doc.UserDoc().linkRelationshipList = linkRelationshipList;
- Doc.UserDoc().linkColorList = linkColorList;
- }
+ !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>());
+ !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>());
+ !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>());
}
public addLink(linkDoc: Doc, checkExists = false) {
@@ -237,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/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 207c7fcac..70dffd7a1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -195,15 +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.x + this.Bounds.r) / 2, y: (this.Bounds.y + this.Bounds.b) / 2 };
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
const docView = SelectionManager.Views()[0];
- const { left, top, right, bottom } = docView.getBounds() || { left: 0, top: 0, right: 0, bottom: 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);
- if (angle) InkStrokeProperties.Instance?.rotateInk(-angle);
+ const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
+ angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt);
return false;
},
() => {
@@ -322,8 +324,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const doc = Document(docView.rootDoc);
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
- const width = (doc._width || 0);
- let height = (doc._height || (nheight / nwidth * width));
+ const docheight = doc._height || 0;
+ const docwidth = doc._width || 0;
+ const width = docwidth;
+ let height = (docheight || (nheight / nwidth * width));
height = !height || isNaN(height) ? 20 : height;
const scale = docView.props.ScreenToLocalTransform().Scale;
const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable;
@@ -336,17 +340,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
else dW = dH * nwidth / nheight;
}
}
- const actualdW = Math.max(width + (dW * scale), 20);
- const actualdH = Math.max(height + (dH * scale), 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
+ let actualdW = Math.max(width + (dW * scale), 20);
+ let actualdH = Math.max(height + (dH * scale), 20);
const fixedAspect = (nwidth && nheight);
if (fixedAspect) {
if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) {
if (dragRight && modifyNativeDim) {
doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc);
} else {
- if (!doc._fitWidth) doc._height = nheight / nwidth * actualdW;
+ if (!doc._fitWidth) {
+ actualdH = nheight / nwidth * actualdW;
+ doc._height = actualdH;
+ }
else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
}
doc._width = actualdW;
@@ -357,10 +362,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc);
doc._autoHeight = false;
} else {
- if (!doc._fitWidth) doc._width = nwidth / nheight * actualdH;
+ if (!doc._fitWidth) {
+ actualdW = nwidth / nheight * actualdH;
+ doc._width = actualdW;
+ }
else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
}
- if (!modifyNativeDim) doc._height = Math.min(nheight / nwidth * NumCast(doc._width), actualdH);
+ if (!modifyNativeDim) {
+ actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH);
+ doc._height = actualdH;
+ }
else doc._height = actualdH;
}
} else {
@@ -368,6 +379,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
dW && (doc._width = actualdW);
dH && (doc._autoHeight = false);
}
+ doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
+ doc.y = (doc.y || 0) + dY * (actualdH - docheight);
doc._lastModified = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 0644488b3..73de4a3e0 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -3,18 +3,20 @@ import { action, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../fields/Doc";
import { ControlPoint, InkData, PointData } from "../../fields/InkField";
+import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast } 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;
@@ -51,12 +53,12 @@ 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);
+ InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, delta[0] * screenScale, delta[1] * screenScale, 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 +73,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 +100,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/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 712459125..156bb1a6a 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,17 +1,15 @@
import { Bezier } from "bezier-js";
-import { action, computed, observable, reaction } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { DocumentView } from "./nodes/DocumentView";
-import { Document } from "../../fields/documentSchemas";
+import { action, observable, reaction } from "mobx";
+import { Doc, Opt } 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";
export class InkStrokeProperties {
static Instance: InkStrokeProperties | undefined;
@@ -26,21 +24,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) {
@@ -48,7 +41,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));
@@ -74,8 +67,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];
@@ -144,9 +138,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)) {
@@ -165,17 +160,22 @@ export class InkStrokeProperties {
*/
@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 };
+ rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => {
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+ const oldXrangeMin = Math.min(...ink.map(p => p.X));
+ const oldYrangeMin = Math.min(...ink.map(p => p.Y));
+ const docViewCenterPt = view.screenToLocalTransform().transformPoint(scrpt.x, scrpt.y);
+ const inkCenterPt = {
+ X: (docViewCenterPt[0] - inkStrokeWidth / 2) / xScale + oldXrangeMin,
+ Y: (docViewCenterPt[1] - inkStrokeWidth / 2) / yScale + oldYrangeMin
+ };
const newPoints = ink.map(i => {
- const pt = { X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y };
+ 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 + centerPoint.X, Y: newY + centerPoint.Y };
+ return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
});
+ const doc = view.rootDoc;
doc.rotation = NumCast(doc.rotation) + angle;
return newPoints;
});
@@ -186,8 +186,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);
@@ -233,15 +233,19 @@ export class InkStrokeProperties {
return { distance, nearestT, nearestSeg, nearestPt };
}
- snapControl = (inkDoc: Doc, controlIndex: number) => {
+ /**
+ * Handles the movement/scaling of a control point.
+ */
+ snapControl = (inkView: DocumentView, controlIndex: number) => {
+ const inkDoc = inkView.rootDoc;
const ink = Cast(inkDoc.data, InkField)?.inkData;
if (ink) {
const { near, nearestPt, ptsXscale, ptsYscale } = this.snapWithinCurve(ink, inkDoc, controlIndex);
- if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) {
+ if (near / (inkView.props.ScreenToLocalTransform().Scale || 1) < 10) {
const deltaX = (nearestPt.X - ink[controlIndex].X) * ptsXscale;
const deltaY = (nearestPt.Y - ink[controlIndex].Y) * ptsYscale;
- return this.moveControlPtHandle(deltaX, deltaY, controlIndex);
+ return this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
} else {
return this.snapBetweenCurves(ink, inkDoc, controlIndex);
}
@@ -265,14 +269,14 @@ export class InkStrokeProperties {
const { ptsXscale, ptsYscale } = this.inkToScreenScale(ink, inkDoc);
const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale +
(nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale);
-
+
return { near, nearestPt, ptsXscale, ptsYscale };
}
snapBetweenCurves = (ink: InkData, inkDoc: Doc, controlIndex: number) => {
const inkContext = Cast(inkDoc.context, Doc, null);
// Cast(inkContext.data)
-
+
// .filter(doc => doc.type === DocumentType.INK)
// .map(doc => {
// if (InkStrokeProperties.Instance?._lock) {
@@ -297,8 +301,9 @@ export class InkStrokeProperties {
* @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) {
@@ -358,8 +363,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 67e797dcd..6ea3e108c 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 { 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 { 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,7 +20,6 @@ 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");
type InkDocument = makeInterface<[typeof documentSchema]>;
@@ -83,7 +82,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
);
@@ -166,6 +165,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 +173,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}
@@ -205,7 +206,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (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.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", 0.0, false);
+ 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 (
<svg className="inkStroke"
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index ec30a6a5d..ec3bf6c18 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -214,7 +214,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
LightboxView.SetLightboxDoc(undefined);
}
}} >
-
+
<div className="lightboxView-contents" style={{
left: this.leftBorder,
top: this.topBorder,
@@ -237,7 +237,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
DataDoc={undefined}
LayoutTemplate={LightboxView.LightboxDocTemplate}
addDocument={undefined}
- fitContentsToDoc={this.fitToBox}
+ // fitContentsToDoc={this.fitToBox} // bcz: why do we want this? when we initially open a colletion, we shrinkwrap it which allows for user navigation. if we later encounter a collection, it's not clear to me that we want to make it either shrinkwrap or fitContents...
isDocumentActive={returnFalse}
isContentActive={returnTrue}
addDocTab={this.addDocTab}
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index c3cdb7dde..15cd2c144 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -59,6 +59,10 @@
.mainView-container {
color: $dark-gray;
+ .lm_goldenlayout {
+ background: $medium-gray;
+ }
+
.lm_title {
background: $light-gray;
color: $dark-gray;
@@ -166,12 +170,14 @@
position: absolute;
z-index: 2;
background-color: $light-gray;
+
.editable-title {
background-color: $light-gray;
}
}
}
+
.mainView-libraryHandle {
background-color: $light-gray;
}
@@ -179,20 +185,24 @@
{
.propertiesView {
background-color: #252525;
+
input {
background-color: $medium-gray;
}
- .propertiesView-sharingTable
- {
+
+ .propertiesView-sharingTable {
background-color: $medium-gray;
}
+
.editable-title {
background-color: $medium-gray;
}
+
.propertiesView-field {
background-color: $medium-gray;
}
}
+
.mainView-propertiesDragger,
.mainView-libraryHandle {
background: #353535;
@@ -202,8 +212,9 @@
.contextMenu-cont {
background: $medium-gray;
color: $white;
+
input::placeholder {
- color:$white;
+ color: $white;
}
}
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 6d0d5eb39..9a885fbf8 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -466,6 +466,7 @@ export class MainView extends React.Component {
</div>;
}
+
expandFlyout = action((button: Doc) => {
// bcz: What's going on here!?
// Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 77e7b86ea..b2ee33807 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -65,6 +65,29 @@
display: inline;
}
+.empty-tabs-message {
+ position: absolute;
+ width: 100%;
+ z-index: 1;
+ top: 50%;
+ z-index: 1;
+ text-align: center;
+ font-size: 18;
+ color: $dark-gray;
+
+ img {
+ position: relative;
+ top: -1px;
+ margin: 0 5px;
+ }
+}
+
+.lm_header,
+.lm_items {
+ z-index: 2;
+ position: relative;
+}
+
.lm_drag_tab {
padding: 0;
width: 15px !important;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e662f3ddf..5dffc65fc 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -378,7 +378,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 +389,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 +397,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/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index a3da0e0e4..7f2128230 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -820,7 +820,7 @@ export class TreeView extends React.Component<TreeViewProps> {
childDocs: Doc[],
treeView: CollectionTreeView,
parentTreeView: CollectionTreeView | TreeView | undefined,
- conainerCollection: Doc,
+ containerCollection: Doc,
dataDoc: Doc | undefined,
parentCollectionDoc: Doc | undefined,
containerPrevSibling: Doc | undefined,
@@ -846,16 +846,16 @@ export class TreeView extends React.Component<TreeViewProps> {
unobserveHeight: (ref: any) => void,
contextMenuItems: ({ script: ScriptField, filter: ScriptField, label: string, icon: string }[])
) {
- const viewSpecScript = Cast(conainerCollection.viewSpecScript, ScriptField);
+ const viewSpecScript = Cast(containerCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
}
- const docs = TreeView.sortDocs(childDocs, StrCast(conainerCollection.treeViewSortCriterion));
+ const docs = TreeView.sortDocs(childDocs, StrCast(containerCollection.treeViewSortCriterion));
const rowWidth = () => panelWidth() - treeBulletWidth();
const treeViewRefs = new Map<Doc, TreeView | undefined>();
return docs.filter(child => child instanceof Doc).map((child, i) => {
- const pair = Doc.GetLayoutDataDocPair(conainerCollection, dataDoc, child);
+ const pair = Doc.GetLayoutDataDocPair(containerCollection, dataDoc, child);
if (!pair.layout || pair.data instanceof Promise) {
return (null);
}
@@ -883,7 +883,7 @@ export class TreeView extends React.Component<TreeViewProps> {
return <TreeView key={child[Id]} ref={r => treeViewRefs.set(child, r ? r : undefined)}
document={pair.layout}
dataDoc={pair.data}
- containerCollection={conainerCollection}
+ containerCollection={containerCollection}
prevSibling={docs[i]}
treeView={treeView}
indentDocument={indent}
@@ -891,7 +891,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onCheckedClick={onCheckedClick}
onChildClick={onChildClick}
renderDepth={renderDepth}
- removeDoc={StrCast(conainerCollection.freezeChildren).includes("remove") ? undefined : remove}
+ removeDoc={StrCast(containerCollection.freezeChildren).includes("remove") ? undefined : remove}
addDocument={addDocument}
styleProvider={styleProvider}
panelWidth={rowWidth}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f6c2707da..bb4cae8c6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -42,7 +42,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const { A, B, LinkDocs } = this.props;
const linkDoc = LinkDocs[0];
if (SnappingManager.GetIsDragging() || !A.ContentDiv || !B.ContentDiv) return;
- setTimeout(action(() => this._opacity = 1), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
+ setTimeout(action(() => this._opacity = 0.75), 0); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
setTimeout(action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), 750); // this will unhighlight the link line.
const acont = A.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
const bcont = B.ContentDiv.getElementsByClassName("linkAnchorBox-cont");
@@ -180,15 +180,26 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
render() {
if (!this.renderData) return (null);
+
const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
LinkManager.currentLink = this.props.LinkDocs[0];
const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
const linkColorList = Doc.UserDoc().linkColorList as List<string>;
+ const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>;
+ const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);
+
+ const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex];
+
//access stroke color using index of the relationship in the color list (default black)
- const strokeColor = linkRelationshipList.indexOf(linkRelationship) === -1 ? (CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "white" : "black") : linkColorList[linkRelationshipList.indexOf(linkRelationship)];
+ const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? "black" : linkColorList[currRelationshipIndex];
+
+ //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has)
+ //thickness varies linearly from 3px to 12px for increasing link count
+ 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: strokeColor }}
+ <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }}
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 b2db1168d..febccbfcc 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.
};
@@ -1036,7 +1036,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
showTitle={this.props.childShowTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
- (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined}
+ (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
/>;
@@ -1472,7 +1472,7 @@ 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}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index ed196349e..a439a7998 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -38,27 +38,35 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
+ // currently unused
CollectionView: Opt<CollectionView>;
+ // currently unused
ContainingCollection: Opt<CollectionView>;
Document: Doc;
+ // column name
fieldKey: string;
+ // currently unused
renderDepth: number;
+ // called when a button is pressed on the node itself
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined,
addDocument: (document: Doc | Doc[]) => boolean) => boolean;
isFocused: boolean;
changeFocusedCellByIndex: (row: number, col: number) => void;
+ // set whether the cell is in the isEditing mode
setIsEditing: (isEditing: boolean) => void;
isEditable: boolean;
setPreviewDoc: (doc: Doc) => void;
setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
getField: (row: number, col?: number) => void;
+ // currnetly unused
showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void;
}
@observer
export class CollectionSchemaCell extends React.Component<CellProps> {
+ // return a field key that is corrected for whether it COMMENT
public static resolvedFieldKey(column: string, rowDoc: Doc) {
const fieldKey = column;
if (fieldKey.startsWith("*")) {
@@ -72,7 +80,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@observable protected _isEditing: boolean = false;
protected _focusRef = React.createRef<HTMLDivElement>();
protected _rowDoc = this.props.rowProps.original;
+ // Gets the serialized data in proto form of the base proto that this document's proto inherits from
protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
+ // methods for dragging and dropping
protected _dropDisposer?: DragManager.DragDropDisposer;
@observable contents: string = "";
@@ -81,6 +91,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
onKeyDown = (e: KeyboardEvent): void => {
+ // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
document.removeEventListener("keydown", this.onKeyDown);
this._isEditing = true;
@@ -90,7 +101,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
isEditingCallback = (isEditing: boolean): void => {
+ // a general method that takes a boolean that determines whether the cell should be in
+ // is-editing mode
+ // remove the event listener if it's there
document.removeEventListener("keydown", this.onKeyDown);
+ // it's not already in is-editing mode, re-add the event listener
isEditing && document.addEventListener("keydown", this.onKeyDown);
this._isEditing = isEditing;
this.props.setIsEditing(isEditing);
@@ -99,13 +114,15 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
onPointerDown = async (e: React.PointerEvent): Promise<void> => {
+ // pan to the cell
this.onItemDown(e);
+ // focus on it
this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
this.props.setPreviewDoc(this.props.rowProps.original);
- console.log("click cell");
let url: string;
if (url = StrCast(this.props.rowProps.row.href)) {
+ // opens up the the doc in a new window, blurring the old one
try {
new URL(url);
const temp = window.open(url)!;
@@ -120,18 +137,25 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@undoBatch
applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
+ // apply a specified change to the cell
const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
if (!res.success) return false;
+ // change what is rendered to this new changed cell content
doc[this.renderFieldKey] = res.result;
return true;
+ // return whether the change was successful
}
private drop = (e: Event, de: DragManager.DropEvent) => {
+ // if the drag has data at its completion
if (de.complete.docDragData) {
+ // if only one doc was dragged
if (de.complete.docDragData.draggedDocuments.length === 1) {
+ // update the renderFieldKey
this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
}
else {
+ // create schema document reflecting the new column arrangement
const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {});
this._rowDataDoc[this.renderFieldKey] = coll;
}
@@ -140,7 +164,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
}
protected dropRef = (ele: HTMLElement | null) => {
+ // if the drop disposer is not undefined, run its function
this._dropDisposer?.();
+ // if ele is not null, give ele a non-undefined drop disposer
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
}
@@ -164,33 +190,46 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>;
}
- @computed get renderFieldKey() { return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original); }
+ @computed get renderFieldKey() {
+ // gets the resolved field key of this cell
+ return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original);
+ }
+
onItemDown = async (e: React.PointerEvent) => {
+ // if the document is a document used to change UI for search results in schema view
if (this.props.Document._searchDoc) {
const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
+ // Jump to the this document
DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext,
undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
}
}
+
renderCellWithType(type: string | undefined) {
const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
+ // the column
const fieldKey = this.renderFieldKey;
+ // the exact cell
const field = this._rowDoc[fieldKey];
const onPointerEnter = (e: React.PointerEvent): void => {
+ // e.buttons === 1 means the left moue pointer is down
if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) {
dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
}
};
const onPointerLeave = (e: React.PointerEvent): void => {
+ // change the class name to indicate that the cell is no longer being dragged
dragRef.current!.className = "collectionSchemaView-cellContainer";
};
let contents = Field.toString(field as Field);
+ // display 2 hyphens instead of a blank box for empty cells
contents = contents === "" ? "--" : contents;
+ // classname reflects the tatus of the cell
let className = "collectionSchemaView-cellWrapper";
if (this._isEditing) className += " editing";
if (this.props.isFocused && this.props.isEditable) className += " focused";
@@ -198,19 +237,23 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
const positions = [];
if (StrCast(this.props.Document._searchString).toLowerCase() !== "") {
+ // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase();
const search = StrCast(this.props.Document._searchString).toLowerCase();
let start = term.indexOf(search);
let tally = 0;
+ // if search is found in term
if (start !== -1) {
positions.push(start);
}
+ // if search is found in term, continue finding all instances of search in term
while (start < contents?.length && start !== -1) {
term = term.slice(start + search.length + 1);
tally += start + search.length + 1;
start = term.indexOf(search);
positions.push(tally + start);
}
+ // remove the last position
if (positions.length > 1) {
positions.pop();
}
@@ -280,6 +323,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
//two options here: we can strip off outer quotes or we can figure out what's going on with the script
const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
+ // change it if a change is made, otherwise, just compile using the old cell conetnts
script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
// handle numbers and expressions
} else if (inputIsNum || value.startsWith("=")) {
@@ -309,6 +353,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
return retVal;
})}
OnFillDown={async (value: string) => {
+ // computes all of the value preceded by :=
const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
script.compiled && DocListCast(this.props.Document[this.props.fieldKey]).
forEach((doc, i) => value.startsWith(":=") ?
@@ -339,7 +384,10 @@ export class CollectionSchemaStringCell extends CollectionSchemaCell { render()
@observer
export class CollectionSchemaDateCell extends CollectionSchemaCell {
- @computed get _date(): Opt<DateField> { return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined; }
+ @computed get _date(): Opt<DateField> {
+ // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
+ return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined;
+ }
@action
handleChange = (date: any) => {
@@ -378,8 +426,9 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
typecheck: true,
transformer: DocumentIconContainer.getTransformer()
});
-
+ // compile the script
const results = script.compiled && script.run();
+ // if the script was compiled and run
if (results && results.success) {
this._rowDoc[this.renderFieldKey] = results.result;
return true;
@@ -397,6 +446,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
@action
isEditingCallback = (isEditing: boolean): void => {
+ // the isEditingCallback from a general CollectionSchemaCell
document.removeEventListener("keydown", this.onKeyDown);
isEditing && document.addEventListener("keydown", this.onKeyDown);
this._isEditing = isEditing;
@@ -405,6 +455,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
}
render() {
+ // if there's a doc, render it
return !this._doc ? this.renderCellWithType("document") :
<div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1}
onPointerDown={this.onPointerDown}
@@ -440,11 +491,11 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
export class CollectionSchemaImageCell extends CollectionSchemaCell {
choosePath(url: URL) {
- if (url.protocol === "data") return url.href;
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here
+ if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href
+ if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question
- const ext = path.extname(url.href);
+ const ext = path.extname(url.href); // the extension of the file
return url.href.replace(ext, "_o" + path.extname(url.href));
}
@@ -453,12 +504,13 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell {
const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
+ // If there is a path, follow it; otherwise, follow a link to a default image icon
const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
- const aspect = Doc.NativeAspect(this._rowDoc);
- let width = Math.min(75, this.props.rowProps.width);
- const height = Math.min(75, width / aspect);
- width = height * aspect;
+ const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
+ let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
+ const height = Math.min(75, width / aspect); // get a height either proportional to that or 75 px
+ width = height * aspect; // increase the width of the image if necessary to maintain proportionality
const reference = React.createRef<HTMLDivElement>();
return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
@@ -478,13 +530,13 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
@computed get _field() { return this._rowDoc[this.renderFieldKey]; }
@computed get _optionsList() { return this._field as List<any>; }
- @observable private _opened = false;
+ @observable private _opened = false; // whether the list is opened
@observable private _text = "select an item";
- @observable private _selectedNum = 0;
+ @observable private _selectedNum = 0; // the index of the list item selected
@action
onSetValue = (value: string) => {
- // change if its a document
+ // change if it's a document
this._optionsList[this._selectedNum] = this._text = value;
(this._field as List<any>).splice(this._selectedNum, 1, value);
@@ -492,6 +544,7 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
@action
onSelected = (element: string, index: number) => {
+ // if an item is selected, the private variables should update to reflect this
this._text = element;
this._selectedNum = index;
}
@@ -505,6 +558,7 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
const link = false;
const reference = React.createRef<HTMLDivElement>();
+ // if the list is not opened, don't display it; otherwise, do.
if (this._optionsList?.length) {
const options = !this._opened ? (null) :
<div>
@@ -572,6 +626,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
@observer
export class CollectionSchemaButtons extends CollectionSchemaCell {
+ // the navigation buttons for schema view when it is used for search.
render() {
return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> :
<div style={{ paddingTop: 8, paddingLeft: 3 }} >
@@ -583,4 +638,4 @@ export class CollectionSchemaButtons extends CollectionSchemaCell {
</button>
</div>;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index c659f749e..1306b79cb 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -24,6 +24,7 @@ export interface AddColumnHeaderProps {
@observer
export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
+ // the button that allows the user to add a column
render() {
return <button className="add-column" onClick={() => this.props.createColumn()}>
<FontAwesomeIcon icon="plus" size="sm" />
@@ -31,7 +32,6 @@ export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHe
}
}
-
export interface ColumnMenuProps {
columnField: SchemaHeaderField;
// keyValue: string;
@@ -395,8 +395,8 @@ export class KeysDropdown extends React.Component<KeysDropdownProps> {
this.closeResultsVisibility = "none";
}
for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
- if (filters![i] === this.props.col.heading && keyOptions.includes(filters![i].split(":")[1]) === false) {
- keyOptions.push(filters![i + 1]);
+ if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) {
+ keyOptions.push(filters[i + 1]);
}
}
const options = keyOptions.map(key => {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
index 456c38c68..2df95ffd8 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
@@ -21,27 +21,38 @@ export interface MovableColumnProps {
ScreenToLocalTransform: () => Transform;
}
export class MovableColumn extends React.Component<MovableColumnProps> {
+ // The header of the column
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
+ // The container of the function that is responsible for moving the column over to a new plac
private _colDropDisposer?: DragManager.DragDropDisposer;
+ // initial column position
private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ // sensitivity to being dragged, in pixels
private _sensitivity: number = 16;
+ // Column reference ID
private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
onPointerEnter = (e: React.PointerEvent): void => {
+ // if the column is left-clicked and it is being dragged
if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.addEventListener("pointermove", this.onDragMove, true);
}
}
+
onPointerLeave = (e: React.PointerEvent): void => {
this._header!.current!.className = "collectionSchema-col-wrapper";
document.removeEventListener("pointermove", this.onDragMove, true);
!e.buttons && document.removeEventListener("pointermove", this.onPointerMove);
}
+
onDragMove = (e: PointerEvent): void => {
+ // only take into account the horizonal direction when a column is dragged
const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
const rect = this._header!.current!.getBoundingClientRect();
+ // Now store the point at the top center of the column when it was in its original position
const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ // to be compared with its new horizontal position
const before = x[0] < bounds[0];
this._header!.current!.className = "collectionSchema-col-wrapper";
if (before) this._header!.current!.className += " col-before";
@@ -58,11 +69,15 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
colDrop = (e: Event, de: DragManager.DropEvent) => {
document.removeEventListener("pointermove", this.onDragMove, true);
+ // we only care about whether the column is shifted to the side
const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ // get the dimensions of the smallest rectangle that bounds the header
const rect = this._header!.current!.getBoundingClientRect();
const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
+ // get whether the column was dragged before or after where it is now
const before = x[0] < bounds[0];
const colDragData = de.complete.columnDragData;
+ // if there is colDragData, which happen when the drag is complete, reorder the columns according to the established variables
if (colDragData) {
e.stopPropagation();
this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
@@ -85,8 +100,10 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
document.removeEventListener("pointermove", onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
+ // if the left mouse button is the one being held
if (e.buttons === 1) {
const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
+ // If the movemnt of the drag exceeds the sensitivity value
if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
document.removeEventListener("pointermove", this.onPointerMove);
e.stopPropagation();
@@ -105,6 +122,7 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
this._dragRef = ref;
const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ // If the cell thing dragged is not being edited
if (!(e.target as any)?.tagName.includes("INPUT")) {
this._startDragPosition = { x: dx, y: dy };
document.addEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 9ebe14d6c..b64e9dac1 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -132,6 +132,13 @@
min-height: 30px;
border: 0 !important;
}
+ .rt-tr-group:nth-of-type(even) {
+ direction: ltr;
+ flex: 0 1 auto;
+ min-height: 30px;
+ border: 0 !important;
+ background-color: red;
+ }
.rt-tr {
width: 100%;
min-height: 30px;
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 219f7d3a2..db331bb75 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@material-ui/core";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, StrListCast, Field } from "../../../fields/Doc";
+import { Doc, NumListCast, StrListCast, Field } from "../../../fields/Doc";
import { DateCast, StrCast, Cast } from "../../../fields/Types";
import { LinkManager } from "../../util/LinkManager";
import { undoBatch } from "../../util/UndoManager";
@@ -42,14 +42,36 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
@undoBatch
setRelationshipValue = action((value: string) => {
if (LinkManager.currentLink) {
+ const prevRelationship = LinkManager.currentLink.linkRelationship as string;
+ LinkManager.currentLink.linkRelationship = value;
Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
+ const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+
// if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
- if (linkRelationshipList && !linkRelationshipList.includes(value)) {
+ if (!linkRelationshipList?.includes(value)) {
linkRelationshipList.push(value);
+ linkRelationshipSizes.push(1);
const randColor = "rgb(" + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + "," + Math.floor(Math.random() * 255) + ")";
linkColorList.push(randColor);
+ // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
+ } else if (linkRelationshipList && value !== prevRelationship) {
+ const index = linkRelationshipList.indexOf(value);
+ //increment size of new relationship size
+ if (index !== -1 && index < linkRelationshipSizes.length) {
+ const pvalue = linkRelationshipSizes[index];
+ linkRelationshipSizes[index] = (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1);
+ }
+ //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
+ if (linkRelationshipList.includes(prevRelationship)) {
+ const pindex = linkRelationshipList.indexOf(prevRelationship);
+ if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
+ const pvalue = linkRelationshipSizes[pindex];
+ linkRelationshipSizes[pindex] = Math.max(0, (pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1));
+ }
+ }
+
}
this.relationshipButtonColor = "rgb(62, 133, 55)";
setTimeout(action(() => this.relationshipButtonColor = ""), 750);
@@ -141,6 +163,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
style={{ width: "100%" }}
id="input"
value={this.relationship}
+ autoComplete={"off"}
placeholder={"Enter link relationship"}
onKeyDown={this.onRelationshipKey}
onChange={this.handleRelationshipChange}
@@ -169,6 +192,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
<div className="linkEditor-description-editing">
<input
style={{ width: "100%" }}
+ autoComplete={"off"}
id="input"
value={this.description}
placeholder={"Enter link description"}
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index cb6571f92..03377ad4e 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -31,7 +31,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
if (RGBcolor) {
//set opacity to 0.25 by modifiying the rgb string
color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)";
- console.log(color);
}
}
return color;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 728956378..96cc6d600 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -165,19 +165,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
<div className="linkMenu-item-buttons" ref={this._buttonRef} >
- <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Anchor" : "Hide Anchor"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.hidden ? "eye-slash" : "eye"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.hidden ? "Show Link Anchor" : "Hide Link Anchor"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"eye"} size="sm" /></div>
</Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkDisplay ? "Show link" : "Hide link"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={!this.props.linkDoc.linkDisplay ? "eye-slash" : "eye"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? "Hide Link Line" : "Show Link Line"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : this.props.linkDoc.linkDisplay ? "#4476f7"/* $medium-blue */ : "" }} onPointerDown={this.showLink} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"project-diagram"} size="sm" /></div>
</Tooltip>
- <Tooltip title={<><div className="dash-tooltip">{!this.props.linkDoc.linkAutoMove ? "Auto move dot" : "Freeze dot position"}</div></>}>
- <div className="button" ref={this._editRef} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={this.props.linkDoc.linkAutoMove ? "play" : "pause"} size="sm" /></div>
+ <Tooltip title={<><div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? "Click to freeze link anchor position" : "Click to auto move link anchor"}</div></>}>
+ <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? "gray" : !this.props.linkDoc.linkAutoMove ? "" : "#4476f7" /* $medium-blue */ }} onPointerDown={this.autoMove} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon={"play"} size="sm" /></div>
</Tooltip>
<Tooltip title={<><div className="dash-tooltip">Edit Link</div></>}>
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index f23c68409..750213e67 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -88,15 +88,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</div>;
};
const displayDoc = (which: string) => {
- var whichDoc = Cast(this.dataDoc[which], Doc, null);
- if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
+ const whichDoc = Cast(this.dataDoc[which], Doc, null);
+ //if (whichDoc?.type === DocumentType.MARKER)
+ const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
return whichDoc ? <>
- <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
+ <DocumentView
+ ref={(r) => {
+ whichDoc !== targetDoc && r?.focus(targetDoc);
+ }}
+ {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit}
isContentActive={returnFalse}
isDocumentActive={returnFalse}
styleProvider={this.docStyleProvider}
- Document={whichDoc}
+ Document={targetDoc}
DataDoc={undefined}
+ hideLinkButton={true}
pointerEvents={"none"} />
{clearButton(which)}
</> : // placeholder image if doc is missing
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1ccf38de2..949e0e168 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -813,7 +813,11 @@ 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 ||
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/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 8e61a224c..2a153f256 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -404,7 +404,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
} else if ((curDoc.presMovement === PresMovement.Zoom || curDoc.presMovement === PresMovement.Jump) && targetDoc) {
LightboxView.SetLightboxDoc(undefined);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true); // documents open in new tab instead of on right
+ await DocumentManager.Instance.jumpToDocument(targetDoc, true, openInTab, srcContext, undefined, undefined, undefined, includesDoc || tab ? undefined : resetSelection, undefined, true, NumCast(curDoc.presZoom)); // documents open in new tab instead of on right
}
// After navigating to the document, if it is added as a presPinView then it will
// adjust the pan and scale to that of the pinView when it was added.
@@ -1026,6 +1026,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
Array.from(this._selectedArray.keys()).forEach((doc) => doc.presTransition = timeInMS);
}
+ // Converts seconds to ms and updates presTransition
+ setZoom = (number: String, change?: number) => {
+ let scale = Number(number) / 100;
+ if (change) scale += change;
+ if (scale < 0.01) scale = 0.01;
+ if (scale > 1.5) scale = 1.5;
+ Array.from(this._selectedArray.keys()).forEach((doc) => doc.presZoom = scale);
+ }
+
// Converts seconds to ms and updates presDuration
setDurationTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
@@ -1143,11 +1152,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
@computed get transitionDropdown() {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const type = targetDoc.type;
const isPresCollection: boolean = (targetDoc === this.layoutDoc.presCollection);
const isPinWithView: boolean = BoolCast(activeItem.presPinView);
if (activeItem && targetDoc) {
+ const type = targetDoc.type;
const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
+ const zoom = activeItem.presZoom ? NumCast(activeItem.presZoom) * 100 : 75;
let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
const effect = targetDoc.presEffect ? targetDoc.presEffect : 'None';
@@ -1172,6 +1182,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps, PresBoxSchema>
</div>
</div>
}
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
+ <div className="presBox-subheading">Zoom (% screen filled)</div>
+ <div className="ribbon-property">
+ <input className="presBox-input"
+ type="number" value={zoom}
+ onChange={action((e) => this.setZoom(e.target.value))} />%
+ </div>
+ <div className="ribbon-propertyUpDown">
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+ <FontAwesomeIcon icon={"caret-up"} />
+ </div>
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+ <FontAwesomeIcon icon={"caret-down"} />
+ </div>
+ </div>
+ </div>
+ <input type="range" step="1" min="0" max="150" value={zoom}
+ className={`toolbar-slider ${activeItem.presMovement === PresMovement.Zoom ? "" : "none"}`}
+ id="toolbar-slider"
+ onPointerDown={() => this._batch = UndoManager.StartBatch("presZoom")}
+ onPointerUp={() => this._batch?.end()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ this.setZoom(e.target.value);
+ }} />
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? "inline-flex" : "none" }}>
<div className="presBox-subheading">Movement Speed</div>
<div className="ribbon-property">
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 9490edc2c..42d93c60c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -79,6 +79,7 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> { return Cast(field, Doc); }
+export function NumListCast(field: FieldResult) { return Cast(field, listSpec("number"), []); }
export function StrListCast(field: FieldResult) { return Cast(field, listSpec("string"), []); }
export function DocListCast(field: FieldResult) { return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[]; }
export function DocListCastOrNull(field: FieldResult) { return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined; }
@@ -1313,6 +1314,7 @@ export namespace Doc {
if (typeof resolved === "object" && !(resolved instanceof Array)) {
output = convertObject(resolved, excludeEmptyObjects, title, appendToExisting?.targetDoc);
} else {
+ // give the proper types to the data extracted from the JSON
const result = toField(resolved, excludeEmptyObjects);
if (appendToExisting) {
(output = appendToExisting.targetDoc)[appendToExisting.fieldKey || defaultKey] = result;
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 7b83d09ef..54b71e8ce 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';
@@ -342,7 +343,8 @@ export namespace DashUploadUtils {
resolve({ data, error: reason });
});
});
- data && bufferConverterRec(data);
+ return { data: await exifr.parse(image) as any, error };
+ //data && bufferConverterRec(data);
return { data, error };
};
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 0f4a067fc..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,37 +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());
- 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)
@@ -139,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();