From 37be2477782b0b372c16d46df3f9634ebf3677fe Mon Sep 17 00:00:00 2001 From: Geireann Lindfield Roberts <60007097+geireann@users.noreply.github.com> Date: Wed, 21 Sep 2022 01:43:03 -0400 Subject: restyled document decorations and changed novice mode to exclude `data` in treeView --- src/client/views/DocumentDecorations.scss | 189 ++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 65 deletions(-) (limited to 'src/client/views/DocumentDecorations.scss') diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index b490278c3..2e8d31478 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -22,6 +22,129 @@ $resizeHandler: 8px; grid-template-columns: $resizeHandler 1fr $resizeHandler; pointer-events: none; + .documentDecorations-topbar { + display: flex; + grid-column-start: 1; + grid-column-end: 4; + flex-direction: row; + gap: 2px; + + + .documentDecorations-openButton { + display: flex; + align-items: center; + justify-content: center; + background: #3ce312; + border: solid 1.5px #0a620a; + color: #3ce312; + transition: 0.1s ease; + font-size: 11px; + opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + cursor: pointer; + + &:hover { + color: #02600d; + } + } + + .documentDecorations-closeButton { + display: flex; + align-items: center; + justify-content: center; + background: #fb9d75; + border: solid 1.5px #a94442; + color: #fb9d75; + transition: 0.1s ease; + opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + cursor: pointer; + + &:hover { + color: #a94442; + } + + > svg { + margin: 0; + } + } + + .documentDecorations-minimizeButton { + display: flex; + align-items: center; + justify-content: center; + background: #ffdd00; + border: solid 1.5px #a94442; + color: #ffdd00; + transition: 0.1s ease; + font-size: 11px; + opacity: 1; + grid-column: 2; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + cursor: pointer; + + &:hover { + color: #a94442; + } + + > svg { + margin: 0; + } + } + + .documentDecorations-title-Dark, + .documentDecorations-title { + opacity: 1; + width: calc(100% - 60px); // = margin-left + margin-right + grid-column: 3; + pointer-events: auto; + overflow: hidden; + text-align: center; + display: flex; + height: 20px; + border-radius: 8px; + outline: none; + border: none; + + .documentDecorations-titleSpan, + .documentDecorations-titleSpan-Dark { + width: 100%; + border-radius: 8px; + background: $light-gray; + display: inline-block; + cursor: move; + } + .documentDecorations-titleSpan-Dark { + background: hsla(0, 0%, 0%, 0.412); + } + } + + .documentDecorations-title-Dark { + color: white; + background: black; + } + + .documentDecorations-titleBackground { + background: $light-gray; + border-radius: 8px; + width: 100%; + height: 100%; + position: absolute; + } + } + .documentDecorations-centerCont { grid-column: 2; background: none; @@ -225,76 +348,12 @@ $resizeHandler: 8px; cursor: ew-resize; } - .documentDecorations-title-Dark, - .documentDecorations-title { - opacity: 1; - width: calc(100% - 8px); // = margin-left + margin-right - grid-column: 2; - grid-column-end: 2; - pointer-events: auto; - overflow: hidden; - text-align: center; - display: flex; - margin-left: 6px; // closeButton width (14) - leftColumn width (8) - margin-right: 2px; - height: 20px; - position: absolute; - border-radius: 8px; - background: rgba(159, 159, 159, 0.1); - - .documentDecorations-titleSpan, - .documentDecorations-titleSpan-Dark { - width: 100%; - border-radius: 8px; - background: #ffffffa0; - position: absolute; - display: inline-block; - cursor: move; - } - .documentDecorations-titleSpan-Dark { - background: hsla(0, 0%, 0%, 0.412); - } - } - .documentDecorations-title-Dark { - color: white; - background: black; - } - - .documentDecorations-titleBackground { - background: #ffffffcf; - border-radius: 8px; - width: 100%; - height: 100%; - position: absolute; - } - .focus-visible { margin-left: 0px; } } -.documentDecorations-openButton { - display: flex; - align-items: center; - opacity: 1; - grid-column-start: 3; - pointer-events: all; - cursor: pointer; -} -.documentDecorations-closeButton { - display: flex; - align-items: center; - opacity: 1; - grid-column: 1; - pointer-events: all; - width: 14px; - cursor: pointer; - - > svg { - margin: 0; - } -} .documentDecorations-background { background: lightblue; @@ -329,7 +388,7 @@ $resizeHandler: 8px; justify-content: center; align-items: center; gap: 5px; - background: $medium-gray; + background: $light-gray; } .linkButtonWrapper { -- cgit v1.2.3-70-g09d2 From b772fb95bb2f980f91da44d1442f52c4fb0b6eac Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 17 Oct 2022 19:07:32 -0400 Subject: added rotation around an arbitrary point to doc decos. fixed show title for func plots and for shared docs in novice mode. --- src/Utils.ts | 4 + src/client/util/DragManager.ts | 2 +- src/client/views/DocumentDecorations.scss | 56 +++++++------ src/client/views/DocumentDecorations.tsx | 135 ++++++++++++++++++++++-------- src/client/views/StyleProvider.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 77 +++++++++-------- 6 files changed, 175 insertions(+), 101 deletions(-) (limited to 'src/client/views/DocumentDecorations.scss') diff --git a/src/Utils.ts b/src/Utils.ts index 6fc00040f..bf1f72774 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -243,6 +243,10 @@ export namespace Utils { return [Math.sqrt((ys2 - ye) * (ys2 - ye) + (x2 - x) * (x2 - x)), [x, ye, x, ye]]; } + export function rotPt(x: number, y: number, radAng: number) { + return { x: x * Math.cos(radAng) - y * Math.sin(radAng), y: x * Math.sin(radAng) + y * Math.cos(radAng) }; + } + function project(px: number, py: number, ax: number, ay: number, bx: number, by: number) { if (ax === bx && ay === by) return { point: { x: ax, y: ay }, left: false, dot: 0, t: 0 }; const atob = { x: bx - ax, y: by - ay }; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 664933de0..160aba294 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -360,7 +360,7 @@ export namespace DragManager { let useDim = false; if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') { ele = ele.parentElement.parentElement.parentElement; - const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.]*)deg\).*/, '$1'); + const rotStr = ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1'); if (rotStr) rot = Number(rotStr); } else { useDim = true; diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 2e8d31478..e915fc88d 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -8,12 +8,38 @@ $resizeHandler: 8px; .documentDecorations { position: absolute; z-index: 2000; + + // Rotation handler + .documentDecorations-rotation { + border-radius: 100%; + height: 30; + width: 30; + right: -17; + top: calc(50% - 15px); + position: absolute; + pointer-events: all; + cursor: pointer; + background: white; + display: flex; + justify-content: center; + align-items: center; + text-align: center; + font-size: 30px; + } + .documentDecorations-rotationCenter { + position: absolute; + width: 6px; + height: 6px; + pointer-events: all; + background: green; + border-radius: 50%; + } } .documentDecorations-Dark { background: dimgray; } + .documentDecorations-container { - z-index: $docDecorations-zindex; position: absolute; top: 0; left: 0; @@ -29,7 +55,6 @@ $resizeHandler: 8px; flex-direction: row; gap: 2px; - .documentDecorations-openButton { display: flex; align-items: center; @@ -51,7 +76,7 @@ $resizeHandler: 8px; color: #02600d; } } - + .documentDecorations-closeButton { display: flex; align-items: center; @@ -71,7 +96,7 @@ $resizeHandler: 8px; &:hover { color: #a94442; } - + > svg { margin: 0; } @@ -98,7 +123,7 @@ $resizeHandler: 8px; &:hover { color: #a94442; } - + > svg { margin: 0; } @@ -207,25 +232,6 @@ $resizeHandler: 8px; grid-column: 3; } - // Rotation handler - .documentDecorations-rotation { - border-radius: 100%; - height: 30; - width: 30; - right: -10; - top: 50%; - z-index: 1000000; - position: absolute; - pointer-events: all; - cursor: pointer; - background: white; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - font-size: 30px; - } - // Border radius handler .documentDecorations-borderRadius { position: absolute; @@ -353,8 +359,6 @@ $resizeHandler: 8px; } } - - .documentDecorations-background { background: lightblue; position: absolute; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 8b8c32642..8f66eaca7 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -12,7 +12,7 @@ import { InkField } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; -import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents } from '../../Utils'; +import { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; @@ -30,7 +30,6 @@ import { LightboxView } from './LightboxView'; import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; -import { VideoBox } from './nodes/VideoBox'; import React = require('react'); @observer @@ -311,49 +310,83 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P e => {} // clickEvent ); }; + @action + onRotateCenterDown = (e: React.PointerEvent): void => { + this._isRotating = true; + const seldocview = SelectionManager.Views()[0]; + setupMoveUpEvents( + this, + e, + action((e: PointerEvent, down: number[], delta: number[]) => { + const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]); + const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; + const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI); + seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width); + seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height); + + return false; + }), // moveEvent + action(() => {}), // upEvent + action((e, doubleTap) => { + if (doubleTap) { + seldocview.rootDoc.rotateCenterX = 0.5; + seldocview.rootDoc.rotateCenterY = 0.5; + } + }) + ); + }; @action onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; + const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; const rotateUndo = UndoManager.StartBatch('rotatedown'); const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); const centerPoint = { X: (this.Bounds.x + this.Bounds.r) / 2, Y: (this.Bounds.y + this.Bounds.b) / 2 }; + const infos = new Map(); + SelectionManager.Views().forEach(dv => { + const accumRot = (NumCast(dv.rootDoc._jitterRotation) / 180) * Math.PI; + const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y); + const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2]; + const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); + const unrotatedDocPos = { x: NumCast(dv.rootDoc.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.rootDoc.y) + localRotCtrOffset[1] - startRotCtr.y }; + infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot }); + }); + const infoRot = (angle: number, isAbs = false) => { + SelectionManager.Views().forEach( + action(dv => { + const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.rootDoc)!; + const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle); + infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle }); + dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x); + dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y); + dv.rootDoc._jitterRotation = ((isAbs ? 0 : NumCast(dv.rootDoc._jitterRotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 + }) + ); + }; setupMoveUpEvents( this, e, (e: PointerEvent, down: number[], delta: number[]) => { const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); + const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen); if (selectedInk.length) { - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, -deltaAng, centerPoint); } else { - SelectionManager.Views().forEach(dv => { - const oldRotation = NumCast(dv.rootDoc._jitterRotation); - // Rotation between -360 and 360 - let newRotation = (oldRotation - (angle * 180) / Math.PI) % 360; - - const diff = Math.round(newRotation / 45) - newRotation / 45; - if (diff < 0.05) { - console.log('show lines'); - } - dv.rootDoc._jitterRotation = newRotation; - }); + infoRot(deltaAng); } return false; }, // moveEvent action(() => { - SelectionManager.Views().forEach(dv => { - const oldRotation = NumCast(dv.rootDoc._jitterRotation); - const diff = Math.round(oldRotation / 45) - oldRotation / 45; - if (diff < 0.05) { - let newRotation = Math.round(oldRotation / 45) * 45; - dv.rootDoc._jitterRotation = newRotation; - } - }); + const dv = SelectionManager.Views()[0]; + const oldRotation = NumCast(dv.rootDoc._jitterRotation); + const diff = oldRotation - Math.round(oldRotation / 45) * 45; + if (Math.abs(diff) < 5) { + infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true); + } this._isRotating = false; rotateUndo?.end(); - UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']); }), // upEvent emptyFunction ); @@ -603,8 +636,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon'); } + @observable _rotCenter = [0, 0]; + @computed get rotCenter() { + if (SelectionManager.Views().length) { + const seldocview = SelectionManager.Views()[0]; + const loccenter = Utils.rotPt( + NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width), + NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height), + (NumCast(seldocview.rootDoc._jitterRotation) / 180) * Math.PI + ); + return seldocview.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(loccenter.x + NumCast(seldocview.layoutDoc._width) / 2, loccenter.y + NumCast(seldocview.layoutDoc._height) / 2); + } + return this._rotCenter; + } + render() { - const bounds = this.Bounds; + const { b, c, r, x, y } = this.Bounds; + const bounds = { b, c, r, x, y }; const seldocview = SelectionManager.Views().slice(-1)[0]; if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return null; @@ -695,8 +746,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
)} - - {useRotation && ( -
e.preventDefault()}> - } isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} /> -
- )} - {useRounding && (
)}
+ + {useRotation && ( + <> +
+
e.preventDefault()}> + } isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} /> +
+
+
e.preventDefault()} + /> + + )}
)}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 8b256923a..aadcd7169 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -131,7 +131,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt users.user.email === this.dataDoc.author)?.sharingDoc.userColor, Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' ); - const titleView = - !showTitle || Doc.noviceMode ? null : ( -
- field.trim()) - .map(field => targetDoc[field]?.toString()) - .join('\\')} - display={'block'} - fontSize={10} - GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} - SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { - if (this.props.showTitle) { - this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else { - Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; - } + const titleView = !showTitle ? null : ( +
+ field.trim()) + .map(field => targetDoc[field]?.toString()) + .join('\\')} + display={'block'} + fontSize={10} + GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)} + SetValue={undoBatch((input: string) => { + if (input?.startsWith('#')) { + if (this.props.showTitle) { + this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; } else { - var value = input.replace(new RegExp(showTitle + '='), '') as string | number; - if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (showTitle.includes('Date') || showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); + Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; } - return true; - })} - /> -
- ); + } else { + var value = input.replace(new RegExp(showTitle + '='), '') as string | number; + if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (showTitle.includes('Date') || showTitle === 'author') return true; + Doc.SetInPlace(targetDoc, showTitle, value, true); + } + return true; + })} + /> +
+ ); return this.props.hideTitle || (!showTitle && !showCaption) ? ( this.contents ) : ( -- cgit v1.2.3-70-g09d2 From 880023cb520f82fa1bfd4740450a18e86e817b0f Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 18 Oct 2022 13:00:55 -0400 Subject: starting to fix resizing rotated docs. --- src/client/views/DocumentDecorations.scss | 2 +- src/client/views/DocumentDecorations.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) (limited to 'src/client/views/DocumentDecorations.scss') diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index e915fc88d..5c8dd36cc 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -14,7 +14,7 @@ $resizeHandler: 8px; border-radius: 100%; height: 30; width: 30; - right: -17; + right: -30; top: calc(50% - 15px); position: absolute; pointer-events: all; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index d79cf014d..9881ef9f1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -579,10 +579,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } else doc._height = actualdH; } } else { + const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; + const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._jitterRotation) / 180) * Math.PI); + const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); dW && (doc._width = actualdW); dH && (doc._autoHeight = false); + + const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; + const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._jitterRotation) / 180) * Math.PI); + doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc + doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]); } doc.x = (doc.x || 0) + dX * (actualdW - docwidth); doc.y = (doc.y || 0) + (dragBottom ? 0 : dY * (actualdH - docheight)); -- cgit v1.2.3-70-g09d2 From a81ab2e6f75681bc4fb3a7b49d2056144e396b94 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 19 Oct 2022 12:38:19 -0400 Subject: fixed erasing straight line stroke segments caused by intersections with other strokes. cleaned up more with gestures. changed docView lock icon to be part of docDecorations. --- src/client/views/DocumentDecorations.scss | 28 ++++++- src/client/views/DocumentDecorations.tsx | 41 +++++++++- src/client/views/GestureOverlay.tsx | 21 +++--- src/client/views/StyleProvider.scss | 18 ++--- src/client/views/StyleProvider.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 58 ++++++-------- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/ImageBox.tsx | 5 +- src/client/views/nodes/button/FontIconBox.tsx | 20 ++--- src/pen-gestures/GestureUtils.ts | 2 - src/pen-gestures/ndollar.ts | 88 +++++++++------------- 11 files changed, 148 insertions(+), 137 deletions(-) (limited to 'src/client/views/DocumentDecorations.scss') diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 5c8dd36cc..4e0b061a6 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -1,7 +1,7 @@ @import 'global/globalCssVariables'; $linkGap: 3px; -$headerHeight: 20px; +$headerHeight: 25px; $resizeHandler: 8px; .documentDecorations-Dark, @@ -54,6 +54,8 @@ $resizeHandler: 8px; grid-column-end: 4; flex-direction: row; gap: 2px; + pointer-events: all; + cursor: move; .documentDecorations-openButton { display: flex; @@ -236,8 +238,8 @@ $resizeHandler: 8px; .documentDecorations-borderRadius { position: absolute; border-radius: 100%; - left: 3px; - top: 23px; + left: 7px; + top: 27px; background: $medium-gray; height: 10; width: 10; @@ -245,6 +247,21 @@ $resizeHandler: 8px; cursor: nwse-resize; } + .documentDecorations-lock { + position: absolute; + background: black; + right: 11; + top: 30px; + color: gray; + height: 14; + width: 14; + pointer-events: all; + margin: auto; + display: flex; + align-items: center; + flex-direction: column; + } + .documentDecorations-rotationPath { position: absolute; width: 100%; @@ -260,6 +277,7 @@ $resizeHandler: 8px; cursor: nwse-resize; background: unset; opacity: 1; + transform: scale(2); } .documentDecorations-topLeftResizer { @@ -314,6 +332,7 @@ $resizeHandler: 8px; .documentDecorations-topLeftResizer:hover, .documentDecorations-bottomRightResizer:hover { opacity: 1; + background: black; } .documentDecorations-bottomRightResizer { @@ -325,6 +344,7 @@ $resizeHandler: 8px; cursor: nesw-resize; background: unset; opacity: 1; + transform: scale(2); } .documentDecorations-topRightResizer { @@ -339,7 +359,6 @@ $resizeHandler: 8px; .documentDecorations-topRightResizer:hover, .documentDecorations-bottomLeftResizer:hover { - cursor: nesw-resize; background: black; opacity: 1; } @@ -392,6 +411,7 @@ $resizeHandler: 8px; justify-content: center; align-items: center; gap: 5px; + top: 4px; background: $light-gray; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 9881ef9f1..6cf7df357 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -140,6 +140,16 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } }; + @action onContainerDown = (e: React.PointerEvent): void => { + setupMoveUpEvents( + this, + e, + e => this.onBackgroundMove(true, e), + e => {}, + emptyFunction + ); + }; + @action onTitleDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, @@ -311,6 +321,27 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); }; + @action + onLockDown = (e: React.PointerEvent): void => { + // Call util move event function + setupMoveUpEvents( + this, // target + e, // pointerEvent + returnFalse, // moveEvent + emptyFunction, // upEvent + e => { + UndoManager.RunInBatch( + () => + SelectionManager.Views().map(dv => { + dv.rootDoc._lockedPosition = !dv.rootDoc._lockedPosition; + dv.rootDoc._pointerEvents = dv.rootDoc._lockedPosition ? 'none' : undefined; + }), + 'toggleBackground' + ); + } // clickEvent + ); + }; + setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => { const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]); const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; @@ -791,7 +822,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> -
+
{hideDeleteButton ?
: topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ?
: topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} {hideResizers ?
: titleArea} @@ -817,21 +848,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P key="rad" style={{ background: `${this._isRounding ? Colors.MEDIUM_BLUE : undefined}`, - left: `${radiusHandleLocation + 3}`, - top: `${radiusHandleLocation + 23}`, + transform: `translate(${radiusHandleLocation}px, ${radiusHandleLocation}px)`, }} className={`documentDecorations-borderRadius`} onPointerDown={this.onRadiusDown} onContextMenu={e => e.preventDefault()} /> )} +
e.preventDefault()}> + +
{hideDocumentButtonBar ? null : (
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 23b03bc50..362aa3c86 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -1,7 +1,7 @@ import React = require('react'); import * as fitCurve from 'fit-curve'; import { action, computed, observable, runInAction, trace } from 'mobx'; -import { Doc } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; import { InkData, InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; @@ -49,7 +49,7 @@ interface GestureOverlayProps { export class GestureOverlay extends Touchable { static Instance: GestureOverlay; - @observable public InkShape: string = ''; + @observable public InkShape: Opt; @observable public SavedColor?: string; @observable public SavedWidth?: number; @observable public Tool: ToolglassTools = ToolglassTools.None; @@ -644,8 +644,6 @@ export class GestureOverlay extends Touchable { if (this._points.length > 1) { const B = this.svgBounds; const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); - //push first points to so interactionUtil knows pointer is up - this._points.push({ X: this._points[0].X, Y: this._points[0].Y }); const initialPoint = this._points[0]; const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + this.height; @@ -685,10 +683,10 @@ export class GestureOverlay extends Touchable { //if any of the shape is activated in the CollectionFreeFormViewChrome else if (this.InkShape) { this.makeBezierPolygon(this.InkShape, false); - this.dispatchGesture(GestureUtils.Gestures.Stroke); + this.dispatchGesture(this.InkShape); this._points.length = 0; if (!CollectionFreeFormViewChrome.Instance?._keepPrimitiveMode) { - this.InkShape = ''; + this.InkShape = undefined; Doc.ActiveTool = InkTool.None; } } @@ -703,12 +701,13 @@ export class GestureOverlay extends Touchable { case GestureUtils.Gestures.Rectangle: case GestureUtils.Gestures.Circle: this.makeBezierPolygon(result.Name, true); - case GestureUtils.Gestures.StartBracket: - case GestureUtils.Gestures.EndBracket: actionPerformed = this.dispatchGesture(result.Name); break; case GestureUtils.Gestures.Line: - actionPerformed = this.handleLineGesture(); + if (!(actionPerformed = this.handleLineGesture())) { + this.makeBezierPolygon(result.Name, true); + actionPerformed = this.dispatchGesture(GestureUtils.Gestures.Stroke); + } break; case GestureUtils.Gestures.Scribble: console.log('scribble'); @@ -972,7 +971,7 @@ export class GestureOverlay extends Touchable { ActiveDash(), 1, 1, - this.InkShape, + this.InkShape ?? '', 'none', 1.0, false @@ -999,7 +998,7 @@ export class GestureOverlay extends Touchable { ActiveDash(), 1, 1, - this.InkShape, + this.InkShape ?? '', 'none', 1.0, false diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 8929954c8..b1c97164a 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -1,11 +1,11 @@ .styleProvider-lock { - font-size: 12px; - width: 20; - height: 20; - position: absolute; - right: -25; - top: -5; - background: transparent; + font-size: 10; + width: 15; + height: 15; + position: absolute; + right: -0; + top: 0; + background: black; pointer-events: all; opacity: 0.3; display: flex; @@ -15,7 +15,7 @@ cursor: default; } .styleProvider-lock:hover { - opacity:1; + opacity: 1; } .styleProvider-treeView-icon, @@ -26,4 +26,4 @@ .styleProvider-treeView-icon { opacity: 0; -} \ No newline at end of file +} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index aadcd7169..a5a886f42 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -276,7 +276,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt 0 && ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? ( diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index daf69d4f6..983cf4f9b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -568,6 +568,8 @@ export class CollectionFreeFormView extends CollectionSubView { switch (ge.gesture) { + case GestureUtils.Gestures.Line: + break; default: case GestureUtils.Gestures.Circle: case GestureUtils.Gestures.Rectangle: @@ -602,33 +604,6 @@ export class CollectionFreeFormView extends CollectionSubView p.X)), Math.min(...ge.points.map(p => p.Y))); - const rb = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); - const bounds = { x: lt[0], r: rb[0], y: lt[1], b: rb[1] }; - const bWidth = bounds.r - bounds.x; - const bHeight = bounds.b - bounds.y; - const sel = this.getActiveDocuments().filter(doc => { - const l = NumCast(doc.x); - const r = l + doc[WidthSym](); - const t = NumCast(doc.y); - const b = t + doc[HeightSym](); - const pass = !(bounds.x > r || bounds.r < l || bounds.y > b || bounds.b < t); - if (pass) { - doc.x = l - bounds.x - bWidth / 2; - doc.y = t - bounds.y - bHeight / 2; - } - return pass; - }); - this.addDocument(Docs.Create.FreeformDocument(sel, { title: 'nested collection', x: bounds.x, y: bounds.y, _width: bWidth, _height: bHeight, _panX: 0, _panY: 0 })); - sel.forEach(d => this.props.removeDocument?.(d)); - e.stopPropagation(); - break; - case GestureUtils.Gestures.StartBracket: - const start = this.getTransform().transformPoint(Math.min(...ge.points.map(p => p.X)), Math.min(...ge.points.map(p => p.Y))); - this._inkToTextStartX = start[0]; - this._inkToTextStartY = start[1]; - break; - case GestureUtils.Gestures.EndBracket: if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color); @@ -805,15 +780,15 @@ export class CollectionFreeFormView extends CollectionSubView ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve } @@ -862,6 +837,15 @@ export class CollectionFreeFormView extends CollectionSubView { + if ((otherCurve as any)._linear) { + return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] }); + } + return curve.intersects(otherCurve); + }; + /** * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all * ink strokes in the current collection. @@ -886,7 +870,7 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))); - curve.intersects(otherCurve).forEach((val: string | number, i: number) => { + this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => { // Converting the Bezier.js Split type to a t-value number. const t = +val.toString().split('/')[0]; if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical). diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1c48d47e9..040e03150 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1082,7 +1082,7 @@ export class DocumentViewInternal extends DocComponent )} {audioView} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 959c641a8..461d6984d 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -8,7 +8,7 @@ import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; import { createSchema } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast } from '../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils'; @@ -77,10 +77,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), { fireImmediately: true, delay: 1000 } ); + const layoutDoc = this.layoutDoc; this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), ({ nativeSize, width }) => { - if (true || !this.layoutDoc._height) { + if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 6eaf3c31a..fd0c0d141 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -771,36 +771,26 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); } -/** INK - * setActiveTool - * setStrokeWidth - * setStrokeColor - **/ - -ScriptingGlobals.add(function setActiveTool(tool: string, checkResult?: boolean) { +ScriptingGlobals.add(function setActiveTool(tool: InkTool | GestureUtils.Gestures, checkResult?: boolean) { InkTranscription.Instance?.createInkGroup(); if (checkResult) { return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? Colors.MEDIUM_BLUE : 'transparent'; } - if ([GestureUtils.Gestures.Circle, GestureUtils.Gestures.Rectangle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Triangle].includes(tool as any)) { + if (Object.values(GestureUtils.Gestures).includes(tool as any)) { if (GestureOverlay.Instance.InkShape === tool) { Doc.ActiveTool = InkTool.None; - GestureOverlay.Instance.InkShape = InkTool.None; + GestureOverlay.Instance.InkShape = undefined; } else { Doc.ActiveTool = InkTool.Pen; - GestureOverlay.Instance.InkShape = tool; + GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures; } } else if (tool) { // pen or eraser if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) { Doc.ActiveTool = InkTool.None; - } else if (tool == InkTool.Write) { - // console.log("write mode selected - create groupDoc here!", tool) - Doc.ActiveTool = tool; - GestureOverlay.Instance.InkShape = ''; } else { Doc.ActiveTool = tool as any; - GestureOverlay.Instance.InkShape = ''; + GestureOverlay.Instance.InkShape = undefined; } } else { Doc.ActiveTool = InkTool.None; diff --git a/src/pen-gestures/GestureUtils.ts b/src/pen-gestures/GestureUtils.ts index 2d3b1fdb8..41917aac9 100644 --- a/src/pen-gestures/GestureUtils.ts +++ b/src/pen-gestures/GestureUtils.ts @@ -19,8 +19,6 @@ export namespace GestureUtils { export enum Gestures { Line = 'line', - StartBracket = 'startbracket', - EndBracket = 'endbracket', Stroke = 'stroke', Scribble = 'scribble', Text = 'text', diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index b10a9da17..3ee9506cb 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -145,7 +145,7 @@ export class Result { // // NDollarRecognizer constants // -const NumMultistrokes = 6; +let NumMultistrokes = 0; const NumPoints = 96; const SquareSize = 250.0; const OneDThreshold = 0.25; // customize to desired gesture set (usually 0.20 - 0.35) @@ -162,73 +162,59 @@ const AngleSimilarityThreshold = Deg2Rad(30.0); // NDollarRecognizer class // export class NDollarRecognizer { - public Multistrokes: Multistroke[]; + public Multistrokes: Multistroke[] = []; - /** - * @IMPORTANT - IF YOU'RE ADDING A NEW GESTURE, BE SURE TO INCREMENT THE NumMultiStrokes CONST RIGHT ABOVE THIS CLASS. - */ constructor( useBoundedRotationInvariance: boolean // constructor ) { // // one predefined multistroke for each multistroke type // - this.Multistrokes = new Array(NumMultistrokes); - this.Multistrokes[0] = new Multistroke( - GestureUtils.Gestures.Rectangle, - useBoundedRotationInvariance, - new Array( + this.Multistrokes.push( + new Multistroke( + GestureUtils.Gestures.Rectangle, + useBoundedRotationInvariance, new Array( - new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200), - new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230), - new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160), - new Point(106, 146), //new Point(80, 150), new Point(50, 146), - new Point(30, 143) + new Array( + new Point(30, 146), //new Point(29, 160), new Point(30, 180), new Point(31, 200), + new Point(30, 222), //new Point(50, 219), new Point(70, 225), new Point(90, 230), + new Point(106, 225), //new Point(100, 200), new Point(106, 180), new Point(110, 160), + new Point(106, 146), //new Point(80, 150), new Point(50, 146), + new Point(30, 143) + ) ) ) ); - this.Multistrokes[1] = new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347)))); - this.Multistrokes[2] = new Multistroke( - GestureUtils.Gestures.StartBracket, - useBoundedRotationInvariance, - new Array( - // new Array(new Point(145, 20), new Point(30, 21), new Point(34, 150)) - new Array(new Point(31, 25), new Point(145, 20), new Point(31, 25), new Point(34, 150)) + this.Multistrokes.push(new Multistroke(GestureUtils.Gestures.Line, useBoundedRotationInvariance, new Array(new Array(new Point(12, 347), new Point(119, 347))))); + this.Multistrokes.push( + new Multistroke( + GestureUtils.Gestures.Triangle, // equilateral + useBoundedRotationInvariance, + new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100))) ) ); - this.Multistrokes[3] = new Multistroke( - GestureUtils.Gestures.EndBracket, - useBoundedRotationInvariance, - new Array( - // new Array(new Point(150, 21), new Point(149, 150), new Point(26, 152)) - // new Array(new Point(150, 150), new Point(150, 0), new Point(150, 150), new Point(0, 150)) - new Array(new Point(10, 100), new Point(100, 100), new Point(150, 12), new Point(200, 103), new Point(300, 100)) - ) - ); - this.Multistrokes[4] = new Multistroke( - GestureUtils.Gestures.Triangle, // equilateral - useBoundedRotationInvariance, - new Array(new Array(new Point(40, 100), new Point(100, 200), new Point(140, 102), new Point(42, 100))) - ); - this.Multistrokes[5] = new Multistroke( - GestureUtils.Gestures.Circle, - useBoundedRotationInvariance, - new Array( + this.Multistrokes.push( + new Multistroke( + GestureUtils.Gestures.Circle, + useBoundedRotationInvariance, new Array( - new Point(200, 250), - new Point(240, 230), - new Point(248, 210), - new Point(248, 190), - new Point(240, 170), - new Point(200, 150), - new Point(160, 170), - new Point(151, 190), - new Point(151, 210), - new Point(160, 230), - new Point(201, 250) + new Array( + new Point(200, 250), + new Point(240, 230), + new Point(248, 210), + new Point(248, 190), + new Point(240, 170), + new Point(200, 150), + new Point(160, 170), + new Point(151, 190), + new Point(151, 210), + new Point(160, 230), + new Point(201, 250) + ) ) ) ); + NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes // // PREDEFINED STROKES // -- cgit v1.2.3-70-g09d2 From dc942e6228e003caa3754a72c0e126d64332a004 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 1 Nov 2022 18:29:11 -0400 Subject: fixes for goldenlayout to initialize more cleanly. updated all old ReactDOM.render() to ReactDom.createRoot(). fixes for PDF/Web sidebar sizing. added text from pdf selection anchors to sidebar notes. fixed PDF text selection to align properly. --- src/client/goldenLayout.js | 11 +- src/client/util/DragManager.ts | 2 +- src/client/util/HypothesisUtils.ts | 160 ++++++++++++--------- src/client/views/DocumentDecorations.scss | 5 +- src/client/views/DocumentDecorations.tsx | 9 +- src/client/views/GlobalKeyHandler.ts | 2 +- src/client/views/Main.tsx | 7 +- src/client/views/MainView.scss | 9 -- src/client/views/MainView.tsx | 94 +----------- src/client/views/MarqueeAnnotator.tsx | 6 +- src/client/views/SidebarAnnos.tsx | 22 +++ src/client/views/StyleProvider.tsx | 6 +- .../views/collections/CollectionDockingView.scss | 4 + .../views/collections/CollectionDockingView.tsx | 17 ++- src/client/views/collections/TabDocView.tsx | 6 +- src/client/views/nodes/DocumentLinksButton.tsx | 3 - src/client/views/nodes/PDFBox.tsx | 13 +- src/client/views/nodes/WebBox.tsx | 14 +- src/client/views/nodes/button/FontIconBox.tsx | 12 +- .../nodes/formattedText/DashDocCommentView.tsx | 8 +- .../views/nodes/formattedText/DashDocView.tsx | 12 +- .../views/nodes/formattedText/DashFieldView.tsx | 53 +++++-- .../views/nodes/formattedText/EquationView.tsx | 10 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 1 + .../views/nodes/formattedText/RichTextRules.ts | 7 +- .../views/nodes/formattedText/SummaryView.tsx | 9 +- src/client/views/nodes/formattedText/nodes_rts.ts | 1 + src/client/views/pdf/PDFViewer.scss | 12 +- src/client/views/pdf/PDFViewer.tsx | 6 +- src/debug/Repl.tsx | 37 +++-- 30 files changed, 278 insertions(+), 280 deletions(-) (limited to 'src/client/views/DocumentDecorations.scss') diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index dd11e6466..bc08b4d0b 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -459,7 +459,7 @@ this._bDragging = false; this.emit('dragStop', oEvent, this._nOriginalX + this._nX); } else { // make title receive pointer events to allow setting insertion position or selecting texst range - const classname = typeof oEvent.target?.className === "string" ? oEvent.target.className : ""; + const classname = oEvent.target ? (typeof oEvent.target.className === "string" ? oEvent.target.className : ""): ""; if (classname.includes("lm_title_wrap")) { oEvent.target.children[0].style.pointerEvents = "all"; oEvent.target.children[0].focus(); @@ -5391,10 +5391,11 @@ * @returns {void} */ _render: function () { - this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]); - this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () { + this._reactComponent = ReactDOM.createRoot(this._container.getElement()[0]) + this._reactComponent.render(this._getReactComponent()); + this._originalComponentWillUpdate = this._reactComponent.componentDidUpdate || function () { }; - this._reactComponent.componentWillUpdate = this._onUpdate.bind(this); + this._reactComponent.componentDidUpdate = this._onUpdate.bind(this); if (this._container.getState()) { this._reactComponent.setState(this._container.getState()); } @@ -5407,7 +5408,7 @@ * @returns {void} */ _destroy: function () { - ReactDOM.unmountComponentAtNode(this._container.getElement()[0]); + // ReactDOM.unmountComponentAtNode(this._container.getElement()[0]); this._container.off('open', this._render, this); this._container.off('destroy', this._destroy, this); }, diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 36e5a65fb..d0690fa10 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -450,7 +450,7 @@ export namespace DragManager { const hideDragShowOriginalElements = (hide: boolean) => { dragLabel.style.display = hide ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); - eles.forEach(ele => (ele.hidden = hide)); + setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; options?.hideSource && hideDragShowOriginalElements(true); diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index e910a9118..297520d5d 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -1,30 +1,28 @@ -import { StrCast, Cast } from "../../fields/Types"; -import { SearchUtil } from "./SearchUtil"; -import { action, runInAction } from "mobx"; -import { Doc, Opt } from "../../fields/Doc"; -import { DocumentType } from "../documents/DocumentTypes"; -import { Docs } from "../documents/Documents"; -import { SelectionManager } from "./SelectionManager"; -import { WebField } from "../../fields/URLField"; -import { DocumentManager } from "./DocumentManager"; -import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton"; -import { simulateMouseClick, Utils } from "../../Utils"; -import { DocumentView } from "../views/nodes/DocumentView"; -import { Id } from "../../fields/FieldSymbols"; +import { StrCast, Cast } from '../../fields/Types'; +import { SearchUtil } from './SearchUtil'; +import { action, runInAction } from 'mobx'; +import { Doc, Opt } from '../../fields/Doc'; +import { DocumentType } from '../documents/DocumentTypes'; +import { Docs } from '../documents/Documents'; +import { SelectionManager } from './SelectionManager'; +import { WebField } from '../../fields/URLField'; +import { DocumentManager } from './DocumentManager'; +import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton'; +import { simulateMouseClick, Utils } from '../../Utils'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { Id } from '../../fields/FieldSymbols'; export namespace Hypothesis { - /** - * Retrieve a WebDocument with the given url, prioritizing results that are on screen. + * Retrieve a WebDocument with the given url, prioritizing results that are on screen. * If none exist, create and return a new WebDocument. */ export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); - console.log(result ? "existing doc found" : "existing doc NOT found"); + console.log(result ? 'existing doc found' : 'existing doc NOT found'); return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; - /** * Search for a WebDocument whose url field matches the given uri, return undefined if not found */ @@ -33,18 +31,18 @@ export namespace Hypothesis { if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise const results: Doc[] = []; - await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => { - const docs = res.docs; - const filteredDocs = docs.filter(doc => - doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data - ); - filteredDocs.forEach(doc => { - uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? - }); - })); + await SearchUtil.Search('web', true).then( + action(async (res: SearchUtil.DocSearchResult) => { + const docs = res.docs; + const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data); + filteredDocs.forEach(doc => { + uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? + }); + }) + ); const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc)); - return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen + return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen }; /** @@ -52,17 +50,19 @@ export namespace Hypothesis { */ export const linkListener = async (e: any) => { const annotationId: string = e.detail.id; - const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation + const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation const sourceDoc: Doc = await getSourceWebDoc(annotationUri); - if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself) + if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { + // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself) runInAction(() => { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; DocumentLinksButton.StartLink = sourceDoc; DocumentLinksButton.StartLinkView = undefined; }); - } else { // if a link has already been started, complete the link to sourceDoc + } else { + // if a link has already been started, complete the link to sourceDoc runInAction(() => { DocumentLinksButton.AnnotationId = annotationId; DocumentLinksButton.AnnotationUri = annotationUri; @@ -81,31 +81,41 @@ export namespace Hypothesis { export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => { // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done - !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc); + //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc); var success = false; const onSuccess = action(() => { - console.log("Edit success!!"); + console.log('Edit success!!'); success = true; clearTimeout(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - document.removeEventListener("editSuccess", onSuccess); + //DocumentLinksButton.invisibleWebDoc = undefined; + document.removeEventListener('editSuccess', onSuccess); }); const newHyperlink = `[${title}\n](${url})`; - const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful - !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", { - detail: { newHyperlink: newHyperlink, id: annotationId }, - bubbles: true - })), 300); - - setTimeout(action(() => { - if (!success) { - clearInterval(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - } - }), 10000); // give up if no success after 10s - document.addEventListener("editSuccess", onSuccess); + const interval = setInterval( + () => + // keep trying to edit until annotations have loaded and editing is successful + !success && + document.dispatchEvent( + new CustomEvent<{ newHyperlink: string; id: string }>('addLink', { + detail: { newHyperlink: newHyperlink, id: annotationId }, + bubbles: true, + }) + ), + 300 + ); + + setTimeout( + action(() => { + if (!success) { + clearInterval(interval); + //DocumentLinksButton.invisibleWebDoc = undefined; + } + }), + 10000 + ); // give up if no success after 10s + document.addEventListener('editSuccess', onSuccess); }; /** @@ -114,33 +124,40 @@ export namespace Hypothesis { export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => { if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation - !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink + //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink var success = false; const onSuccess = action(() => { - console.log("Edit success!"); + console.log('Edit success!'); success = true; clearTimeout(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - document.removeEventListener("editSuccess", onSuccess); + // DocumentLinksButton.invisibleWebDoc = undefined; + document.removeEventListener('editSuccess', onSuccess); }); const annotationId = StrCast(linkDoc.annotationId); 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 }, - bubbles: true - })); + 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 }, + bubbles: true, + }) + ); }, 300); - setTimeout(action(() => { - if (!success) { - clearInterval(interval); - DocumentLinksButton.invisibleWebDoc = undefined; - } - }), 10000); // give up if no success after 10s - document.addEventListener("editSuccess", onSuccess); + setTimeout( + action(() => { + if (!success) { + clearInterval(interval); + //DocumentLinksButton.invisibleWebDoc = undefined; + } + }), + 10000 + ); // give up if no success after 10s + document.addEventListener('editSuccess', onSuccess); }; /** @@ -149,17 +166,20 @@ export namespace Hypothesis { export const scrollToAnnotation = (annotationId: string, target: Doc) => { var success = false; const onSuccess = () => { - console.log("Scroll success!!"); + console.log('Scroll success!!'); document.removeEventListener('scrollSuccess', onSuccess); clearInterval(interval); success = true; }; - const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful - document.dispatchEvent(new CustomEvent('scrollToAnnotation', { - detail: annotationId, - bubbles: true - })); + const interval = setInterval(() => { + // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful + document.dispatchEvent( + new CustomEvent('scrollToAnnotation', { + detail: annotationId, + bubbles: true, + }) + ); const targetView: Opt = DocumentManager.Instance.getFirstDocumentView(target); const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false); @@ -168,4 +188,4 @@ export namespace Hypothesis { document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s }; -} \ No newline at end of file +} diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 4e0b061a6..fe1190e27 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -250,8 +250,8 @@ $resizeHandler: 8px; .documentDecorations-lock { position: absolute; background: black; - right: 11; - top: 30px; + right: 23px; + top: 3px; color: gray; height: 14; width: 14; @@ -260,6 +260,7 @@ $resizeHandler: 8px; display: flex; align-items: center; flex-direction: column; + border-radius: 15%; } .documentDecorations-rotationPath { diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 06eb6c6d7..8e0686038 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -782,6 +782,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + const useLock = bounds.r - bounds.x > 120; const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate? const rotation = NumCast(seldocview.rootDoc._rotation); @@ -856,9 +857,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onContextMenu={e => e.preventDefault()} /> )} -
e.preventDefault()}> - -
+ {!useLock ? null : ( +
e.preventDefault()}> + +
+ )} {hideDocumentButtonBar ? null : (
, document.getElementById('root')); + ReactDOM.createRoot(document.getElementById('root')!).render(); }, 0); })(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index c5ac1cf52..069206126 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -371,12 +371,3 @@ h1, width: 200px; height: 800px; } - -.mainVew-invisibleWebRef { - position: absolute; - left: 50; - top: 50; - display: block; - width: 500px; - height: 1000px; -} diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 052846e71..9648a7807 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -7,7 +7,7 @@ import { action, computed, configure, observable, reaction, runInAction } from ' import { observer } from 'mobx-react'; import 'normalize.css'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; import { DocCast, StrCast } from '../../fields/Types'; @@ -948,40 +948,6 @@ export class MainView extends React.Component { ); } - @computed get invisibleWebBox() { - // see note under the makeLink method in HypothesisUtils.ts - return !DocumentLinksButton.invisibleWebDoc ? null : ( -
- 500} - PanelHeight={() => 800} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> -
- ); - } - render() { return (
{this.snapLines} -
); } - - makeWebRef = (ele: HTMLDivElement) => { - reaction( - () => DocumentLinksButton.invisibleWebDoc, - invisibleDoc => { - ReactDOM.unmountComponentAtNode(ele); - invisibleDoc && - ReactDOM.render( - -
- 500} - PanelHeight={() => 800} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> -
- ; -
, - ele - ); - - let success = false; - const onSuccess = () => { - success = true; - clearTimeout(interval); - document.removeEventListener('editSuccess', onSuccess); - }; - - // For some reason, Hypothes.is annotations don't load until a click is registered on the page, - // so we keep simulating clicks until annotations have loaded and editing is successful - const interval = setInterval(() => !success && simulateMouseClick(ele, 50, 50, 50, 50), 500); - setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s - document.addEventListener('editSuccess', onSuccess); - } - ); - }; } ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) { diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index d9a989309..ce0e58d90 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -27,6 +27,7 @@ export interface MarqueeAnnotatorProps { mainCont: HTMLDivElement; docView: DocumentView; savedAnnotations: () => ObservableMap; + selectionText: () => string; annotationLayer: HTMLDivElement; addDocument: (doc: Doc) => boolean; getPageFromScroll?: (top: number) => number; @@ -147,7 +148,7 @@ export class MarqueeAnnotator extends React.Component { return marqueeAnno; } - const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title }); + const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title }); let minX = Number.MAX_VALUE; let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; @@ -271,7 +272,8 @@ export class MarqueeAnnotator extends React.Component { width: `${this._width}px`, height: `${this._height}px`, border: `${this._width === 0 ? '' : '2px dashed black'}`, - }}/> + }} + /> ); } } diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 12f41394d..df1eb72ce 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -15,6 +15,7 @@ import { SearchBox } from './search/SearchBox'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProvider'; import React = require('react'); +import { RichTextField } from '../../fields/RichTextField'; interface ExtraProps { fieldKey: string; @@ -72,6 +73,27 @@ export class SidebarAnnos extends React.Component { FormattedTextBox.DontSelectInitialText = true; this.allMetadata.map(tag => (target[tag] = tag)); DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on'); + + Doc.GetProto(target).text = new RichTextField( + JSON.stringify({ + doc: { + type: 'doc', + content: [ + { + type: 'paragraph', + attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }], + }, + { + type: 'paragraph', + attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null }, + }, + ], + }, + selection: { type: 'text', anchor: 4, head: 4 }, + }), + '' + ); this.addDocument(target); this._stackRef.current?.focusDocument(target, {}); return target; diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 869570a17..5abf4bde2 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -288,11 +288,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt 0 && - ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? ( + return doc && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
toggleLockedPosition(doc)}>
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index ac3541f2c..78e44dfa2 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -89,6 +89,10 @@ position: relative; } +.lm_stack { + position: relative; +} + .lm_drag_tab { padding: 0; width: 15px !important; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e9b41de25..92319d080 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -1,6 +1,6 @@ import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import * as GoldenLayout from '../../../client/goldenLayout'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -318,7 +318,7 @@ export class CollectionDockingView extends CollectionSubView() { } }; - componentDidMount: () => void = () => { + componentDidMount: () => void = async () => { if (this._containerRef.current) { this._lightboxReactionDisposer = reaction( () => LightboxView.LightboxDoc, @@ -335,8 +335,11 @@ export class CollectionDockingView extends CollectionSubView() { this._ignoreStateChange = ''; } ); - setTimeout(this.setupGoldenLayout); - //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window + reaction( + () => this.props.PanelWidth(), + width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows + { fireImmediately: true } + ); } }; @@ -460,7 +463,7 @@ export class CollectionDockingView extends CollectionSubView() { }; tabDestroyed = (tab: any) => { - if (![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { + if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); } @@ -470,8 +473,8 @@ export class CollectionDockingView extends CollectionSubView() { Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); this.tabMap.delete(tab); tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); - tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); - setTimeout(this.stateChanged); + //tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele)); + this.stateChanged(); } }; tabCreated = (tab: any) => { diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index cde5132a3..31ed5a83b 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -4,7 +4,7 @@ import { Tooltip } from '@material-ui/core'; import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -143,8 +143,8 @@ export class TabDocView extends React.Component { const docIcon = ; const closeIcon = ; - ReactDOM.render(docIcon, iconWrap); - ReactDOM.render(closeIcon, closeWrap); + ReactDOM.createRoot(iconWrap).render(docIcon); + ReactDOM.createRoot(closeWrap).render(closeIcon); tab.reactComponents = [iconWrap, closeWrap]; tab.element[0].prepend(iconWrap); tab._disposers.layerDisposer = reaction( diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 1d455ad08..627487a9e 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -40,9 +40,6 @@ export class DocumentLinksButton extends React.Component; - public static invisibleWebRef = React.createRef(); - @action @undoBatch onLinkButtonMoved = (e: PointerEvent) => { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index c7001f846..fcb3ccb07 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -74,7 +74,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent ); } - sidebarWidth = () => - !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); @@ -483,7 +486,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent
-
+
- !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth)); - + sidebarWidth = () => { + if (!this.SidebarShown) return 0; + if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) + const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); + return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); + }; @computed get content() { const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting; @@ -1000,6 +1003,7 @@ export class WebBox extends ViewBoxAnnotatableComponent{' '} @@ -1015,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent this.sidebarBtnDown(e, false)} /> -
+
() { // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}> // //
; - setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView + //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView return (
(this.colorPickerClosed = !this.colorPickerClosed))} + onClick={action(e => { + this.colorPickerClosed = !this.colorPickerClosed; + e.stopPropagation(); + })} onPointerDown={e => e.stopPropagation()}> {this.Icon(color)}
@@ -395,7 +397,7 @@ export class FontIconBox extends DocComponent() { {/* {dropdownCaret} */} {this.colorPickerClosed ? null : (
-
e.stopPropagation()} onClick={e => e.stopPropagation()}> +
e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}> {this.colorPicker(curColor)}
, this.dom); + this.root = ReactDOM.createRoot(this.dom); + this.root.render(); (this as any).dom = this.dom; } destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index 5f576be41..63ee7d1f3 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -1,7 +1,7 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; @@ -15,6 +15,7 @@ import React = require('react'); export class DashDocView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.dom = document.createElement('span'); @@ -38,14 +39,13 @@ export class DashDocView { e.stopPropagation(); }; - ReactDOM.render( -
}> , - ]; - - return this.getElement(buttons); + ]); } } diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx index 4895dcdc5..0fd2a7808 100644 --- a/src/client/views/nodes/formattedText/EquationView.tsx +++ b/src/client/views/nodes/formattedText/EquationView.tsx @@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react'; import { IReactionDisposer } from 'mobx'; import { observer } from 'mobx-react'; import { TextSelection } from 'prosemirror-state'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { StrCast } from '../../../../fields/Types'; import './DashFieldView.scss'; @@ -11,7 +11,7 @@ import React = require('react'); export class EquationView { dom: HTMLDivElement; // container for label and value - + root: any; constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) { this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; @@ -22,13 +22,13 @@ export class EquationView { e.stopPropagation(); }; - ReactDOM.render(, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(); } _editor: EquationEditor | undefined; setEditor = (editor?: EquationEditor) => (this._editor = editor); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); } setSelection() { this._editor?.mathField.focus(); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 096f9a92c..0e48dd93a 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1132,6 +1132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 47833dd43..dc5d8ada8 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -248,8 +248,7 @@ export class RichTextRules { // [[fieldKey:Doc]] => show field of doc new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { const fieldKey = match[1]; - const rawdocid = match[3]; - const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined; + const docid = match[3]?.replace(':', ''); const value = match[2]?.substring(1); if (!fieldKey) { if (docid) { @@ -259,7 +258,7 @@ export class RichTextRules { if (rstate) { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid); + const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid); DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined); const fstate = this.TextBox.EditorView?.state; @@ -275,7 +274,7 @@ export class RichTextRules { const num = value.match(/^[0-9.]$/); this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid }); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: true }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); }), diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx index 01acc3de9..1fe6d822b 100644 --- a/src/client/views/nodes/formattedText/SummaryView.tsx +++ b/src/client/views/nodes/formattedText/SummaryView.tsx @@ -1,6 +1,6 @@ import { TextSelection } from 'prosemirror-state'; import { Fragment, Node, Slice } from 'prosemirror-model'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import React = require('react'); // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. @@ -9,6 +9,7 @@ import React = require('react'); // method instead of changing prosemirror's text when the expand/elide buttons are clicked. export class SummaryView { dom: HTMLSpanElement; // container for label and value + root: any; constructor(node: any, view: any, getPos: any) { const self = this; @@ -35,13 +36,13 @@ export class SummaryView { return js.apply(this, arguments); }; - ReactDOM.render(, this.dom); - (this as any).dom = this.dom; + this.root = ReactDOM.createRoot(this.dom); + this.root.render(); } className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed'); destroy() { - ReactDOM.unmountComponentAtNode(this.dom); + // ReactDOM.unmountComponentAtNode(this.dom); } selectNode() {} diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 66d747bf7..aa2475dca 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -263,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = { fieldKey: { default: '' }, docid: { default: '' }, hideKey: { default: false }, + editable: { default: true }, }, group: 'inline', draggable: false, diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 822af6a68..470aa3eb1 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -27,14 +27,14 @@ opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! - span { - padding-right: 5px; - padding-bottom: 4px; - } + // span { + // padding-right: 5px; + // padding-bottom: 4px; + // } } .textLayer ::selection { - background: #ACCEF7; + background: #accef7; } // should match the backgroundColor in createAnnotation() @@ -111,4 +111,4 @@ .pdfViewerDash-interactive { pointer-events: all; -} \ No newline at end of file +} diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 5c10c7cef..abc7336bd 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -79,6 +79,8 @@ export class PDFViewer extends React.Component { private _initialScroll: Opt; private _forcedScroll = true; + selectionText = () => this._selectionText; + @observable isAnnotating = false; // key where data is stored @computed get allAnnotations() { @@ -358,7 +360,8 @@ export class PDFViewer extends React.Component { MarqueeAnnotator.clearAnnotations(this._savedAnnotations); this._marqueeing = [e.clientX, e.clientY]; this.isAnnotating = true; - if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).parentElement.className !== 'textLayer')) { + const target = e.target as any; + if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) { this._textSelecting = false; document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called } else { @@ -574,6 +577,7 @@ export class PDFViewer extends React.Component { docView={this.props.docViewPath().lastElement()} finishMarquee={this.finishMarquee} savedAnnotations={this.savedAnnotations} + selectionText={this.selectionText} annotationLayer={this._annotationLayer.current} mainCont={this._mainCont.current} anchorMenuCrop={this._textSelecting ? undefined : this.crop} diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx index 1b12e208c..b8081648f 100644 --- a/src/debug/Repl.tsx +++ b/src/debug/Repl.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import * as ReactDOM from 'react-dom/client'; import { observer } from 'mobx-react'; import { observable, computed } from 'mobx'; import { CompileScript } from '../client/util/Scripting'; @@ -11,39 +11,40 @@ import { resolvedPorts } from '../client/util/CurrentUserUtils'; @observer class Repl extends React.Component { - @observable text: string = ""; + @observable text: string = ''; - @observable executedCommands: { command: string, result: any }[] = []; + @observable executedCommands: { command: string; result: any }[] = []; onChange = (e: React.ChangeEvent) => { this.text = e.target.value; - } + }; onKeyDown = (e: React.KeyboardEvent) => { - if (!e.ctrlKey && e.key === "Enter") { + if (!e.ctrlKey && e.key === 'Enter') { e.preventDefault(); const script = CompileScript(this.text, { - addReturn: true, typecheck: false, - params: { makeInterface: "any" } + addReturn: true, + typecheck: false, + params: { makeInterface: 'any' }, }); if (!script.compiled) { - this.executedCommands.push({ command: this.text, result: "Compile Error" }); + this.executedCommands.push({ command: this.text, result: 'Compile Error' }); } else { const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e })); result.success && this.executedCommands.push({ command: this.text, result: result.result }); } - this.text = ""; + this.text = ''; } - } + }; @computed get commands() { return this.executedCommands.map(command => { return ( -
+

{command.command}

{/*
{JSON.stringify(command.result, null, 2)}
*/} -
{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}
+
{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}
); }); @@ -52,16 +53,14 @@ class Repl extends React.Component { render() { return (
-
- {this.commands} -
-