From 441a3dab4ada425d28a55435be51339e3d28c892 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:44:19 -0500 Subject: adding comments --- src/fields/InkField.ts | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'src/fields') diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index dbe51b24a..b79a03146 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -4,6 +4,7 @@ import { ObjectField } from "./ObjectField"; import { Copy, ToScriptString, ToString, Update } from "./FieldSymbols"; import { Scripting } from "../client/util/Scripting"; +// Helps keep track of the current ink tool in use. export enum InkTool { None = "none", Pen = "pen", @@ -12,11 +13,13 @@ export enum InkTool { Stamp = "stamp" } +// Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; Y: number; } +// Defines an ink as an array of points. export type InkData = Array; const pointSchema = createSimpleSchema({ @@ -28,6 +31,7 @@ const strokeDataSchema = createSimpleSchema({ "*": true }); +// Holistic class representing the store of an ink. @Deserializable("ink") export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) -- cgit v1.2.3-70-g09d2 From b0efa4a390415072eaeb06c8719ea57d73e10466 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Wed, 30 Jun 2021 12:52:52 -0500 Subject: ink Bézier handle movement fixed + small visual changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/util/InteractionUtils.tsx | 2 +- src/client/views/InkStrokeProperties.ts | 119 ++++++++++++++++++++++++-------- src/client/views/InkingStroke.scss | 18 ++--- src/client/views/InkingStroke.tsx | 93 ++++++++++++++----------- src/fields/InkField.ts | 24 +++++++ 5 files changed, 177 insertions(+), 79 deletions(-) (limited to 'src/fields') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 01d00db30..ba935e3bf 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -208,7 +208,7 @@ export namespace InteractionUtils { (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt); @@ -79,10 +78,6 @@ export class InkStrokeProperties { }); } - constructor() { - InkStrokeProperties.Instance = this; - } - /** * Adds a new control point to the ink instance when editing its format. * @param x The x-coordinate of the current new point. @@ -213,35 +208,99 @@ export class InkStrokeProperties { } /** - * Handles the movement / scaling of control points of an ink instance. - * @param xDiff The movement of the control point's x-coordinate. - * @param yDiff The movement of the control point's y-coordinate. - * @param controlNum The index of the current control point selected. - * @returns The changed x- and y-coordinates of the control points. + * Handles the movement/scaling of a control point. */ @undoBatch @action - control = (xDiff: number, yDiff: number, controlNum: number) => - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; + const order = controlIndex % 4; for (var i = 0; i < ink.length; i++) { - newPoints.push( - (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) || - ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) ? - { X: ink[i].X - xDiff / ptsXscale, Y: ink[i].Y - yDiff / ptsYscale } : - { X: ink[i].X, Y: ink[i].Y }); + const leftHandlePoint = order === 0 && i === controlIndex + 1; + const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; + if (controlIndex === i || + leftHandlePoint || + rightHandlePoint || + (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + (order === 3 && i === controlIndex - 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || + ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { + newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } } return newPoints; }); + /** + * Rotates the target point about the origin point for a given angle (radians). + */ + @action + rotatePoint = (target: PointData, origin: PointData, angle: number) => { + target.X -= origin.X; + target.Y -= origin.Y; + const newX = Math.cos(angle) * target.X - Math.sin(angle) * target.Y; + const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y; + target.X = newX + origin.X; + target.Y = newY + origin.Y; + return target + } + + /** + * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin. + */ + angleChange = (a: PointData, b: PointData, origin: PointData) => { + // Finding vector representation of inputted points relative to new origin. + let vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y }; + let vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y }; + const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X; + // Determining whether rotation is clockwise or counterclockwise. + const sign = crossProduct < 0 ? 1 : -1; + const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); + const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); + // Normalizing the vectors. + vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; + vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; + const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; + const theta = Math.acos(dotProduct); + return sign * theta; + } + + /** + * Handles the movement/scaling of a handle point. + */ + @undoBatch + @action + moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const newPoints: { X: number, Y: number }[] = []; + const order = handleIndex % 4; + let newHandlePoint = { X: 0, Y: 0 }; + + for (var i = 0; i < ink.length; i++) { + if (handleIndex === i) { + newHandlePoint = { X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }; + newPoints.push({ X: newHandlePoint.X, Y: newHandlePoint.Y }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + + if (handleIndex !== 1 && handleIndex !== ink.length - 2) { + const oldHandlePoint = ink[handleIndex]; + let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; + const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + order === 1 ? newPoints[handleIndex - 3] = oppositeHandlePoint : newPoints[handleIndex + 3] = oppositeHandlePoint; + } + + return newPoints; + }); + /** * Changes the color of the border of the ink instance. * @param color The new hex value to change the border to. diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss index 30ab1967e..f67b1779d 100644 --- a/src/client/views/InkingStroke.scss +++ b/src/client/views/InkingStroke.scss @@ -1,11 +1,11 @@ .inkingStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; - svg:not(:root) { - overflow: visible !important; - } -} \ No newline at end of file + svg:not(:root) { + overflow: visible !important; + } +} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 859e53b97..163eb05b0 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -2,7 +2,7 @@ import { action } 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 { InkData, InkField, InkTool, ControlPoint, HandlePoint, HandleLine } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { Cast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; @@ -28,33 +28,51 @@ export class InkingStroke extends ViewBoxBaseComponent { + analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); } - public static toggleMask = action((inkDoc: Doc) => { + @action + public static toggleMask = (inkDoc: Doc) => { inkDoc.isInkMask = !inkDoc.isInkMask; inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; inkDoc.color = "#9b9b9bff"; inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; - }); + }; /** * Handles the movement of a selected control point when the user clicks and drags. - * @param e React Pointer Event. - * @param controlNum The number of the currently selected control point. + * @param controlNum The index of the currently selected control point. */ @action onControlDown = (e: React.PointerEvent, controlNum: number): void => { if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance.control(0, 0, 1); + InkStrokeProperties.Instance.moveControl(0, 0, 1); + const controlUndo = UndoManager.StartBatch("DocDecs set radius"); + const screenScale = this.props.ScreenToLocalTransform().Scale; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { + InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + return false; + }, + () => controlUndo?.end(), emptyFunction); + } + } + + /** + * Handles the movement of a selected handle point when the user clicks and drags. + * @param controlNum The index of the currently selected handle point. + */ + onHandleDown = (e: React.PointerEvent, handleNum: number): void => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.control(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); return false; }, () => controlUndo?.end(), emptyFunction); @@ -69,7 +87,7 @@ export class InkingStroke extends ViewBoxBaseComponent { if (InkStrokeProperties.Instance) { InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.delPts, true); + document.addEventListener("keydown", this.onDelete, true); } } @@ -78,7 +96,7 @@ export class InkingStroke extends ViewBoxBaseComponent { + onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); } @@ -101,7 +119,6 @@ export class InkingStroke extends ViewBoxBaseComponent p.X); const ys = data.map(p => p.Y); @@ -118,12 +135,18 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); + + const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, strokeWidth / 6, + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); // Invisible polygonal line that enables the ink to be selected by the user. const hpoints = InteractionUtils.CreatePolyline(data, left, top, @@ -136,10 +159,13 @@ export class InkingStroke extends ViewBoxBaseComponent= 4) { + + // create separate functions for these for (var i = 0; i <= data.length - 4; i += 4) { controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); @@ -147,29 +173,21 @@ export class InkingStroke extends ViewBoxBaseComponent @@ -178,27 +196,24 @@ export class InkingStroke extends ViewBoxBaseComponent { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> ); - // Green circles that allow the user to edit the curvature of the line using the selected point as the anchor. + // Blue circles that allow the user to edit the curvature of the line using the selected control point as the anchor. const handles = handlePoints.map((pts, i) => - this.onControlDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> + this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> ); - // Points (red circles) of the ink that are made visible to user when editing its format. + // Control points of the ink (blue outlined squares) that are made visible to user when editing its format. const controls = controlPoints.map((pts, i) => - { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" /> ); - // Set of two green lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. + // Set of two blue lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. const handleLines = handleLine.map((pts, i) => - + - ); @@ -227,8 +242,8 @@ export class InkingStroke extends ViewBoxBaseComponent ); } diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index b79a03146..c158dac42 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -13,6 +13,7 @@ export enum InkTool { Stamp = "stamp" } + // Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; @@ -22,6 +23,29 @@ export interface PointData { // Defines an ink as an array of points. export type InkData = Array; +export interface ControlPoint { + X: number; + Y: number; + I: number; +} + +export interface HandlePoint { + X: number; + Y: number; + I: number; + dot1: number; + dot2: number; +} + +export interface HandleLine { + X1: number; + Y1: number; + X2: number; + Y2: number; + dot1: number; + dot2: number; +} + const pointSchema = createSimpleSchema({ X: true, Y: true }); -- cgit v1.2.3-70-g09d2 From 5ab81f49a11bd8a74725228a887a90c88a3848ff Mon Sep 17 00:00:00 2001 From: vkalev Date: Tue, 6 Jul 2021 14:27:29 -0500 Subject: added breaking of tangent handle lines by holding 'Alt' key when moving --- src/client/views/InkHandles.tsx | 32 +++++++++++++++++++++++++++----- src/client/views/InkStrokeProperties.ts | 6 +++--- src/fields/InkField.ts | 2 ++ 3 files changed, 32 insertions(+), 8 deletions(-) (limited to 'src/fields') diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index c2163c124..993e427b3 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -15,6 +15,8 @@ export interface InkControlProps { @observer export class InkHandles extends React.Component { + @observable private _brokenIndices: number[] = []; + /** * Handles the movement of a selected handle point when the user clicks and drags. * @param handleNum The index of the currently selected handle point. @@ -24,13 +26,25 @@ export class InkHandles extends React.Component { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; + document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, handleNum), true); setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum, this._brokenIndices); return false; }, () => controlUndo?.end(), emptyFunction ); } } + + @action + onBreakTangent = (e: KeyboardEvent, handleIndex: number) => { + if (["Alt"].includes(e.key)) { + const order = handleIndex % 4; + const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + if (!this._brokenIndices.includes(handleIndex) && !this._brokenIndices.includes(oppositeHandleIndex)) { + this._brokenIndices.push(handleIndex, oppositeHandleIndex); + } + } + } render() { const formatInstance = InkStrokeProperties.Instance; @@ -39,15 +53,15 @@ export class InkHandles extends React.Component { const handlePoints: HandlePoint[] = []; const handleLines: HandleLine[] = []; if (data.length >= 4) { - // adding first and last (single) handle lines - handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[1].X, Y2: data[1].Y, dot1: 0, dot2: 0 }); - handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); for (let i = 0; i <= data.length - 4; i += 4) { handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); } + // adding first and last (single) handle lines + handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 }); + handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); for (let i = 2; i < data.length - 4; i += 4) { - handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 3].X, Y2: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); + handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); } } const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format; @@ -77,6 +91,14 @@ export class InkHandles extends React.Component { stroke="#1F85DE" strokeWidth={dotsize / 8} display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + )} ); diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index a5c028730..812e8ff6e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -220,15 +220,15 @@ export class InkStrokeProperties { */ @undoBatch @action - moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => + moveHandle = (deltaX: number, deltaY: number, handleIndex: number, brokenIndices: number[]) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const order = handleIndex % 4; const oldHandlePoint = ink[handleIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; - // Rotating opposite handle (first and final control point only have one handle). - if (handleIndex !== 1 && handleIndex !== ink.length - 2) { + // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). + if (!brokenIndices.includes(handleIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index c158dac42..485376a34 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -42,6 +42,8 @@ export interface HandleLine { Y1: number; X2: number; Y2: number; + X3: number; + Y3: number; dot1: number; dot2: number; } -- cgit v1.2.3-70-g09d2 From 48620bbe25f92eb179d53846aae5f0164ca6f1c2 Mon Sep 17 00:00:00 2001 From: vkalev Date: Thu, 15 Jul 2021 01:27:43 -0500 Subject: adding new point creates tangent handle lines + snapping handle tangents not working --- src/client/views/InkControls.tsx | 12 ++- src/client/views/InkHandles.tsx | 1 - src/client/views/InkStrokeProperties.ts | 170 +++++++++++++++++++++----------- src/fields/InkField.ts | 3 - 4 files changed, 121 insertions(+), 65 deletions(-) (limited to 'src/fields') diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx index 4d8b2c6b5..da7b0df16 100644 --- a/src/client/views/InkControls.tsx +++ b/src/client/views/InkControls.tsx @@ -30,12 +30,18 @@ export class InkControls extends React.Component { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = controlIndex % 4; + const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2; + const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex); return false; }, - () => controlUndo?.end(), emptyFunction); + () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) => + { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { + InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + }})); } } @@ -112,7 +118,9 @@ export class InkControls extends React.Component { width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} strokeWidth={strokeWidth / 6} stroke="#1F85DE" fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"} - onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} + onPointerDown={(e) => { + this.changeCurrPoint(control.I); + this.onControlDown(e, control.I); }} onMouseEnter={() => this.onEnterControl(i)} onMouseLeave={this.onLeaveControl} pointerEvents="all" diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index a33380221..ba3fdf9db 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -24,7 +24,6 @@ export class InkHandles extends React.Component { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; - const order = handleIndex % 4; const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2; diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 4ec03c560..a3f7562e0 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -71,70 +71,109 @@ export class InkStrokeProperties { */ @undoBatch @action - addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1) { - const newPoint = { X: x, Y: y }; - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - - // Updating the indices of the control points whose handle tangency has been broken. - this._brokenIndices = this._brokenIndices.map((control) => { - if (control >= spNum) { - return control + 4; - } else { - return control; + addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => { + this.applyFunction((doc: Doc, ink: InkData) => { + const newControl = { X: x, Y: y }; + const newPoints: InkData = []; + let [counter, start, end] = [0, 0, 0]; + for (let k = 0; k < points.length; k++) { + if (end === 0) { + controls.forEach((control) => { + if (control.X === points[k].X && control.Y === points[k].Y) { + if (k < index) { + counter++; + start = k; + } else if (k > index) { + end = k; } - }); - - // const [handleA, handleB] = this.getNewHandlePoints(newPoint, pts[index-1], pts[index+1]); - newPoints.push(newPoint); - newPoints.push(newPoint); - newPoints.push(newPoint); - newPoints.push(newPoint); - - for (var i = spNum; i < ink.length; i++) { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - this._currentPoint = -1; - Doc.GetProto(doc).data = new InkField(newPoints); - } + }); } } - })); + if (end === 0) end = points.length-1; + // Index of new control point with regards to the ink data. + const newIndex = Math.floor(counter / 2) * 4 + 2; + // Creating new ink data with new control point and handle points inputted. + for (let i = 0; i < ink.length; i++) { + if (i === newIndex) { + const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index+1), points.slice(index, end), newControl); + newPoints.push(handleA, newControl, newControl, handleB); + const [rightControl, rightHandle] = [points[end], ink[i]]; + const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle); + rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y }); + } else if (i === newIndex - 1) { + const [leftControl, leftHandle] = [points[start], ink[i]]; + const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle); + leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y }); + } else { + ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + + } + // Updating the indices of the control points whose handle tangency has been broken. + this._brokenIndices = this._brokenIndices.map((control) => { + if (control >= newIndex) { + return control + 4; + } else { + return control; + } + }); + this._currentPoint = -1; + return newPoints; + }); } - getNewHandlePoints = (newControl: PointData, a: PointData, b: PointData) => { - // find midpoint between the left and right control point of new control - // rotate midpoint by +-pi/2 to get new handle points - // multiplying x-y coordinates of both by 10/L where L is its current magnitude - const angle = this.angleChange(a, b, newControl); - const midpoint = this.rotatePoint(a, newControl, angle/2); - // const handleA = this.rotatePoint(midpoint, newControl, -Math.PI/2); - // const handleB = this.rotatePoint(midpoint, newControl, -Math.PI/2); - const handleA = { X: midpoint.X + (20 * Math.cos(-Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(-Math.PI/2)) }; - const handleB = { X: midpoint.X + (20 * Math.cos(Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(Math.PI/2)) }; + /** + * Scales a handle point of a control point that is adjacent to a newly added one. + * @param isLeft Determines if the current control point is on the left or right side of the newly added one. + * @param start Beginning index of curve from the left control point to the newly added one. + * @param end Final index of curve from the newly added control point to its right neighbor. + */ + getScaledHandlePoint(isLeft: boolean, start: number, end: number, index: number, control: PointData, handle: PointData) { + const prevSize = end - start; + const newSize = isLeft ? index - start : end - index; + const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y }; + const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; + return scaledVector; + } + /** + * Determines the position of the handle points of a newly added control point by finding the + * tangent vectors to the split curve at the new control. Given the properties of Bézier curves, + * the tangent vector to a control point is equivalent to the first/last (depending on the direction + * of the curve) leg of the Bézier curve's derivative. + * (Source: https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html) + * + * @param C The curve represented by all points from the previous control until the newly added point. + * @param D The curve represented by all points from the newly added point to the next control. + * @param newControl The newly added control point. + */ + getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => { + const [m, n] = [C.length, D.length]; + let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2))); + let handleSizeB = Math.sqrt((Math.pow(D[n-1].X - newControl.X, 2)) + (Math.pow(D[n-1].Y - newControl.Y, 2))); + if (handleSizeA < 75 && handleSizeB < 75) { + handleSizeA *= 3; + handleSizeB *= 3; + } + if (Math.abs(handleSizeA - handleSizeB) < 50) { + handleSizeA *= 5; + handleSizeB *= 5; + } else if (Math.abs(handleSizeA - handleSizeB) < 150) { + handleSizeA *= 2; + handleSizeB *= 2; + } + // Finding the last leg of the derivative curve of C. + const dC = { X: (handleSizeA / n) * (C[m-1].X - C[m-2].X), Y: (handleSizeA / n) * (C[m-1].Y - C[m-2].Y) }; + // Finding the first leg of the derivative curve of D. + const dD = { X: (handleSizeB / m) * (D[1].X - D[0].X), Y: (handleSizeB / m) * (D[1].Y - D[0].Y) }; + const handleA = { X: newControl.X - dC.X, Y: newControl.Y - dC.Y }; + const handleB = { X: newControl.X + dD.X, Y: newControl.Y + dD.Y }; return [handleA, handleB]; } /** - * Deletes the points of the current ink instance. - * @returns The changed x- and y-coordinates of the control points. + * Deletes the current control point of the selected ink instance. */ @undoBatch @action @@ -160,9 +199,8 @@ export class InkStrokeProperties { }, true) /** - * Rotates the points of the current ink instance by a certain angle degree. - * @param angle The angle at which to rotate the ink (all of its x- and y-coordinates). - * @returns The changed x- and y-coordinates of the control points. + * Rotates the entire selected ink instance. + * @param angle The angle at which to rotate the ink in radians. */ @undoBatch @action @@ -210,6 +248,18 @@ export class InkStrokeProperties { return newPoints; }) + snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => { + this.applyFunction((doc: Doc, ink: InkData) => { + this._brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 1); + const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]]; + const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI); + const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint); + const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference); + ink[handleIndexB] = newHandleB; + return ink; + }); + } + /** * Rotates the target point about the origin point for a given angle (radians). */ @@ -224,6 +274,11 @@ export class InkStrokeProperties { return target; } + /** + * Finds the angle between two inputted vectors. + * + * α = arccos(a·b / |a|·|b|), where a and b are both vectors. + */ angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => { const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); @@ -255,20 +310,17 @@ export class InkStrokeProperties { @action moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { - const order = handleIndex % 4; const oldHandlePoint = ink[handleIndex]; let oppositeHandlePoint = ink[oppositeHandleIndex]; const controlPoint = ink[controlIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; - // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). if (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); ink[oppositeHandleIndex] = oppositeHandlePoint; } - return ink; }) } \ No newline at end of file diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 485376a34..1270a2dab 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -57,13 +57,10 @@ const strokeDataSchema = createSimpleSchema({ "*": true }); -// Holistic class representing the store of an ink. @Deserializable("ink") export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; - // inkData: InkData; - constructor(data: InkData) { super(); -- cgit v1.2.3-70-g09d2 From 14e66ac5bcdaa5e244be68ccb8cbb0c495917910 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 23 Jul 2021 14:41:18 -0400 Subject: fixed issues with layoutString templates (eg customView): scale properly when in a time view, have a data doc, scripts are called with 'scale' parmeter --- src/client/util/CurrentUserUtils.ts | 16 +++++++++------ src/client/views/DocComponent.tsx | 2 +- src/client/views/nodes/DocumentContentsView.tsx | 11 +++++----- .../views/nodes/formattedText/FormattedTextBox.tsx | 11 +++++----- src/fields/Doc.ts | 24 ++++++++++++++++++++-- 5 files changed, 45 insertions(+), 19 deletions(-) (limited to 'src/fields') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8a98304b2..22504f102 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -405,14 +405,18 @@ export class CurrentUserUtils { selection: { type: "text", anchor: 1, head: 1 }, storedMarks: [] }; - const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { title: "text", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, cloneFieldFilter: new List(["system"]) }, "header"); // text needs to be a space to allow templateText to be created + const headerTemplate = Docs.Create.RTFDocument(new RichTextField(JSON.stringify(json), ""), { + title: "text", version: headerViewVersion, target: doc, _height: 70, _headerPointerEvents: "all", + _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, system: true, _fitWidth: true, + cloneFieldFilter: new List(["system"]) + }, "header"); const headerBtnHgt = 10; headerTemplate[DataSym].layout = - "
" + - ` ` + - " " + - ` Metadata` + - "
"; + "" + + ` ` + + " " + + ` Metadata` + + ""; // "
" + // " " + diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index da8af7cc0..0b70ce68d 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -119,7 +119,7 @@ export function ViewBoxAnnotatableComponent

{ const style: { [key: string]: any } = {}; - const divKeys = ["width", "height", "fontSize", "left", "background", "left", "right", "top", "bottom", "pointerEvents", "position"]; + const divKeys = ["width", "height", "fontSize", "transform", "left", "background", "left", "right", "top", "bottom", "pointerEvents", "position"]; const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a property expression string: { script } into a value return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.rootDoc, this: this.layoutDoc, scale }).result as string || ""; }; diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index a0a40becb..9b75cd8f9 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -64,6 +64,7 @@ interface HTMLtagProps { htmltag: string; onClick?: ScriptField; onInput?: ScriptField; + scaling: number; } //" {this.title}" @@ -82,7 +83,7 @@ interface HTMLtagProps { export class HTMLtag extends React.Component { click = (e: React.MouseEvent) => { const clickScript = (this.props as any).onClick as Opt; - clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc }); + clickScript?.script.run({ this: this.props.Document, self: this.props.RootDoc, scale: this.props.scaling }); } onInput = (e: React.FormEvent) => { const onInputScript = (this.props as any).onInput as Opt; @@ -90,9 +91,9 @@ export class HTMLtag extends React.Component { } render() { const style: { [key: string]: any } = {}; - const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "Document", "key", "onInput", "onClick", "__proto__"]).omit; + const divKeys = OmitKeys(this.props, ["children", "htmltag", "RootDoc", "scaling", "Document", "key", "onInput", "onClick", "__proto__"]).omit; const replacer = (match: any, expr: string, offset: any, string: any) => { // bcz: this executes a script to convert a propery expression string: { script } into a value - return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name })?.script.run({ self: this.props.RootDoc, this: this.props.Document }).result as string || ""; + return ScriptField.MakeFunction(expr, { self: Doc.name, this: Doc.name, scale: "number" })?.script.run({ self: this.props.RootDoc, this: this.props.Document, scale: this.props.scaling }).result as string || ""; }; Object.keys(divKeys).map((prop: string) => { const p = (this.props as any)[prop] as string; @@ -184,7 +185,7 @@ export class DocumentContentsView extends React.Component with corresponding HTML tag as in: becomes const replacer2 = (match: any, p1: string, offset: any, string: any) => { - return ` 1) { const code = XRegExp.matchRecursive(splits[1], "{", "}", "", { valueNames: ["between", "left", "match", "right", "between"] }); layoutFrame = splits[0] + ` ${func}={props.${func}} ` + splits[1].substring(code[1].end + 1); - return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, value: "string" }); + return ScriptField.MakeScript(code[1].value, { this: Doc.name, self: Doc.name, scale: "number", value: "string" }); } return undefined; // add input function to props diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 95d8f555c..6dd63fb47 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -71,6 +71,7 @@ export interface FormattedTextBoxProps { xPadding?: number; // used to override document's settings for xMargin --- see CollectionCarouselView yPadding?: number; noSidebar?: boolean; + dontScale?: boolean; dontSelectOnLoad?: boolean; // suppress selecting the text box when loaded (and mark as not being associated with scrollTop document field) } export const GoogleRef = "googleDocId"; @@ -126,7 +127,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); } @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); } @computed get titleHeight() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get autoHeightMargins() { return this.titleHeight + (this.layoutDoc._autoHeightMargins && !this.props.dontSelectOnLoad ? NumCast(this.layoutDoc._autoHeightMargins) : 0); } + @computed get autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._autoHeightMargins); } @computed get _recording() { return this.dataDoc?.mediaState === "recording"; } set _recording(value) { !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? "recording" : undefined); @@ -1524,10 +1525,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp

this.isContentActive() && e.stopPropagation()} style={{ - transform: `scale(${scale})`, - transformOrigin: "top left", - width: `${100 / scale}%`, - height: `${100 / scale}%`, + transform: this.props.dontScale ? undefined : `scale(${scale})`, + transformOrigin: this.props.dontScale ? undefined : "top left", + width: this.props.dontScale ? undefined : `${100 / scale}%`, + height: this.props.dontScale ? undefined : `${100 / scale}%`, // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, ...this.styleFromLayoutString(scale) // this converts any expressions in the format string to style props. e.g., }}> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index bd0ba3ad7..464a8ad05 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -803,6 +803,27 @@ export namespace Doc { return undefined; } + // Makes a delegate of a document by first creating a delegate where data should be stored + // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc). + // This is appropriate if you're trying to create a document that behaves like all + // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs) + export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc { + const delegateProto = new Doc(); + delegateProto[Initializing] = true; + delegateProto.proto = doc; + delegateProto.author = Doc.CurrentUserEmail; + delegateProto.isPrototype = true; + title && (delegateProto.title = title); + const delegate = new Doc(id, true); + delegate[Initializing] = true; + delegate.proto = delegateProto; + delegate.author = Doc.CurrentUserEmail; + Doc.AddDocToList(delegateProto[DataSym], "aliases", delegate); + delegate[Initializing] = false; + delegateProto[Initializing] = false; + return delegate; + } + let _applyCount: number = 0; export function ApplyTemplate(templateDoc: Doc) { if (templateDoc) { @@ -1150,8 +1171,7 @@ export namespace Doc { return ndoc; } export function delegateDragFactory(dragFactory: Doc) { - const ndoc = Doc.MakeDelegate(dragFactory); - ndoc.isPrototype = true; + const ndoc = Doc.MakeDelegateWithProto(dragFactory); if (ndoc && dragFactory["dragFactory-count"] !== undefined) { dragFactory["dragFactory-count"] = NumCast(dragFactory["dragFactory-count"]) + 1; Doc.GetProto(ndoc).title = ndoc.title + " " + NumCast(dragFactory["dragFactory-count"]).toString(); -- cgit v1.2.3-70-g09d2 From 8717e90d12c8caa16984f5a55eb8f442dcf5cbab Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 27 Jul 2021 14:31:30 -0400 Subject: fixe MakeClone to handle links properly. fixed cloning richtext to update rich text references to documents properly. fixed dragging to call MakeClone properly. --- src/client/util/DragManager.ts | 12 +++---- src/client/util/LinkManager.ts | 38 ++++++++-------------- src/client/util/Scripting.ts | 2 +- src/client/views/StyleProvider.tsx | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +-- .../collectionSchema/CollectionSchemaCells.tsx | 20 ++++++------ src/fields/Doc.ts | 33 ++++++++++++------- 8 files changed, 55 insertions(+), 58 deletions(-) (limited to 'src/fields') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index ab58f25e9..dd50727dd 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -210,16 +210,16 @@ export namespace DragManager { dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); return dropDoc; }; - const finishDrag = (e: DragCompleteEvent) => { + const finishDrag = async (e: DragCompleteEvent) => { const docDragData = e.docDragData; dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails if (docDragData && !docDragData.droppedDocuments.length) { docDragData.dropAction = dragData.userDropAction || dragData.dropAction; docDragData.droppedDocuments = - dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : + await Promise.all(dragData.draggedDocuments.map(async d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) : docDragData.dropAction === "alias" ? Doc.MakeAlias(d) : docDragData.dropAction === "proto" ? Doc.GetProto(d) : - docDragData.dropAction === "copy" ? Doc.MakeClone(d) : d); + docDragData.dropAction === "copy" ? (await Doc.MakeClone(d)).clone : d)); !["same", "proto"].includes(docDragData.dropAction as any) && docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []); const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); @@ -509,7 +509,7 @@ export namespace DragManager { `translate(${(xs[i] += moveVec.x) + (options?.offsetX || 0)}px, ${(ys[i] += moveVec.y) + (options?.offsetY || 0)}px) scale(${scaleXs[i]}, ${scaleYs[i]})`) ); }; - const upHandler = (e: PointerEvent) => { + const upHandler = async (e: PointerEvent) => { dispatchDrag(document.elementFromPoint(e.x, e.y) || document.body, e, new DragCompleteEvent(false, dragData), snapDrag(e, xFromLeft, yFromTop, xFromRight, yFromBottom), finishDrag, options); endDrag(); }; @@ -517,7 +517,7 @@ export namespace DragManager { document.addEventListener("pointerup", upHandler); } - function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions) { + async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number, y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions) { const dropArgs = { bubbles: true, detail: { @@ -531,7 +531,7 @@ export namespace DragManager { } }; target.dispatchEvent(new CustomEvent("dashPreDrop", dropArgs)); - finishDrag?.(complete); + await finishDrag?.(complete); target.dispatchEvent(new CustomEvent("dashOnDrop", dropArgs)); options?.dragComplete?.(complete); } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 3c3d5c3b8..08f4ac9b7 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,15 +1,14 @@ +import { observable, observe, action } from "mobx"; import { computedFn } from "mobx-utils"; -import { Doc, DocListCast, Opt, DirectLinksSym, Field } from "../../fields/Doc"; -import { BoolCast, Cast, StrCast, PromiseValue } from "../../fields/Types"; +import { DirectLinksSym, Doc, DocListCast, Field, Opt } from "../../fields/Doc"; +import { List } from "../../fields/List"; +import { ProxyField } from "../../fields/Proxy"; +import { BoolCast, Cast, PromiseValue, StrCast } from "../../fields/Types"; import { LightboxView } from "../views/LightboxView"; import { DocumentViewSharedProps, ViewAdjustment } from "../views/nodes/DocumentView"; import { DocumentManager } from "./DocumentManager"; import { SharingManager } from "./SharingManager"; import { UndoManager } from "./UndoManager"; -import { observe, observable, reaction } from "mobx"; -import { listSpec } from "../../fields/Schema"; -import { List } from "../../fields/List"; -import { ProxyField } from "../../fields/Proxy"; type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* @@ -34,7 +33,7 @@ export class LinkManager { LinkManager._instance = this; setTimeout(() => { LinkManager.userDocs = [Doc.LinkDBDoc().data as Doc, ...SharingManager.Instance.users.map(user => user.linkDatabase)]; - const addLinkToDoc = (link: Doc): any => { + const addLinkToDoc = action((link: Doc): any => { const a1 = link?.anchor1; const a2 = link?.anchor2; if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => addLinkToDoc(link))); @@ -43,8 +42,8 @@ export class LinkManager { Doc.GetProto(a2)[DirectLinksSym].add(link); Doc.GetProto(link)[DirectLinksSym].add(link); } - }; - const remLinkFromDoc = (link: Doc): any => { + }); + const remLinkFromDoc = action((link: Doc): any => { const a1 = link?.anchor1; const a2 = link?.anchor2; if (a1 instanceof Promise || a2 instanceof Promise) return PromiseValue(a1).then(a1 => PromiseValue(a2).then(a2 => remLinkFromDoc(link))); @@ -53,7 +52,7 @@ export class LinkManager { Doc.GetProto(a2)[DirectLinksSym].delete(link); Doc.GetProto(link)[DirectLinksSym].delete(link); } - }; + }); const watchUserLinks = (userLinks: List) => { const toRealField = (field: Field) => field instanceof ProxyField ? field.value() : field; // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields observe(userLinks, change => { @@ -75,8 +74,10 @@ export class LinkManager { }); } - public addLink(linkDoc: Doc) { - return Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + public addLink(linkDoc: Doc, checkExists = false) { + if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) { + Doc.AddDocToList(Doc.LinkDBDoc(), "data", linkDoc); + } } public deleteLink(linkDoc: Doc) { return Doc.RemoveDocFromList(Doc.LinkDBDoc(), "data", linkDoc); } public deleteAllLinksOnAnchor(anchor: Doc) { LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc)); } @@ -85,19 +86,6 @@ export class LinkManager { public getAllDirectLinks(anchor: Doc): Doc[] { return Array.from(Doc.GetProto(anchor)[DirectLinksSym]); } // finds all links that contain the given anchor - public getAllLinks(): Doc[] { return []; }//this.allLinks(); } - - // allLinks = computedFn(function allLinks(this: any): Doc[] { - // const linkData = Doc.LinkDBDoc().data; - // const lset = new Set(DocListCast(linkData)); - // SharingManager.Instance.users.forEach(user => DocListCast(user.linkDatabase?.data).forEach(doc => lset.add(doc))); - // LinkManager.Instance.allLinks().filter(link => { - // const a1 = Cast(link?.anchor1, Doc, null); - // const a2 = Cast(link?.anchor2, Doc, null); - // return link && ((a1?.author !== undefined && a2?.author !== undefined) || link.author === Doc.CurrentUserEmail) && (Doc.AreProtosEqual(anchor, a1) || Doc.AreProtosEqual(anchor, a2) || Doc.AreProtosEqual(link, anchor)); - // }); - // return Array.from(lset); - // }, true); relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { const lfield = Doc.LayoutFieldKey(anchor); diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index c3c3083be..f981f84cd 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -181,7 +181,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an if (batch) { batch.end(); } - onError?.(error); + onError?.(script + " " + error); return { success: false, error, result: errorVal }; } }; diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 6b94539c9..32ddc140c 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -101,7 +101,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt 400 || col.alpha() < 0.25) return Colors.DARK_GRAY; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index f39443ae2..a5d27f038 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -454,7 +454,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: if (completed) completed(set); else { if (isFreeformView && generatedDocuments.length > 1) { - addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)!); + addDocument(DocUtils.pileup(generatedDocuments, options.x!, options.y!)); } else { generatedDocuments.forEach(addDocument); } diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index b1f2750c3..1f4fcb2a5 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -368,8 +368,8 @@ export class MarqueeView extends React.Component this.props.removeDocument?.(d)); const newCollection = DocUtils.pileup(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2); - this.props.addDocument?.(newCollection!); - this.props.selectDocuments([newCollection!]); + this.props.addDocument?.(newCollection); + this.props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); } diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx index 90f64f163..0c434eae5 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx @@ -246,13 +246,13 @@ export class CollectionSchemaCell extends React.Component { } else { // check if the input is a number let inputIsNum = true; - for (let s of value) { - if (isNaN(parseInt(s)) && !(s == ".") && !(s == ",")) { + for (const s of value) { + if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) { inputIsNum = false; } } // check if the input is a boolean - let inputIsBool: boolean = value == "false" || value == "true"; + const inputIsBool: boolean = value === "false" || value === "true"; // what to do in the case if (!inputIsNum && !inputIsBool && !value.startsWith("=")) { // if it's not a number, it's a string, and should be processed as such @@ -263,12 +263,12 @@ export class CollectionSchemaCell extends React.Component { const vsqLength = valueSansQuotes.length; // get rid of outer quotes valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0, - valueSansQuotes.charAt(vsqLength - 1) == "\"" ? vsqLength - 1 : vsqLength); + valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength); } let inputAsString = '"'; // escape any quotes in the string for (const i of valueSansQuotes) { - if (i == '"') { + if (i === '"') { inputAsString += '\\"'; } else { inputAsString += i; @@ -278,7 +278,7 @@ export class CollectionSchemaCell extends React.Component { inputAsString += '"'; //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 + const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length; 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("=")) { @@ -286,18 +286,18 @@ export class CollectionSchemaCell extends React.Component { const inputscript = value.substring(value.startsWith("=") ? 1 : 0); // if commas are not stripped, the parser only considers the numbers after the last comma let inputSansCommas = ""; - for (let s of inputscript) { - if (!(s == ",")) { + for (const s of inputscript) { + if (!(s === ",")) { inputSansCommas += s; } } const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length + const changeMade = value.length !== value.length || value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); // handle booleans } else if (inputIsBool) { const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } }); - const changeMade = value.length !== value.length || value.length - 2 !== value.length + const changeMade = value.length !== value.length || value.length - 2 !== value.length; script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run)); } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 464a8ad05..7993af149 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -516,30 +516,30 @@ export namespace Doc { return alias; } - export async function makeClone(doc: Doc, cloneMap: Map, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise { + export async function makeClone(doc: Doc, cloneMap: Map, linkMap: Map, rtfs: { copy: Doc, key: string, field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise { if (Doc.IsBaseProto(doc)) return doc; if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; const copy = dontCreate ? asBranch ? (Cast(doc.branchMaster, Doc, null) || doc) : doc : new Doc(undefined, true); cloneMap.set(doc[Id], copy); - if (LinkManager.Instance.getAllLinks().includes(doc) && LinkManager.Instance.getAllLinks().indexOf(copy) === -1) LinkManager.Instance.addLink(copy); - const filter = [...exclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; - await Promise.all([...Object.keys(doc), "links"].map(async key => { + const fieldExclusions = (doc.type === DocumentType.TEXTANCHOR) ? exclusions.filter(ex => ex !== "annotationOn") : exclusions; + const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; + await Promise.all(Object.keys(doc).map(async key => { if (filter.includes(key)) return; const assignKey = (val: any) => !dontCreate && (copy[key] = val); const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === "links" && Doc.IsPrototype(doc) ? doc[key] : ProxyField.WithoutProxy(() => doc[key]); + const field = ProxyField.WithoutProxy(() => doc[key]); const copyObjectField = async (field: ObjectField) => { const list = Cast(doc[key], listSpec(Doc)); const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc); if (docs !== undefined && docs.length) { - const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, rtfs, exclusions, dontCreate, asBranch))); + const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch))); !dontCreate && assignKey(new List(clones)); } else if (doc[key] instanceof Doc) { - assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields + assignKey(key.includes("layout[") ? undefined : key.startsWith("layout") ? doc[key] as Doc : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded teplate fields } else { !dontCreate && assignKey(ObjectField.MakeCopy(field)); if (field instanceof RichTextField) { - if (field.Data.includes('"docid":') || field.Data.includes('"targetId":') || field.Data.includes('"linkId":')) { + if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) { rtfs.push({ copy, key, field }); } } @@ -547,14 +547,17 @@ export namespace Doc { }; if (key === "proto") { if (doc[key] instanceof Doc) { - assignKey(await Doc.makeClone(doc[key]!, cloneMap, rtfs, exclusions, dontCreate, asBranch)); + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); + } + } else if (key === "anchor1" || key === "anchor2") { + if (doc[key] instanceof Doc) { + assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch)); } } else { if (field instanceof RefField) { assignKey(field); } else if (cfield instanceof ComputedField) { !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript)); - (key === "links" && field instanceof ObjectField) && await copyObjectField(field); } else if (field instanceof ObjectField) { await copyObjectField(field); } else if (field instanceof Promise) { @@ -564,6 +567,10 @@ export namespace Doc { } } })); + for (const link of Array.from(doc[DirectLinksSym])) { + const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch); + linkMap.set(link, linkClone); + } if (!dontCreate) { Doc.SetInPlace(copy, "title", (asBranch ? "BRANCH: " : "CLONE: ") + doc.title, true); asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc); @@ -576,8 +583,10 @@ export namespace Doc { } export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false) { const cloneMap = new Map(); + const linkMap = new Map(); const rtfMap: { copy: Doc, key: string, field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ["context", "annotationOn", "cloneOf", "branches", "branchOf"], dontCreate, asBranch); + Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true)); rtfMap.map(({ copy, key, field }) => { const replacer = (match: any, attr: string, id: string, offset: any, string: any) => { const mapped = cloneMap.get(id); @@ -589,7 +598,7 @@ export namespace Doc { }; const regex = `(${Utils.prepend("/doc/")})([^"]*)`; const re = new RegExp(regex, "g"); - copy[key] = new RichTextField(field.Data.replace(/("docid":|"targetId":|"linkId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); + copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); return { clone: copy, map: cloneMap }; } -- cgit v1.2.3-70-g09d2 From b33e45f1f839b3c6eaf1076e605abacd1bc6883c Mon Sep 17 00:00:00 2001 From: geireann Date: Thu, 29 Jul 2021 15:35:39 -0400 Subject: lots of updates! --- src/client/util/SettingsManager.tsx | 79 ++++++-- src/client/views/AntimodeMenu.scss | 2 +- src/client/views/MainView.scss | 27 --- src/client/views/MainView.tsx | 61 ++---- src/client/views/_nodeModuleOverrides.scss | 52 ++++- .../views/collections/CollectionDockingView.scss | 98 +++++++--- .../views/collections/CollectionDockingView.tsx | 2 +- src/client/views/collections/TabDocView.scss | 59 +++++- src/client/views/collections/TabDocView.tsx | 107 +++++++---- src/client/views/global/globalCssVariables.scss | 4 +- src/client/views/global/globalEnums.tsx | 4 + src/client/views/topbar/TopBar.scss | 211 +++++++++++++++++++++ src/client/views/topbar/TopBar.tsx | 58 ++++++ src/fields/Doc.ts | 6 +- 14 files changed, 618 insertions(+), 152 deletions(-) create mode 100644 src/client/views/topbar/TopBar.scss create mode 100644 src/client/views/topbar/TopBar.tsx (limited to 'src/fields') diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 777394b05..3987497b8 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -18,6 +18,12 @@ const higflyout = require("@hig/flyout"); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; +export enum ColorScheme { + Dark = "Dark", + Light = "Light", + System = "Match System" +} + @observer export class SettingsManager extends React.Component<{}> { public static Instance: SettingsManager; @@ -32,7 +38,7 @@ export class SettingsManager extends React.Component<{}> { @observable activeTab = "Accounts"; @computed get backgroundColor() { return Doc.UserDoc().activeCollectionBackground; } - + @computed get colorScheme() { return Doc.UserDoc().colorScheme; } constructor(props: {}) { super(props); @@ -69,6 +75,28 @@ export class SettingsManager extends React.Component<{}> { else DocServer.Control.makeEditable(); }); + @undoBatch + @action + changeColorScheme = action((e: React.ChangeEvent) => { + const scheme: ColorScheme = (e.currentTarget as any).value; + switch (scheme) { + case ColorScheme.Light: + Doc.UserDoc().colorScheme = ColorScheme.Light; + addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "#d3d3d3 !important" }); + break; + case ColorScheme.Dark: + Doc.UserDoc().colorScheme = ColorScheme.Dark; + addStyleSheetRule(SettingsManager._settingsStyle, "lm_header", { background: "black !important" }); + break; + case ColorScheme.System: default: + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + Doc.UserDoc().colorScheme = e.matches ? ColorScheme.Dark : ColorScheme.Light; + }); + break; + } + }); + + @computed get colorsContent() { const colorBox = (func: (color: ColorState) => void) => {
; - const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]; - const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; + const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System]; return
@@ -102,14 +129,11 @@ export class SettingsManager extends React.Component<{}> {
Border/Header Color
{userColorFlyout}
-
-
Default Font
-
- - + {colorSchemes.map(scheme => )}
@@ -132,6 +156,16 @@ export class SettingsManager extends React.Component<{}> { checked={BoolCast(Doc.UserDoc()._raiseWhenDragged)} />
Raise on drag
+
+ Doc.UserDoc()._showLabel = !Doc.UserDoc()._showLabel} + checked={BoolCast(Doc.UserDoc()._showLabel)} /> +
Show tool button labels
+
+
+ Doc.UserDoc()._showMenuLabel = !Doc.UserDoc()._showMenuLabel} + checked={BoolCast(Doc.UserDoc()._showMenuLabel)} /> +
Show menu button labels
+
; } @@ -149,6 +183,27 @@ export class SettingsManager extends React.Component<{}> { ; } + @computed get textContent() { + + const fontFamilies = ["Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text", "Roboto"]; + const fontSizes = ["7px", "8px", "9px", "10px", "12px", "14px", "16px", "18px", "20px", "24px", "32px", "48px", "72px"]; + + return ( +
+
+
Default Font
+
+ + +
+
+
); + } + @action changeVal = (e: React.ChangeEvent, pass: string) => { const value = (e.target as any).value; @@ -228,7 +283,7 @@ export class SettingsManager extends React.Component<{}> { // { title: "Accounts", ele: this.accountsContent }, { title: "Preferences", ele: this.preferencesContent }]; const tabs = [{ title: "Accounts", ele: this.accountsContent }, { title: "Modes", ele: this.modesContent }, - { title: "Appearance", ele: this.appearanceContent }]; + { title: "Appearance", ele: this.appearanceContent }, { title: "Text", ele: this.textContent }]; return
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss index 2bac03af4..b509f9f54 100644 --- a/src/client/views/AntimodeMenu.scss +++ b/src/client/views/AntimodeMenu.scss @@ -6,7 +6,7 @@ z-index: 10001; height: $antimodemenu-height; background: $dark-gray; - box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); + // box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25); // border-radius: 0px 6px 6px 6px; z-index: 1001; display: flex; diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index 07ca0257c..2069986ad 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -419,31 +419,4 @@ display: block; width: 500px; height: 1000px; -} - -.lm_drag_tab { - padding: 0; - width: 15px !important; - height: 15px !important; - position: relative !important; - display: inline-flex !important; - align-items: center; - top: 0 !important; - right: unset !important; - left: 0 !important; -} -.lm_close_tab { - padding: 0; - width: 15px !important; - height: 15px !important; - position: relative !important; - display: inline-flex !important; - align-items: center; - top: 0 !important; - right: unset !important; - left: 0 !important; -} -.lm_tab, .lm_tab_active { - display: flex !important; - padding-right: 0 !important; } \ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index f34851b00..7d6bfbd40 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -63,6 +63,7 @@ import { PreviewCursor } from './PreviewCursor'; import { PropertiesView } from './PropertiesView'; import { SearchBox } from './search/SearchBox'; import { DefaultStyleProvider, DashboardStyleProvider, StyleProp } from './StyleProvider'; +import { TopBar } from './topbar/TopBar'; const _global = (window /* browser */ || global /* node */) as any; @observer @@ -78,7 +79,7 @@ export class MainView extends React.Component { @observable private _sidebarContent: any = this.userDoc?.sidebar; @observable private _flyoutWidth: number = 0; - @computed private get topOffset() { return (CollectionMenu.Instance?.Pinned ? 35 : 0) + Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } + @computed private get topOffset() { return Number(SEARCH_PANEL_HEIGHT.replace("px", "")); } //TODO remove @computed private get leftOffset() { return this.menuPanelWidth() - 2; } @computed private get userDoc() { return Doc.UserDoc(); } @computed private get darkScheme() { return BoolCast(CurrentUserUtils.ActiveDashboard?.darkScheme); } @@ -180,8 +181,8 @@ export class MainView extends React.Component { const targClass = targets[0].className.toString(); if (SearchBox.Instance._searchbarOpen || SearchBox.Instance.open) { const check = targets.some((thing) => - (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || - thing.className === "collectionSchema-header-menuOptions")); + (thing.className === "collectionSchemaView-searchContainer" || (thing as any)?.dataset.icon === "filter" || + thing.className === "collectionSchema-header-menuOptions")); !check && SearchBox.Instance.resetSearch(true); } !targClass.includes("contextMenu") && ContextMenu.Instance.closeMenu(); @@ -242,8 +243,9 @@ export class MainView extends React.Component { } getPWidth = () => this._panelWidth - this.propertiesWidth(); - getPHeight = () => this._panelHeight; + getPHeight = () => this._panelHeight - (CollectionMenu.Instance?.Pinned ? 35 : 0); getContentsHeight = () => this._panelHeight; + getMenuPanelHeight = () => this._panelHeight + (CollectionMenu.Instance?.Pinned ? 35 : 0); @computed get mainDocView() { return { e.stopPropagation(); e.preventDefault(); }} + // style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, width: `calc(100% - ${this._flyoutWidth + this.propertiesWidth()}px)` }}> + // FIXME update with property panel width style={{ minWidth: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)`, transform: LightboxView.LightboxDoc ? "scale(0.0001)" : undefined, - width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` + //TODO:glr width: `calc(100% - ${this._flyoutWidth + this.menuPanelWidth() + this.propertiesWidth()}px)` }}> {!this.mainContainer ? (null) : this.mainDocView}
; @@ -358,7 +362,7 @@ export class MainView extends React.Component { removeDocument={returnFalse} ScreenToLocalTransform={this.sidebarScreenToLocal} PanelWidth={this.menuPanelWidth} - PanelHeight={this.getContentsHeight} + PanelHeight={this.getMenuPanelHeight} renderDepth={0} docViewPath={returnEmptyDoclist} focus={DocUtils.DefaultFocus} @@ -405,16 +409,19 @@ export class MainView extends React.Component { {this.menuPanel}
{this.flyout} -
+
+
+ - {this.dockingContent} + {this.dockingContent} -
- +
+ +
+ {this.propertiesWidth() < 10 ? (null) : }
- {this.propertiesWidth() < 10 ? (null) : }
; } @@ -525,35 +532,8 @@ export class MainView extends React.Component { @computed get search() { TraceMobx(); - return
- + return
+
; } @@ -605,7 +585,6 @@ export class MainView extends React.Component { {this.search} - {LinkDescriptionPopup.descriptionPopup ? : null} {DocumentLinksButton.LinkEditorDocView ? : (null)} {LinkDocPreview.LinkInfo ? : (null)} diff --git a/src/client/views/_nodeModuleOverrides.scss b/src/client/views/_nodeModuleOverrides.scss index 56346b68b..cb59489c0 100644 --- a/src/client/views/_nodeModuleOverrides.scss +++ b/src/client/views/_nodeModuleOverrides.scss @@ -1,8 +1,49 @@ +@import "./global/globalCssVariables"; // this file is for overriding all the css from installed node modules // goldenlayout stuff div .lm_header { background: $dark-gray; + overflow: hidden; +} + +/* Width */ +.lm_header::-webkit-scrollbar { + -webkit-appearance: none; + display: none; +} + +/* Width */ +.lm_header:hover::-webkit-scrollbar { + -webkit-appearance: none; + display: block; + height: 0px; +} + +/* Track */ +.lm_header:hover::-webkit-scrollbar-track { + -webkit-appearance: none; + display: none; +} + +/* Handle */ +.lm_header:hover::-webkit-scrollbar-thumb { + -webkit-appearance: none; + background: $dark-gray; +} + +/* Handle on hover */ +.lm_header:hover::-webkit-scrollbar-thumb:hover { + -webkit-appearance: none; + background: $dark-gray; +} + +.lm_tabs { + display: flex; + position: absolute; + width: calc(100% - 60px); + overflow: scroll; + background: $dark-gray; } .lm_tab { @@ -15,7 +56,16 @@ div .lm_header { } .lm_header .lm_controls { - right: 1em !important; + align-items: center; + position: absolute; + background-color: #000000; + border-radius: 5px; + display: flex; + top: 2px; + justify-content: space-evenly; + right: 2px; + height: 18px; + width: 65px; } // @TODO the ril__navgiation buttons in the img gallery are a lil messed up but I can't figure out diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index a054f0ae1..b8180fe24 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,40 +1,46 @@ -@import "../../views/global/globalCssVariables.scss"; +@import "../global/globalCssVariables.scss"; .lm_title { - margin-top: 3px; - border-radius: 5px; - border: solid 0px dimgray; - border-width: 2px 2px 0px; - height: 20px; - transform: translate(0px, -3px); + -webkit-appearance: none; + display: inline-block; + align-self: center; + align-items: center; + height: 100%; + overflow: hidden; + text-overflow: ellipsis; + background: transparent; + border: solid 0px transparent; cursor: grab; + color: $black; } .lm_title.focus-visible { + -webkit-appearance: none; cursor: text; } .lm_title_wrap { overflow: hidden; - height: 19px; - margin-top: -2px; - display: inline-block; + align-items: center; + align-self: center; + background: transparent; + width: max-content; + height: 100%; + display: flex; } .lm_active .lm_title { - border: solid 1px lightgray; -} - -.lm_header .lm_tab .lm_close_tab { - position: absolute; - text-align: center; + -webkit-appearance: none; + // font-weight: 700; } .lm_header .lm_tab { - padding-right: 20px; - margin-top: -1px; - border-bottom: 1px black; + padding: 0px; + opacity: 0.7; + box-shadow: none; + height: 19px; + // border-bottom: 1px black; .collectionDockingView-gear { display: none; @@ -42,9 +48,13 @@ } .lm_header .lm_tab.lm_active { - padding-right: 20px; - margin-top: 1px; - border-bottom: unset; + padding: 0; + opacity: 1; + margin: 0; + box-shadow: none; + height: 22px; + margin-right: 2px; + // border-bottom: unset; .collectionDockingView-gear { display: inline-block; @@ -55,6 +65,41 @@ display: inline; } +.lm_drag_tab { + padding: 0; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} + +.lm_close_tab { + padding: 0; + align-self: center; + margin-right: 5px; + background-color: black; + border-radius: 3px; + opacity: 1 !important; + width: 15px !important; + height: 15px !important; + position: relative !important; + display: inline-flex !important; + align-items: center; + top: 0 !important; + right: unset !important; + left: 0 !important; +} + +.lm_tab, +.lm_tab_active { + display: flex !important; + padding-right: 0 !important; +} + .collectiondockingview-container { width: 100%; height: 100%; @@ -82,16 +127,17 @@ } .lm_content { - background: white; + background: $white; } .lm_controls>li { - opacity: 0.6; - transform: scale(1.2); + opacity: 1; + transform: scale(1); } .lm_controls .lm_popout { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAAAUCAAAAABHICnvAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QAAKqNIzIAAAAHdElNRQfkCBsXMgbrEyzaAAAAT0lEQVQY02NgIAcIu8tgEW3/u4IDQ5B14/8LQlhFhckVFfCJjIyIOfP/QWpEZGSQJFS05s9fIPj3/z+YmseCTxS7CZS7DI+PsYcOjpAkDAA6H0KZxzDzlgAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyMC0wOC0yN1QyMzo1MDowNi0wNDowMDvgVpQAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjAtMDgtMjdUMjM6NTA6MDYtMDQ6MDBKve4oAAAAAElFTkSuQmCC) + transform: rotate(45deg); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAQUlEQVR4nHXOQQ4AMAgCQeT/f6aXpsGK3jSTuCVJAAr7iBdoAwCKd0nwfaAdHbYERw5b44+E8JoBjEYGMBq5gAYP3usUDu2IvoUAAAAASUVORK5CYII=); } .lm_maximised .lm_controls .lm_maximise { diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 388f9a909..a8471f8e2 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -445,4 +445,4 @@ Scripting.addGlobal(function openInLightbox(doc: any) { LightboxView.AddDocTab(d "opens up document in a lightbox", "(doc: any)"); Scripting.addGlobal(function openOnRight(doc: any) { return CollectionDockingView.AddSplit(doc, "right"); }, "opens up document in tab on right side of the screen", "(doc: any)"); -Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); +Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.ReplaceTab(doc, "right", undefined, shiftKey); }); \ No newline at end of file diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index 9acbc4f85..a963f1cb9 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -1,19 +1,62 @@ input.lm_title:focus, -input.lm_title -{ +input.lm_title { max-width: unset !important; + outline: none; transition-delay: unset; - width: 100%; + width: max-content; cursor: text; } + input.lm_title { transition-delay: 0.35s; - width: 100px; + width: max-content; cursor: pointer; } -.tabDocView-drag { - margin: auto; + +.lm_iconWrap { + display: flex; + color: black; + width: 15px; + height: 15px; + align-items: center; + align-self: center; + justify-content: center; + margin: 3px; + border-radius: 20%; + + .moreInfoDot { + background-color: white; + border-radius: 100%; + width: 3px; + height: 3px; + margin: 0.5px; + } +} + +.ffMenu { + display: grid; + grid-auto-rows: 35px; + grid-auto-columns: auto auto auto auto auto; + right: 10px; + bottom: 50px; + position: absolute; + min-height: 35px; + height: max-content; + border: solid 2px black; + border-radius: 5px; + background-color: #bddbe6; + width: max-content; + min-width: 35px; + + .ffMenuButton { + display: flex; + width: 35px; + height: 35px; + align-items: center; + justify-content: center; + } } + .miniMap-hidden, .miniMap { position: absolute; @@ -37,6 +80,7 @@ input.lm_title { } } } + .miniMap-hidden { position: absolute; bottom: 0; @@ -46,7 +90,8 @@ input.lm_title { transform: translate(20px, 20px) rotate(45deg); border-radius: 30px; padding: 2px; - > svg { + + >svg { margin-top: 3px; transform: translate(0px, 7px); } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 7e2f7811e..0e67bebd8 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -1,3 +1,4 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import 'golden-layout/src/css/goldenlayout-base.css'; @@ -9,9 +10,9 @@ import * as ReactDOM from 'react-dom'; import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; -import { Cast, NumCast, StrCast, BoolCast } from "../../../fields/Types"; +import { BoolCast, Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; +import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -24,15 +25,15 @@ import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { LightboxView } from '../LightboxView'; import { DocFocusOptions, DocumentView, DocumentViewProps } from "../nodes/DocumentView"; -import { FieldViewProps } from '../nodes/FieldView'; -import { PinProps, PresBox, PresMovement } from '../nodes/PresBox'; +import { PresBox, PinProps, PresMovement } from '../nodes/PresBox'; import { DefaultLayerProvider, DefaultStyleProvider, StyleLayers, StyleProp } from '../StyleProvider'; import { CollectionDockingView } from './CollectionDockingView'; import { CollectionDockingViewMenu } from './CollectionDockingViewMenu'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -import { CollectionViewType, CollectionView } from './CollectionView'; +import { CollectionView, CollectionViewType } from './CollectionView'; import "./TabDocView.scss"; import React = require("react"); +import Color = require('color'); const _global = (window /* browser */ || global /* node */) as any; interface TabDocViewProps { @@ -52,6 +53,14 @@ export class TabDocView extends React.Component { @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); } + @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? "black" : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); } + // @computed get renderBounds() { + // const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + // const xbounds = bounds[2] - bounds[0]; + // const ybounds = bounds[3] - bounds[1]; + // const dim = Math.max(xbounds, ybounds); + // return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + // } get stack() { return (this.props as any).glContainer.parent.parent; } get tab() { return (this.props as any).glContainer.tab; } @@ -65,15 +74,25 @@ export class TabDocView extends React.Component { tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); tab.DashDoc = doc; CollectionDockingView.Instance.tabMap.add(tab); - + const iconType: IconProp = Doc.toIcon(doc); // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. const titleEle = tab.titleElement[0]; + const iconWrap = document.createElement("div"); + const closeWrap = document.createElement("div"); + + titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; titleEle.onchange = undoBatch(action((e: any) => { titleEle.size = e.currentTarget.value.length + 3; Doc.GetProto(doc).title = e.currentTarget.value; })); + + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; + + if (tab.element[0].children[1].children.length === 1) { const toggle = document.createElement("div"); toggle.style.width = "10px"; @@ -83,18 +102,42 @@ export class TabDocView extends React.Component { toggle.style.borderTopRightRadius = "7px"; toggle.style.position = "relative"; toggle.style.display = "inline-block"; - toggle.style.background = "gray"; - toggle.style.borderLeft = "solid 1px black"; + toggle.style.background = "transparent"; toggle.onclick = (e: MouseEvent) => { if (tab.contentItem === tab.header.parent.getActiveContentItem()) { tab.DashDoc.activeLayer = tab.DashDoc.activeLayer ? undefined : StyleLayers.Background; } }; - tab.element[0].style.borderTopRightRadius = "8px"; - tab.element[0].children[1].appendChild(toggle); - tab._disposers.layerDisposer = reaction(() => - ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), - ({ layer, color }) => toggle.style.background = !layer ? color : "dimgrey", { fireImmediately: true }); + iconWrap.className = "lm_iconWrap"; + iconWrap.id = "lm_iconWrap"; + closeWrap.className = "lm_iconWrap"; + closeWrap.id = "lm_closeWrap"; + closeWrap.onclick = (e: MouseEvent) => { + tab.header.parent.contentItem.remove(); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); + }; + const docIcon = ; + const closeIcon = ; + ReactDOM.render(docIcon, iconWrap); + ReactDOM.render(closeIcon, closeWrap); + // tab.element[0].append(closeWrap); + tab.element[0].prepend(iconWrap); + tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + ({ layer, color }) => { + const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color + titleEle.style.color = textColor; + titleEle.style.backgroundColor = "transparent"; + iconWrap.style.color = textColor; + closeWrap.style.color = textColor; + moreInfoDrag.style.backgroundColor = textColor; + tab.element[0].style.background = !layer ? color : "dimgrey"; + }, { fireImmediately: true }); + // TODO:glr fix + // tab.element[0].style.borderTopRightRadius = "8px"; + // tab.element[0].children[1].appendChild(toggle); + // tab._disposers.layerDisposer = reaction(() => + // ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + // ({ layer, color }) => toggle.style.background = !layer ? color : "dimgrey", { fireImmediately: true }); } // shifts the focus to this tab when another tab is dragged over it tab.element[0].onmouseenter = (e: MouseEvent) => { @@ -103,13 +146,11 @@ export class TabDocView extends React.Component { tab.setActive(true); } }; - const dragBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([dragHdl], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); - }; + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected titleEle.onpointerdown = action((e: any) => { - if (e.target.className !== "lm_close_tab") { + if (e.target.className !== "lm_iconWrap") { if (this.view) SelectionManager.SelectView(this.view, false); else this._activated = true; if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); @@ -123,25 +164,25 @@ export class TabDocView extends React.Component { const toggle = tab.element[0].children[1].children[0] as HTMLInputElement; selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch"); - toggle.style.fontWeight = selected ? "bold" : ""; - toggle.style.textTransform = selected ? "uppercase" : ""; + // toggle.style.fontWeight = selected ? "bold" : ""; + // toggle.style.textTransform = selected ? "uppercase" : ""; })); //attach the selection doc buttons menu to the drag handle const stack = tab.contentItem.parent; - const dragHdl = document.createElement("div"); - dragHdl.className = "lm_drag_tab"; + const moreInfoDrag = document.createElement("div"); + moreInfoDrag.className = "lm_iconWrap"; tab._disposers.buttonDisposer = reaction(() => this.view, view => - view && [ReactDOM.render( [view]} Stack={stack} />, dragHdl), tab._disposers.buttonDisposer?.()], + view && [ReactDOM.render( [view]} Stack={stack} />, moreInfoDrag), tab._disposers.buttonDisposer?.()], { fireImmediately: true }); - tab.reactComponents = [dragHdl]; - tab.closeElement.before(dragHdl); + // tab.reactComponents = [moreInfoDrag]; + // tab.element[0].children[3].before(moreInfoDrag); // highlight the tab when the tab document is brushed in any part of the UI tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { titleEle.value = title; - titleEle.style.padding = degree ? 0 : 2; - titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + // titleEle.style.padding = degree ? 0 : 2; + // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; }, { fireImmediately: true }); // clean up the tab when it is closed @@ -221,9 +262,9 @@ export class TabDocView extends React.Component { })).observe(this.props.glContainer._element[0]); this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(undefined); - this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), - ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), - { fireImmediately: true }); + // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), + // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), + // { fireImmediately: true }); } componentWillUnmount() { @@ -243,10 +284,10 @@ export class TabDocView extends React.Component { } // adds a tab to the layout based on the locaiton parameter which can be: - // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, + // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right - // replace[:{left,right,top,bottom,}] - e.g., "replace" will replace the current stack contents, - // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // replace[:{left,right,top,bottom,}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack addDocTab = (doc: Doc, location: string) => { @@ -460,4 +501,4 @@ export class TabMinimapView extends React.Component {
; } -} \ No newline at end of file +} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index ead5e166e..a8d4235bd 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -21,8 +21,6 @@ $large-padding: 32px; //icon sizes $icon-size: 28px; -$antimodemenu-height: 36px; - // fonts $sans-serif: "Noto Sans", sans-serif; $large-header: 16px; @@ -33,6 +31,8 @@ $small-text: 9px; // misc values $border-radius: 0.3em; $search-thumnail-size: 130; +$topbar-height: 32px; +$antimodemenu-height: 36px; // dragged items $contextMenu-zindex: 100000; // context menu shows up over everything diff --git a/src/client/views/global/globalEnums.tsx b/src/client/views/global/globalEnums.tsx index 1e0381c33..2aeb8e338 100644 --- a/src/client/views/global/globalEnums.tsx +++ b/src/client/views/global/globalEnums.tsx @@ -31,4 +31,8 @@ export enum Padding { export enum IconSizes { ICON_SIZE = "28px", +} + +export enum Borders { + STANDARD = "solid 1px #9F9F9F" } \ No newline at end of file diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss new file mode 100644 index 000000000..324b96dbd --- /dev/null +++ b/src/client/views/topbar/TopBar.scss @@ -0,0 +1,211 @@ +@import "../global/globalCssVariables"; + +.topbar-container { + display: flex; + flex-direction: column; + width: 100%; + position: relative; + font-size: 10px; + line-height: 1; + overflow-y: auto; + overflow-x: visible; + background: $dark-gray; + overflow: visible; + z-index: 1000; + + .topbar-bar { + height: $topbar-height; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; + align-items: center; + background-color: $dark-gray; + + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; + align-items: center; + + .topbar-lozenge-dashboard { + display: flex; + + .topbar-dashboards { + display: none; + } + + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; + + &:hover { + cursor: pointer; + } + } + } + + .topbar-lozenge-dashboard:hover { + .topbar-dashboards { + display: inline-flex; + } + } + } + + .topBar-icon { + color: black; + cursor: pointer; + font-size: 15px; + height: 30; + width: 30; + display: flex; + justify-content: center; + align-items: center; + margin-right: 5px; + justify-self: center; + align-self: center; + border-radius: 100%; + transition: linear 0.1s; + background-color: #92adb900; + } + + .topBar-icon:hover { + background-color: rgba(0, 0, 0, 0.15); + } + + .topbar-right { + grid-column: 3; + position: relative; + display: flex; + justify-content: flex-end; + + .topbar-lozenge-user, + .topbar-lozenge { + height: 23; + font-size: 12; + color: black; + font-family: 'Roboto'; + font-weight: 400; + padding: 4px; + align-self: center; + margin-right: 7px; + display: flex; + align-items: center; + border: black 1px solid; + + .topbar-logoff { + border-radius: 3px; + background: olivedrab; + color: white; + display: none; + margin-left: 5px; + padding: 1px 2px 1px 2px; + cursor: pointer; + } + + .topbar-logoff { + background: red; + } + + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; + + &:hover { + cursor: pointer; + } + } + } + + .topbar-lozenge-user:hover { + .topbar-logoff { + display: inline-block; + } + } + + } + + .topbar-left { + grid-column: 1; + color: black; + font-family: 'Roboto'; + position: relative; + display: flex; + width: 450; + } + + .topbar-barChild { + + &.topbar-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px + } + + &.topbar-input { + margin:5px; + border-radius:20px; + border:$dark-gray; + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + /* align-self: stretch; */ + outline: none; + + &:focus { + width: 500px; + outline: none; + } + } + + &.topbar-filter { + align-self: stretch; + + button { + transform: none; + + &:hover { + transform: none; + } + } + } + + &.topbar-submit { + margin-left: 2px; + margin-right: 2px + } + + &.topbar-close { + color: $white; + max-height: $topbar-height; + } + } + } +} + +.topbar-results { + display: flex; + flex-direction: column; + top: 300px; + display: flex; + flex-direction: column; + height: 100%; + overflow: visible; + + .no-result { + width: 500px; + background: $light-gray; + padding: 10px; + height: 50px; + text-transform: uppercase; + text-align: left; + font-weight: bold; + } +} \ No newline at end of file diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx new file mode 100644 index 000000000..79239d4ea --- /dev/null +++ b/src/client/views/topbar/TopBar.tsx @@ -0,0 +1,58 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import * as React from 'react'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { StrCast } from '../../../fields/Types'; +import { Utils } from '../../../Utils'; +import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { ColorScheme, SettingsManager } from "../../util/SettingsManager"; +import { undoBatch } from "../../util/UndoManager"; +import "./TopBar.scss"; +import { Colors, Borders } from "../global/globalEnums"; + +export const TopBar = () => { + + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + return ( +
+
+
+
+ +
+
CurrentUserUtils.createNewDashboard(Doc.UserDoc()))} + style={{ color: Doc.UserDoc().colorScheme === ColorScheme.Dark ? "white" : "black" }}> + +
+
CurrentUserUtils.snapshotDashboard(Doc.UserDoc()))} + style={{ color: Doc.UserDoc().colorScheme === ColorScheme.Dark ? "white" : "black" }}> + +
+
+
+
+
+
+ +
+
SettingsManager.Instance.open()} + style={{ color: Doc.UserDoc().colorScheme === ColorScheme.Dark ? "white" : "black" }}> + +
+
+ {`${Doc.CurrentUserEmail}`} +
window.location.assign(Utils.prepend("/logout"))}> + Logoff +
+
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 464a8ad05..ee8d36f09 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -23,6 +23,7 @@ import { Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types"; import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLField"; import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); +import { IconProp } from "@fortawesome/fontawesome-svg-core"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -1184,7 +1185,10 @@ export namespace Doc { case DocumentType.IMG: return "image"; case DocumentType.COMPARISON: return "columns"; case DocumentType.RTF: return "sticky-note"; - case DocumentType.COL: return !doc?.isFolder ? "folder" + (isOpen ? "-open" : "") : "chevron-" + (isOpen ? "down" : "right"); + case DocumentType.COL: + const folder: IconProp = isOpen ? "folder-open" : "folder"; + const chevron: IconProp = isOpen ? "chevron-down" : "chevron-right" + return !doc?.isFolder ? folder : chevron; case DocumentType.WEB: return "globe-asia"; case DocumentType.SCREENSHOT: return "photo-video"; case DocumentType.WEBCAM: return "video"; -- cgit v1.2.3-70-g09d2 From b6b2057cf28e8c0d3c22b9056074fe5155602d0a Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 29 Jul 2021 20:15:46 -0400 Subject: converted HTMLANCHOR and TEXTANCHOR to MARKER --- src/client/documents/DocumentTypes.ts | 4 +--- src/client/documents/Documents.ts | 14 +++++--------- src/client/util/DocumentManager.ts | 4 ++-- src/client/util/SelectionManager.ts | 2 +- src/client/views/StyleProvider.tsx | 2 +- src/client/views/collections/CollectionTimeView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- src/client/views/nodes/WebBox.tsx | 2 +- src/fields/Doc.ts | 2 +- 9 files changed, 14 insertions(+), 20 deletions(-) (limited to 'src/fields') diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8565784b4..dba7ff907 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -18,7 +18,7 @@ export enum DocumentType { LABEL = "label", // simple text label BUTTON = "button", // onClick button WEBCAM = "webcam", // webcam - HTMLANCHOR = "htmlanchor", // text selection anchor in PDF/Web + MARKER = "marker", // generic marker document not intended to be viewed independently of its context (e.g., for text selections in PDF/Web/RTF) DATE = "date", // calendar view of a date SCRIPTING = "script", // script editor EQUATION = "equation", // equation editor @@ -40,6 +40,4 @@ export enum DocumentType { LINKDB = "linkdb", // database of links ??? why do we have this SCRIPTDB = "scriptdb", // database of scripts GROUPDB = "groupdb", // database of groups - - TEXTANCHOR = "textanchor" // selection of text in a text box } \ No newline at end of file diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f1db3e32c..e863b4198 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -427,7 +427,7 @@ export namespace Docs { [DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey } }], - [DocumentType.HTMLANCHOR, { + [DocumentType.MARKER, { layout: { view: CollectionView, dataField: defaultDataKey }, options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } }], @@ -452,10 +452,6 @@ export namespace Docs { layout: { view: EmptyBox, dataField: defaultDataKey }, options: { links: ComputedField.MakeFunction("links(self)") as any } }], - [DocumentType.TEXTANCHOR, { - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { targetDropAction: "move", links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } - }] ]); const suffix = "Proto"; @@ -670,9 +666,9 @@ export namespace Docs { viewProps["acl-Override"] = "None"; viewProps["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Add; const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewProps, true, true); - ![DocumentType.LINK, DocumentType.TEXTANCHOR, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); + ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); - !Doc.IsSystem(dataDoc) && ![DocumentType.HTMLANCHOR, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR, DocumentType.TEXTANCHOR].includes(proto.type as any) && + !Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) && !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); return viewDoc; @@ -802,7 +798,7 @@ export namespace Docs { } export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.TEXTANCHOR), undefined, options, id); + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id); } export function FreeformDocument(documents: Array, options: DocumentOptions, id?: string) { @@ -811,7 +807,7 @@ export namespace Docs { return inst; } export function HTMLAnchorDocument(documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.HTMLANCHOR), new List(documents), options, id); + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); } export function PileDocument(documents: Array, options: DocumentOptions, id?: string) { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 304215a8f..5b092258a 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -28,7 +28,7 @@ export class DocumentManager { DocListCast(view.rootDoc.links).forEach(link => { const whichOtherAnchor = view.props.LayoutTemplateString?.includes("anchor2") ? "anchor1" : "anchor2"; const otherDoc = link && (link[whichOtherAnchor] as Doc); - const otherDocAnno = otherDoc?.type === DocumentType.TEXTANCHOR ? otherDoc.annotationOn as Doc : undefined; + const otherDocAnno = DocumentType.MARKER === otherDoc?.type ? otherDoc.annotationOn as Doc : undefined; otherDoc && DocumentManager.Instance.DocumentViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, otherDoc) || Doc.AreProtosEqual(dv.rootDoc, otherDocAnno)). forEach(otherView => { if (otherView.rootDoc.type !== DocumentType.LINK || otherView.props.LayoutTemplateString !== view.props.LayoutTemplateString) { @@ -162,7 +162,7 @@ export class DocumentManager { const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext : undefined; const targetDocContext = contextDoc || annotatedDoc; const targetDocContextView = targetDocContext && getFirstDocView(targetDocContext); - const focusView = !docView && targetDoc.type === DocumentType.TEXTANCHOR && annoContainerView ? annoContainerView : docView; + const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView; if (!docView && annoContainerView && !focusView) { annoContainerView.focus(targetDoc); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below } diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 7aeb19391..dbcc49f3d 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -23,7 +23,7 @@ export namespace SelectionManager { @action SelectView(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.HTMLANCHOR) { + if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) { if (!ctrlPressed) { this.DeselectAll(); } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index dc6ac0366..c9e532745 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -174,7 +174,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt doc) { @observable _focusRangeFilters: Opt; getAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ + const anchor = Docs.Create.HTMLAnchorDocument({ title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, annotationOn: this.rootDoc }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 35da09af6..8ef0057bd 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1487,7 +1487,7 @@ export class CollectionFreeFormView extends CollectionSubView e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.props.Document.type === DocumentType.HTMLANCHOR ? "none" : // bcz: ugh.. this is here to prevent htmlanchor's, which render as freeform views, from grabbing events -- need a better approach. + pointerEvents: this.props.Document.type === DocumentType.MARKER ? "none" : // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach. this.backgroundEvents ? "all" : this.props.pointerEvents as any, transform: `scale(${this.contentScaling || 1})`, width: `${100 / (this.contentScaling || 1)}%`, diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index abc3a7d7d..f5b1f96f2 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -163,7 +163,7 @@ export class WebBox extends ViewBoxAnnotatableComponent ex !== "annotationOn") : exclusions; + const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== "annotationOn") : exclusions; const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec("string"), [])]; await Promise.all(Object.keys(doc).map(async key => { if (filter.includes(key)) return; -- cgit v1.2.3-70-g09d2 From 0546ecf205b7d2b76f341a7157beebf95fb888a8 Mon Sep 17 00:00:00 2001 From: bobzel Date: Sun, 1 Aug 2021 22:43:46 -0400 Subject: made url server references relative. --- src/Utils.ts | 1 - .../apis/google_docs/GooglePhotosClientUtils.ts | 2 +- src/client/documents/Documents.ts | 90 ++-------------------- src/client/util/HypothesisUtils.ts | 2 +- src/client/views/collections/CollectionSubView.tsx | 2 +- .../views/collections/CollectionTimeView.tsx | 2 +- src/client/views/nodes/AudioBox.tsx | 2 +- src/client/views/nodes/DocumentLinksButton.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 6 +- src/client/views/nodes/FieldView.tsx | 6 +- src/client/views/nodes/ImageBox.tsx | 2 +- src/client/views/nodes/LinkDocPreview.tsx | 4 +- src/client/views/nodes/PDFBox.tsx | 24 ------ src/client/views/nodes/ScreenshotBox.tsx | 4 +- src/client/views/nodes/VideoBox.tsx | 8 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 6 +- .../views/nodes/formattedText/RichTextMenu.tsx | 8 +- src/client/views/pdf/AnchorMenu.tsx | 1 - src/fields/Doc.ts | 12 ++- src/fields/URLField.ts | 15 +++- src/mobile/ImageUpload.tsx | 2 +- src/server/server_Initialization.ts | 3 +- 22 files changed, 58 insertions(+), 146 deletions(-) (limited to 'src/fields') diff --git a/src/Utils.ts b/src/Utils.ts index d87c3cc6b..194c38a6f 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -67,7 +67,6 @@ export namespace Utils { export function prepend(extension: string): string { return window.location.origin + extension; } - export function fileUrl(filename: string): string { return prepend(`/files/${filename}`); } diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 899e65a16..ff9460b62 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -285,7 +285,7 @@ export namespace GooglePhotos { const photos = await endpoint(); const albumId = StrCast(collection.albumId); if (albumId && albumId.length) { - const enrichment = new photos.TextEnrichment(content || Utils.prepend("/doc/" + collection[Id])); + const enrichment = new photos.TextEnrichment(content || Doc.globalServerPath(collection)); const position = new photos.AlbumPosition(photos.AlbumPosition.POSITIONS.FIRST_IN_ALBUM); const enrichmentItem = await photos.albums.addEnrichment(albumId, enrichment, position); if (enrichmentItem) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e863b4198..ac52b0acf 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -549,84 +549,6 @@ export namespace Docs { */ export namespace Create { - /** - * Synchronously returns a collection into which - * the device documents will be put. This is initially empty, - * but gets populated by updates from the web socket. When everything is over, - * this function cleans up after itself. - * s - * Look at Websocket.ts for the server-side counterpart to this - * function. - */ - export function Buxton() { - let responded = false; - const loading = new Doc; - loading.title = "Please wait for the import script..."; - const parent = TreeDocument([loading], { - title: "The Buxton Collection", - _width: 400, - _height: 400 - }); - const parentProto = Doc.GetProto(parent); - const { _socket } = DocServer; - - // just in case, clean up - _socket.off(MessageStore.BuxtonDocumentResult.Message); - _socket.off(MessageStore.BuxtonImportComplete.Message); - - // this is where the client handles the receipt of a new valid parsed document - Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, invalid: errors }) => { - if (!responded) { - responded = true; - parentProto.data = new List(); - } - if (device) { - const { title, __images, additionalMedia } = device; - delete device.__images; - delete device.additionalMedia; - const { ImageDocument, StackingDocument } = Docs.Create; - const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight })); - const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => { - const imageDoc = ImageDocument(url, { - title: `image${i}.${extname(url)}`, - _nativeWidth: nativeWidth, - _nativeHeight: nativeHeight - }); - const media = additionalMedia[i]; - if (media) { - for (const key of Object.keys(media)) { - imageDoc[`additionalMedia_${key}`] = Utils.prepend(`/files/${key}/buxton/${media[key]}`); - } - } - return imageDoc; - }); - // the main document we create - const doc = StackingDocument(deviceImages, { title, hero: new ImageField(constructed[0].url) }); - doc.nameAliases = new List([title.toLowerCase()]); - // add the parsed attributes to this main document - Doc.Get.FromJson({ data: device, appendToExisting: { targetDoc: Doc.GetProto(doc) } }); - Doc.AddDocToList(parentProto, "data", doc); - } else if (errors) { - console.log("Documents:" + errors); - } else { - alert("A Buxton document import was completely empty (??)"); - } - }); - - // when the import is complete, we stop listening for these creation - // and termination events and alert the user - Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => { - _socket.off(MessageStore.BuxtonDocumentResult.Message); - _socket.off(MessageStore.BuxtonImportComplete.Message); - alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`); - }); - const startTime = Date.now(); - Utils.Emit(_socket, MessageStore.BeginBuxtonImport, ""); // signal the server to start importing - return parent; // synchronously return the collection, to be populateds - } - - Scripting.addGlobal(Buxton); - /** * This function receives the relevant document prototype and uses * it to create a new of that base-level prototype, or the @@ -675,7 +597,7 @@ export namespace Docs { } export function ImageDocument(url: string, options: DocumentOptions = {}) { - const imgField = new ImageField(new URL(url)); + const imgField = new ImageField(url); return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options }); } @@ -689,11 +611,11 @@ export namespace Docs { } export function VideoDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(new URL(url)), options); + return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options); } export function YoutubeDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options); + return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options); } export function WebCamDocument(url: string, options: DocumentOptions = {}) { @@ -709,7 +631,7 @@ export namespace Docs { } export function AudioDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), + return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); } @@ -782,11 +704,11 @@ export namespace Docs { } export function PdfDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options); + return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options); } export function WebDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(new URL(url)) : undefined, options); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 8ddfce772..635673025 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -126,7 +126,7 @@ export namespace Hypothesis { }); const annotationId = StrCast(linkDoc.annotationId); - const linkUrl = Utils.prepend("/doc/" + sourceDoc[Id]); + const linkUrl = Doc.globalServerPath(sourceDoc); const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", { detail: { targetUrl: linkUrl, id: annotationId }, diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index a5d27f038..0d9b64d24 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -303,7 +303,7 @@ export function CollectionSubView(schemaCtor: (doc: Doc) => T, moreProps?: } else { const path = window.location.origin + "/doc/"; if (text.startsWith(path)) { - const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0]; + const docid = text.replace(Doc.globalServerPath(), "").split("?")[0]; DocServer.GetRefField(docid).then(f => { if (f instanceof Doc) { if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 339163510..08b5e6bac 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -37,7 +37,7 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { @observable _focusRangeFilters: Opt; getAnchor = () => { - const anchor = Docs.Create.HTMLAnchorDocument({ + const anchor = Docs.Create.HTMLAnchorDocument([], { title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, annotationOn: this.rootDoc }); diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index a2e36f12e..82bad971d 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -196,7 +196,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent { const [{ result }] = await Networking.UploadFilesToServer(e.data); if (!(result instanceof Error)) { - this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client)); + this.props.Document[this.props.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } }; this._recordStart = new Date().getTime(); diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index ddc36daa1..aa3f10188 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -194,7 +194,7 @@ export class DocumentLinksButton extends React.Component GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); } - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "fingerprint" }); + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" }); } } @@ -760,7 +760,7 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument(Utils.prepend("/assets/cheat-sheet.pdf"), { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); } @@ -885,7 +885,7 @@ export class DocumentViewInternal extends DocComponent { const [{ result }] = await Networking.UploadFilesToServer(e.data); if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(Utils.prepend(result.accessPaths.agnostic.client), { title: "audio test", _width: 200, _height: 32 }); + const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 }); audioDoc.treeViewExpandedView = "layout"; const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc)); if (audioAnnos === undefined) { diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 86250c9d1..ebbc1138a 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -64,9 +64,9 @@ export class FieldView extends React.Component { // else if (field instaceof PresBox) { // return ; // } - else if (field instanceof VideoField) { - return ; - } + // else if (field instanceof VideoField) { + // return ; + // } // else if (field instanceof AudioField) { // return ; //} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index cfd43bb62..2c0106960 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -238,7 +238,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent { @computed get href() { if (this.props.hrefs?.length) { const href = this.props.hrefs[this._hrefInd]; - if (href.indexOf(Utils.prepend("/doc/")) !== 0) { // link to a web page URL -- try to show a preview + if (href.indexOf(Doc.localServerPath()) !== 0) { // link to a web page URL -- try to show a preview if (href.startsWith("https://en.wikipedia.org/wiki/")) { wiki().page(href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(action(summary => this._toolTipText = summary.substring(0, 500)))); } else { setTimeout(action(() => this._toolTipText = "url => " + href)); } } else { // hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated - const anchorDoc = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + const anchorDoc = href.replace(Doc.localServerPath(), "").split("?")[0]; anchorDoc && DocServer.GetRefField(anchorDoc).then(action(anchor => { if (anchor instanceof Doc && DocListCast(anchor.links).length) { this._linkDoc = DocListCast(anchor.links)[0]; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 8f61e252b..0b451e2b4 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -53,30 +53,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)); else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action(pdf => this._pdf = pdf)); } - - const backup = "oldPath"; - const href = this.pdfUrl?.url.href; - if (href) { - const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g; - const matches = pathCorrectionTest.exec(href); - // console.log("\nHere's the { url } being fed into the outer regex:"); - // console.log(href); - // console.log("And here's the 'properPath' build from the captured filename:\n"); - if (matches !== null && href.startsWith(window.location.origin)) { - const properPath = Utils.prepend(`/files/pdfs/${matches[0]}`); - //console.log(properPath); - if (!properPath.includes(href)) { - console.log(`The two (url and proper path) were not equal`); - const proto = Doc.GetProto(this.props.Document); - proto[this.props.fieldKey] = new PdfField(properPath); - proto[backup] = href; - } else { - //console.log(`The two (url and proper path) were equal`); - } - } else { - console.log("Outer matches was null!"); - } - } } componentWillUnmount() { this._selectReactionDisposer?.(); } diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 700f8a7d3..0e235a62d 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -227,7 +227,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent { const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); if (!(result instanceof Error)) { - this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client)); + this.dataDoc[this.props.fieldKey + "-audio"] = new AudioField(result.accessPaths.agnostic.client); } }; this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true }); @@ -244,7 +244,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent { const aspect = this.player!.videoWidth / this.player!.videoHeight; Doc.SetNativeWidth(this.dataDoc, this.player!.videoWidth); @@ -182,8 +178,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent { - const url = this.choosePath(Utils.prepend(relative)); + private createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => { + const url = !imagePath.startsWith("/") ? Utils.CorsProxy(imagePath) : imagePath; const width = this.layoutDoc._width || 1; const height = this.layoutDoc._height || 0; const imageSummary = Docs.Create.ImageDocument(url, { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 140d39929..f7e9ee028 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -371,7 +371,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; const anchor = Docs.Create.TextanchorDocument(); const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!; - const allAnchors = [{ href: Utils.prepend("/doc/" + anchor[Id]), title: "a link", anchorId: anchor[Id] }]; + const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }]; const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location }); tr = tr.addMark(flattened[i].from, flattened[i].to, link); }); @@ -705,7 +705,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp let tr = state.tr.addMark(sel.from, sel.to, splitter); if (sel.from !== sel.to) { const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to) }); - const href = targetHref ?? Utils.prepend("/doc/" + anchor[Id]); + const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { @@ -1042,7 +1042,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type.name === "link"); - const allLinks = [{ href: Utils.prepend(`/doc/${linkId}`), title, linkId }]; + const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }]; const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: "add:right", title, docref: true }); marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link); return node.mark(marks); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index a6f8ff2e2..fb4114023 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -821,8 +821,8 @@ export class RichTextMenu extends AntimodeMenu { if (link) { const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined; if (href) { - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + if (href.indexOf(Doc.localServerPath()) === 0) { + const linkclicked = href.replace(Doc.localServerPath(), "").split("?")[0]; if (linkclicked) { const linkDoc = await DocServer.GetRefField(linkclicked); if (linkDoc instanceof Doc) { @@ -864,8 +864,8 @@ export class RichTextMenu extends AntimodeMenu { const allAnchors = linkAnchor.attrs.allAnchors.slice(); this.TextView.RemoveAnchorFromSelection(allAnchors); // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected. - allAnchors.filter((aref: any) => aref?.href.indexOf(Utils.prepend("/doc/")) === 0).forEach((aref: any) => { - const anchorId = aref.href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + allAnchors.filter((aref: any) => aref?.href.indexOf(Doc.localServerPath()) === 0).forEach((aref: any) => { + const anchorId = aref.href.replace(Doc.localServerPath(), "").split("?")[0]; anchorId && DocServer.GetRefField(anchorId).then(linkDoc => LinkManager.Instance.deleteLink(linkDoc as Doc)); }); } diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index 70ca19842..55816ed52 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -85,7 +85,6 @@ export class AnchorMenu extends AntimodeMenu { @action toggleLinkPopup = (e: React.MouseEvent) => { //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button - console.log(window.getSelection().toString()) //change popup visibility field to visible this._showLinkPopup = !this._showLinkPopup; } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 111fd3f0d..a7e5d8541 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -596,7 +596,7 @@ export namespace Doc { const mapped = cloneMap.get(id); return href + (mapped ? mapped[Id] : id); }; - const regex = `(${Utils.prepend("/doc/")})([^"]*)`; + const regex = `(${Doc.localServerPath()})([^"]*)`; const re = new RegExp(regex, "g"); copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text); }); @@ -896,6 +896,16 @@ export namespace Doc { return true; } + + // converts a document id to a url path on the server + export function globalServerPath(doc: Doc | string = ""): string { + return Utils.prepend("/doc/" + (doc instanceof Doc ? doc[Id] : doc)); + } + // converts a document id to a url path on the server + export function localServerPath(doc?: Doc): string { + return "/doc/" + (doc ? doc[Id] : ""); + } + export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { const doc2Layout = Doc.Layout(doc2); const doc1Layout = Doc.Layout(doc1); diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index fb71160ca..d96e8a70a 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -3,14 +3,17 @@ import { serializable, custom } from "serializr"; import { ObjectField } from "./ObjectField"; import { ToScriptString, ToString, Copy } from "./FieldSymbols"; import { Scripting, scriptingGlobal } from "../client/util/Scripting"; +import { Utils } from "../Utils"; function url() { return custom( function (value: URL) { - return value.href; + return value.origin === window.location.origin ? + value.pathname : + value.href; }, function (jsonValue: string) { - return new URL(jsonValue); + return new URL(jsonValue, window.location.origin); } ); } @@ -24,15 +27,21 @@ export abstract class URLField extends ObjectField { constructor(url: URL | string) { super(); if (typeof url === "string") { - url = new URL(url); + url = url.startsWith("http") ? new URL(url) : new URL(url, window.location.origin); } this.url = url; } [ToScriptString]() { + if (Utils.prepend(this.url.pathname) === this.url.href) { + return `new ${this.constructor.name}("${this.url.pathname}")`; + } return `new ${this.constructor.name}("${this.url.href}")`; } [ToString]() { + if (Utils.prepend(this.url.pathname) === this.url.href) { + return this.url.pathname; + } return this.url.href; } diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index 98696496f..f910d765e 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -50,7 +50,7 @@ export class Uploader extends React.Component { if (result instanceof Error) { return; } - const path = Utils.prepend(result.accessPaths.agnostic.client); + const path = result.accessPaths.agnostic.client; let doc = null; // Case 1: File is a video if (file.type === "video/mp4") { diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index e40f2b8e5..0f4a067fc 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -142,8 +142,9 @@ function registerCorsProxy(server: express.Express) { const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; server.use("/corsProxy", async (req, res) => { - const requrl = decodeURIComponent(req.url.substring(1)); const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ""; + const requrlraw = decodeURIComponent(req.url.substring(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/ and the requested url path was relative, // then we redirect again to the cors referer and just add the relative path. -- cgit v1.2.3-70-g09d2