diff options
48 files changed, 525 insertions, 293 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index bfb29fe8d..ca1432de2 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -448,7 +448,7 @@ export function returnEmptyDoclist() { return [] as any[]; } export let emptyPath = []; -export function emptyFunction() { } +export function emptyFunction() { return undefined; } export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); } diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index da5c8efa9..219f51a3a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -94,6 +94,7 @@ type DROPt = DAInfo | dropActionType; export class DocumentOptions { system?: BOOLt = new BoolInfo("is this a system created/owned doc"); _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); + allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?"); childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection "); targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? "); color?: string; // foreground color data doc @@ -289,6 +290,7 @@ export class DocumentOptions { treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked treeViewTruncateTitleWidth?: number; + treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy sidebarColor?: string; // background color of text sidebar sidebarViewType?: string; // collection type of text sidebar @@ -821,7 +823,7 @@ export namespace Docs { } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 656ba61e8..b9e62b303 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -339,7 +339,7 @@ export class CurrentUserUtils { const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc); if (doc.templateDocs === undefined) { doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], { - title: "template layouts", _xPadding: 0, system: true, + title: "template layouts", _xMargin: 0, system: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }) })); } @@ -421,7 +421,11 @@ export class CurrentUserUtils { ((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0; } if (doc.emptySlide === undefined) { - const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) }); + const textDoc = Docs.Create.TreeDocument([], { + title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, + allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, + backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) + }); Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text'); FormattedTextBox.SelectOnLoad = textDoc[Id]; doc.emptySlide = textDoc; @@ -811,7 +815,7 @@ export class CurrentUserUtils { const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add", - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true, explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." @@ -840,7 +844,7 @@ export class CurrentUserUtils { const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true }); doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "My Trails", _showTitle: "title", _height: 100, - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, explainer: "All of the trails that you have created will appear here." @@ -865,7 +869,7 @@ export class CurrentUserUtils { }); doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], { title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100, - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true, @@ -884,7 +888,7 @@ export class CurrentUserUtils { const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true }); doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true, - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias", + treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias", treeViewTruncateTitleWidth: 150, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." @@ -902,7 +906,7 @@ export class CurrentUserUtils { if (doc.currentFilter === undefined) { doc.currentFilter = Docs.Create.FilterDocument({ title: "Unnamed Filter", _height: 150, - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none", + treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none", treeViewTruncateTitleWidth: 150, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true }); @@ -918,7 +922,7 @@ export class CurrentUserUtils { doc.treeViewOpen = true; doc.treeViewExpandedView = "fields"; doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], { - treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title", + treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title", treeViewTruncateTitleWidth: 150, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true })) as any as Doc; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f5704d2bf..ae3fa3170 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -335,8 +335,10 @@ export namespace DragManager { } export let docsBeingDragged: Doc[] = []; export let CanEmbed = false; + export let DocDragData: DocumentDragData | undefined; export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { if (dragData.dropAction === "none") return; + DocDragData = dragData instanceof DocumentDragData ? dragData : undefined; const batch = UndoManager.StartBatch("dragging"); eles = eles.filter(e => e); CanEmbed = dragData.canEmbed || false; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index e507ec3bf..2cba2c1f2 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -27,7 +27,7 @@ export namespace SelectionManager { manager.SelectedViews.set(docView, docView.rootDoc); docView.props.whenChildContentsActiveChanged(true); - } else if (!ctrlPressed && Array.from(manager.SelectedViews.entries()).length > 1) { + } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) { Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false)); manager.SelectedSchemaDocument = undefined; manager.SelectedViews.clear(); @@ -57,7 +57,8 @@ export namespace SelectionManager { export function SelectView(docView: DocumentView, ctrlPressed: boolean): void { manager.SelectView(docView, ctrlPressed); } - export function SelectSchemaViewDoc(document: Opt<Doc>): void { + export function SelectSchemaViewDoc(document: Opt<Doc>, deselectAllFirst?: boolean): void { + if (deselectAllFirst) manager.DeselectAll(); manager.SelectSchemaViewDoc(document); } diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index 32c351bf5..b9772fd57 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -44,7 +44,7 @@ interface ViewBoxBaseProps { fieldKey: string; layerProvider?: (doc: Doc, assign?: boolean) => boolean; isSelected: (outsideReaction?: boolean) => boolean; - isContentActive: () => boolean; + isContentActive: () => boolean | undefined; renderDepth: number; rootSelected: (outsideReaction?: boolean) => boolean; } @@ -65,10 +65,12 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor: lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result; - isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None || - (this.props.isContentActive?.() || this.props.Document.forceActive || - this.props.isSelected(outsideReaction) || - this.props.rootSelected(outsideReaction)) ? true : false) + isContentActive = (outsideReaction?: boolean) => ( + this.props.isContentActive?.() === false ? false : + (CurrentUserUtils.SelectedTool !== InkTool.None || + (this.props.isContentActive?.() || this.props.Document.forceActive || + this.props.isSelected(outsideReaction) || + this.props.rootSelected(outsideReaction)) ? true : undefined)) protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; } return Component; @@ -82,7 +84,7 @@ export interface ViewBoxAnnotatableProps { fieldKey: string; filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) layerProvider?: (doc: Doc, assign?: boolean) => boolean; - isContentActive: () => boolean; + isContentActive: () => boolean | undefined; select: (isCtrlPressed: boolean) => void; whenChildContentsActiveChanged: (isActive: boolean) => void; isSelected: (outsideReaction?: boolean) => boolean; @@ -165,13 +167,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T // otherwise, the document being moved must be able to be removed from its container before // moving it into the target. @action.bound - moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean => { + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => { if (Doc.AreProtosEqual(this.props.Document, targetCollection)) { return true; } const first = doc instanceof Doc ? doc : doc[0]; if (!first?._stayInCollection && addDocument !== returnFalse) { - return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc)); + return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey)); } return false; } diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index d8ad47ecb..82dca1287 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -240,8 +240,10 @@ $linkGap: 3px; text-align: center; display: flex; margin-left: 5px; - height: 22px; + height: 20px; position: absolute; + border-radius: 8px; + background: rgba(159,159,159,0.1); .documentDecorations-titleSpan, .documentDecorations-titleSpan-Dark { @@ -288,7 +290,7 @@ $linkGap: 3px; text-align: center; display: flex; margin-left: 5px; - height: 22px; + height: 20px; position: absolute; .documentDecorations-titleSpan { width: 100%; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 522995479..1c0b1b995 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -454,17 +454,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P </Tooltip>); const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); - const titleArea = this._edtingTitle ? - <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`} - style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} - type="text" name="dynbox" autoComplete="on" - value={this._accumulatedTitle} - onBlur={e => this.titleBlur()} - onChange={action(e => this._accumulatedTitle = e.target.value)} - onKeyPress={this.titleEntered} /> : - <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} > - <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span> - </div>; + const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> : + this._edtingTitle ? + <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`} + style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} + type="text" name="dynbox" autoComplete="on" + value={this._accumulatedTitle} + onBlur={e => this.titleBlur()} + onChange={action(e => this._accumulatedTitle = e.target.value)} + onKeyPress={this.titleEntered} /> : + <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} > + <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span> + </div>; let inMainMenuPanel = false; for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) { @@ -498,8 +499,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")} - {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} - + {titleArea} + {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} {hideResizers ? (null) : <> {SelectionManager.Views().length !== 1 || hideTitle ? (null) : diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 607137c2f..f24dab949 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -43,7 +43,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { */ @action onControlDown = (e: React.PointerEvent, controlIndex: number): void => { - const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen + const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen; if (InkStrokeProperties.Instance && ptFromScreen) { const order = controlIndex % 4; const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length; diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss index 55e06c6ca..2127826b4 100644 --- a/src/client/views/InkStroke.scss +++ b/src/client/views/InkStroke.scss @@ -13,16 +13,25 @@ } } -.inkStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; - width: 100%; - height: 100%; +.inkStroke-wrapper { + .inkStroke { + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; + width: 100%; + height: 100%; - svg:not(:root) { - overflow: visible !important; - } + svg:not(:root) { + overflow: visible !important; + } + } + + .inkStroke-text { + position: absolute; + &:hover { + background: #9f9f9f0a; + } + } } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index ecb46a5b3..59efb36dd 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,13 +1,13 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc } from "../../fields/Doc"; +import { Doc, HeightSym, WidthSym } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils"; +import { emptyFunction, returnFalse, setupMoveUpEvents, OmitKeys } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; @@ -22,6 +22,7 @@ import { InkTangentHandles } from "./InkTangentHandles"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; import Color = require("color"); import { Transform } from "../util/Transform"; +import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -246,15 +247,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex]; // Invisible polygonal line that enables the ink to be selected by the user. const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, - inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15), + inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15), StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false); // Set of points rendered upon the ink that can be added if a user clicks on one. - return ( + return <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}> <svg className="inkStroke" style={{ + width: "100%", + height: "100%", pointerEvents: "none", transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset", @@ -275,7 +278,26 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume {clickableLine} {inkLine} </svg> - ); + {!closed ? (null) : + <div className="inkStroke-text" style={{ + color: StrCast(this.layoutDoc.textColor, "black"), + pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined, + width: this.layoutDoc[WidthSym](), + }}> + <FormattedTextBox + {...OmitKeys(this.props, ['children']).omit} + yPadding={10} + xPadding={10} + fieldKey={"text"} + fontSize={12} + dontRegisterView={true} + noSidebar={true} + dontScale={true} + isContentActive={this.isContentActive} + /> + </div> + } + </div>; } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 9a885fbf8..546b0e360 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -511,7 +511,7 @@ export class MainView extends React.Component { bringToFront={emptyFunction} select={emptyFunction} isAnyChildContentActive={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} isSelected={returnFalse} docViewPath={returnEmptyDoclist} moveDocument={this.moveButtonDoc} @@ -592,7 +592,7 @@ export class MainView extends React.Component { pinToPres={returnFalse} ScreenToLocalTransform={Transform.Identity} bringToFront={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} whenChildContentsActiveChanged={returnFalse} focus={returnFalse} docViewPath={returnEmptyDoclist} @@ -669,7 +669,7 @@ export class MainView extends React.Component { pinToPres={returnFalse} ScreenToLocalTransform={Transform.Identity} bringToFront={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} whenChildContentsActiveChanged={returnFalse} focus={returnFalse} PanelWidth={() => 500} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index af04b967a..7cf388872 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -191,7 +191,7 @@ export class OverlayView extends React.Component { ScreenToLocalTransform={Transform.Identity} renderDepth={1} isDocumentActive={returnTrue} - isContentActive={returnFalse} + isContentActive={emptyFunction} whenChildContentsActiveChanged={emptyFunction} focus={DocUtils.DefaultFocus} styleProvider={DefaultStyleProvider} diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 86ab881bb..529697f71 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -50,7 +50,7 @@ export default class Palette extends React.Component<PaletteProps> { PanelHeight={() => window.screen.height} renderDepth={0} isDocumentActive={returnTrue} - isContentActive={returnFalse} + isContentActive={emptyFunction} focus={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={returnEmptyString} diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index ab9022a84..1083e0075 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { freezeDimensions={true} dontCenter={"y"} isDocumentActive={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined} NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined} PanelWidth={panelWidth} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 3f120d385..8d8630907 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -46,6 +46,7 @@ export enum StyleProp { JitterRotation = "jitterRotation", // whether documents should be randomly rotated BorderPath = "customBorder", // border path for document view FontSize = "fontSize", // size of text font + FontFamily = "fontFamily", // size of text font } function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; } @@ -91,7 +92,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey"; case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null)); case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton)); - case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"]); + case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize))); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.["fontFamily"], StrCast(Doc.UserDoc().fontFamily))); case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc && StrCast(doc._showTitle, props?.showTitle?.() || diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 5325d5827..f543d924d 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -44,6 +44,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { } private _reactionDisposer?: IReactionDisposer; + private _lightboxReactionDisposer?: IReactionDisposer; private _containerRef = React.createRef<HTMLDivElement>(); private _flush: UndoManager.Batch | undefined; private _ignoreStateChange = ""; @@ -298,6 +299,7 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { componentDidMount: () => void = () => { if (this._containerRef.current) { + this._lightboxReactionDisposer = reaction(() => LightboxView.LightboxDoc, doc => setTimeout(() => !doc && this.onResize(undefined))); new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); this._reactionDisposer = reaction(() => StrCast(this.props.Document.dockingConfig), config => { @@ -320,13 +322,14 @@ export class CollectionDockingView extends CollectionSubView(doc => doc) { window.removeEventListener('resize', this.onResize); this._reactionDisposer?.(); + this._lightboxReactionDisposer?.(); } @action onResize = (event: any) => { const cur = this._containerRef.current; // bcz: since GoldenLayout isn't a React component itself, we need to notify it to resize when its document container's size has changed - cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); + !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); } @action diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 648ff5087..bffaf86b1 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -226,7 +226,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument, layerProvider={this.props.layerProvider} docViewPath={this.props.docViewPath} fitWidth={this.props.childFitWidth} - isContentActive={returnFalse} + isContentActive={emptyFunction} isDocumentActive={this.isContentActive} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5dffc65fc..fc1bcb8b9 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -22,7 +22,6 @@ import ReactLoading from 'react-loading'; export interface SubCollectionViewProps extends CollectionViewProps { CollectionView: Opt<CollectionView>; - SetSubView?: (subView: any) => void; isAnyChildContentActive: () => boolean; } @@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: this.createDashEventsTarget(ele); } - componentDidMount() { - this.props.SetSubView?.(this); - } - componentWillUnmount() { this.gestureDisposer?.(); this._multiTouchDisposer?.(); @@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { - const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay || + const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); } else { diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index d370d21ab..b664d9d82 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -71,6 +71,15 @@ display: none; } +.collectionTreeView-titleBar { + display: inline-block; + width: 100%; + height: max-content; + .contentFittingDocumentView { + display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason) + } +} + .collectionTreeView-keyHeader:hover { background: #797777; cursor: pointer; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 3852987b9..ea077ea40 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,4 +1,3 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; @@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from "../../util/DragManager"; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; +import { Transform } from '../../util/Transform'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; @@ -22,11 +22,11 @@ import { EditableView } from "../EditableView"; import { DocumentView } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProvider'; +import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView } from "./CollectionSubView"; import "./CollectionTreeView.scss"; import { TreeView } from "./TreeView"; import React = require("react"); -import { Transform } from '../../util/Transform'; const _global = (window /* browser */ || global /* node */) as any; export type collectionTreeViewProps = { @@ -41,10 +41,14 @@ export type collectionTreeViewProps = { @observer export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) { - private treedropDisposer?: DragManager.DragDropDisposer; + private _treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; + private _titleRef?: HTMLDivElement | HTMLInputElement | null; private _disposers: { [name: string]: IReactionDisposer } = {}; - MainEle = () => this._mainEle; + private _isDisposing = false; // notes that instance is in process of being disposed + private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes + private observer: any; // observer for monitoring tree view items. + private static expandViewLabelSize = 20; @computed get doc() { return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } @@ -54,6 +58,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll @computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; } @computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; } + @observable _explainerHeight = 0; // height of the description of the tree view + + MainEle = () => this._mainEle; + // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive)); @@ -62,11 +70,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction)) ? true : false) - isDisposing = false; componentWillUnmount() { - this.isDisposing = true; + this._isDisposing = true; super.componentWillUnmount(); - this.treedropDisposer?.(); + this._treedropDisposer?.(); Object.values(this._disposers).forEach(disposer => disposer?.()); } @@ -76,13 +83,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll { fireImmediately: true }); } - refList: Set<any> = new Set(); - observer: any; computeHeight = () => { - if (this.isDisposing) return; - const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot()); - this.layoutDoc._autoHeightMargins = bodyHeight; - this.props.setHeight(this.documentTitleHeight() + bodyHeight); + if (!this._isDisposing) { + const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", "")); + const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot()); + this.layoutDoc._autoHeightMargins = bodyHeight; + this.props.setHeight(bodyHeight + titleHeight); + } } unobserveHeight = (ref: any) => { this.refList.delete(ref); @@ -101,8 +108,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll } } protected createTreeDropTarget = (ele: HTMLDivElement) => { - this.treedropDisposer?.(); - if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); + this._treedropDisposer?.(); + if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); } protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { @@ -165,60 +172,44 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true); } - editableTitle = (childDocs: Doc[]) => { - return !this.dataDoc ? (null) : - <EditableView - contents={this.dataDoc.title} - display={"block"} - maxHeight={72} - height={"auto"} - GetValue={() => StrCast(this.dataDoc.title)} - SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { - if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs); - this.dataDoc.title = value; - return true; - })} />; + get editableTitle() { + return <EditableView + contents={this.dataDoc.title} + display={"block"} + maxHeight={72} + height={"auto"} + GetValue={() => StrCast(this.dataDoc.title)} + SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { + if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren); + this.dataDoc.title = value; + return true; + })} />; } - documentTitle = (childDocs: Doc[]) => { - return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]} - onKeyDown={e => { - e.stopPropagation(); - e.key === "Enter" && this.makeTextCollection(childDocs); - }}> - <DocumentView - Document={this.doc} - DataDoc={undefined} - LayoutTemplateString={FormattedTextBox.LayoutString("text")} - renderDepth={this.props.renderDepth + 1} - isContentActive={this.isContentActive} - isDocumentActive={this.isContentActive} - rootSelected={returnTrue} - docViewPath={this.props.docViewPath} - styleProvider={this.props.styleProvider} - layerProvider={this.props.layerProvider} - PanelWidth={this.documentTitleWidth} - PanelHeight={this.documentTitleHeight} - NativeWidth={this.documentTitleWidth} - NativeHeight={this.documentTitleHeight} - focus={this.props.focus} - treeViewDoc={this.props.Document} - ScreenToLocalTransform={this.titleTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - ContainingCollectionDoc={this.doc} - ContainingCollectionView={this.props.CollectionView} - addDocument={this.props.addDocument} - moveDocument={returnFalse} - removeDocument={returnFalse} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - addDocTab={this.props.addDocTab} - pinToPres={this.props.pinToPres} - bringToFront={returnFalse} - /> - </div>; + get documentTitle() { + return <FormattedTextBox + {...this.props} + fieldKey={"text"} + renderDepth={this.props.renderDepth + 1} + isContentActive={this.isContentActive} + isDocumentActive={this.isContentActive} + rootSelected={returnTrue} + forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight + PanelWidth={this.documentTitleWidth} + PanelHeight={this.documentTitleHeight} + scaling={returnOne} + docFilters={returnEmptyFilter} + docRangeFilters={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + ContainingCollectionDoc={this.doc} + ContainingCollectionView={this.props.CollectionView} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + bringToFront={returnFalse} + />; } childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); @@ -263,21 +254,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll ); } @computed get titleBar() { - const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle; - return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren); + return this.dataDoc === null ? (null) : + <div className="collectionTreeView-titleBar" key={this.doc[Id]} + style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}} + ref={r => this._titleRef = r} + onKeyDown={e => { + if (this.outlineMode) { + e.stopPropagation(); + e.key === "Enter" && this.makeTextCollection(this.treeChildren); + } + }}> + {this.outlineMode ? this.documentTitle : this.editableTitle} + </div>; + } + + @computed get noviceExplainer() { + return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) : + <div className="documentExplanation"> {this.rootDoc.explainer} </div>; } return35 = () => 35; @computed get buttonMenu() { - const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); // To create a multibutton menu add a CollectionLinearView - if (menuDoc) { - - const width: number = NumCast(menuDoc._width, 30); - const height: number = NumCast(menuDoc._height, 30); - console.log(menuDoc.title, width, height); - return (<div className="buttonMenu-docBtn" - style={{ width: width, height: height }}> + return !menuDoc ? null : + (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}> <DocumentView Document={menuDoc} DataDoc={menuDoc} @@ -306,11 +307,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll ContainingCollectionDoc={undefined} /> </div>); - } } - @observable _explainerHeight: number = 0; - @computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); } @computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); } @@ -321,47 +319,81 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - paddingX = () => NumCast(this.doc._xPadding, 15); - paddingTop = () => NumCast(this.doc._yPadding, 20); - paddingBot = () => NumCast(this.doc._yPadding, 20); + marginX = () => NumCast(this.doc._xMargin); + marginTop = () => NumCast(this.doc._yMargin); + marginBot = () => NumCast(this.doc._yMargin); documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); - titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1); - render() { - TraceMobx(); + panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1); + + addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; + remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; + moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => + this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false + + contentFunc = () => { const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined; - const buttonMenu = this.rootDoc.buttonMenu; - const noviceExplainer = this.rootDoc.explainer; - - return !(this.doc instanceof Doc) || !this.treeChildren ? (null) : - <> - {this.titleBar} + const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar; + return [ + <div className="collectionTreeView-contents" key="tree" style={{ + ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}), + overflow: "auto", + height: this.layoutDoc._autoHeight ? "max-content" : "100%" + }} > + {titleBar} <div className="collectionTreeView-container" - style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}} + style={{ + transform: this.outlineMode ? `scale(${this.contentScaling})` : "", + paddingLeft: `${this.marginX()}px`, + height: "max-content", + width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : "" + }} onContextMenu={this.onContextMenu}> - {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}> - {buttonMenu ? this.buttonMenu : null} - {Doc.UserDoc().noviceMode && noviceExplainer ? - <div className="documentExplanation"> - {noviceExplainer} - </div> - : null - } - </div> : null} + {!this.buttonMenu && !this.noviceExplainer ? (null) : + <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}> + {this.buttonMenu} + {this.noviceExplainer} + </div> + } <div className="collectionTreeView-dropTarget" - style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }} + style={{ + background: background(), + height: `calc(100% - ${this._explainerHeight}px)`, + pointerEvents: pointerEvents() + }} onWheel={e => e.stopPropagation()} onDrop={this.onTreeDrop} - ref={this.createTreeDropTarget}> + ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}> <ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} > {this.treeViewElements} </ul> </div > </div> - </>; + </div> + ]; + } + render() { + TraceMobx(); + + return !(this.doc instanceof Doc) || !this.treeChildren ? (null) : + this.doc.treeViewHasOverlay ? + <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} + isAnnotationOverlay={true} + isAnnotationOverlayScrollable={true} + childDocumentsActive={this.props.isDocumentActive} + fieldKey={this.props.fieldKey + "-annotations"} + dropAction={"move"} + select={emptyFunction} + addDocument={this.addAnnotationDocument} + removeDocument={this.remAnnotationDocument} + moveDocument={this.moveAnnotationDocument} + bringToFront={emptyFunction} + renderDepth={this.props.renderDepth + 1} > + {this.contentFunc} + </CollectionFreeFormView> : + this.contentFunc(); } }
\ No newline at end of file diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 38e027fb3..8e84b59de 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -63,6 +63,7 @@ export enum CollectionViewType { } export interface CollectionViewProps extends FieldViewProps { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) + isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently) layoutEngine?: () => string; setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; @@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()); - private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => { + private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => { TraceMobx(); + if (type === undefined) return null; switch (type) { default: case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />; @@ -246,17 +248,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); @computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); } - - @observable _subView: any = undefined; - isContentActive = (outsideReaction?: boolean) => { - return this.props.isContentActive() ? true : false; + return this.props.isContentActive(); } render() { TraceMobx(); const props: SubCollectionViewProps = { ...this.props, - SetSubView: action((subView: any) => this._subView = subView), addDocument: this.addDocument, moveDocument: this.moveDocument, removeDocument: this.removeDocument, @@ -273,7 +271,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab return (<div className={"collectionView"} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}> {this.showIsTagged()} - {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)} + {this.renderSubView(this.collectionViewType, props)} </div>); } } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index eb95bb913..5ba019698 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> <CollectionFreeFormView Document={this.props.document} - SetSubView={() => this} CollectionView={undefined} ContainingCollectionView={undefined} ContainingCollectionDoc={undefined} @@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. noOverlay={true} // don't render overlay Docs since they won't scale setHeight={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} isAnyChildContentActive={returnFalse} select={emptyFunction} dropAction={undefined} diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 1ebc5873e..2e33d3564 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -53,14 +53,11 @@ } } +.treeView-container-outline-active .treeView-container-active { z-index: 100; position: relative; - - .formattedTextbox-sidebar { - background-color: #ffff001f !important; - height: 500px !important; - } + pointer-events: all; } .treeView-openRight { diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 7f2128230..d8f984601 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> { static _openLevelScript: Opt<ScriptField | undefined>; private _header: React.RefObject<HTMLDivElement> = React.createRef(); private _tref = React.createRef<HTMLDivElement>(); - private _docRef: Opt<DocumentView>; + @observable _docRef: Opt<DocumentView>; private _selDisposer: Opt<IReactionDisposer>; private _editTitleScript: (() => ScriptField) | undefined; private _openScript: (() => ScriptField) | undefined; @@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get childLinks() { return this.childDocList("links"); } @computed get childAliases() { return this.childDocList("aliases"); } @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } - @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; } + @computed get selected() { return SelectionManager.IsSelected(this._docRef); } + // SelectionManager.Views().lastElement()?.props.Document === this.props.document; } childDocList(field: string) { const layout = Cast(Doc.LayoutField(this.doc), Doc, null); @@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> { DocListCastOrNull(this.doc[field]); // otherwise use the document's data field } @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc); + if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse + if (this.props.removeDoc?.(doc) === true) { + return addDoc(doc); + } + } + return false; } @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { this.props.treeView.props.select(false); @@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> { this._editTitle = false; } else if (docView.isSelected()) { + const doc = docView.Document; + SelectionManager.SelectSchemaViewDoc(doc); this._editTitle = true; - this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined)); + this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined)); } else { docView.select(false); } @@ -213,16 +221,18 @@ export class TreeView extends React.Component<TreeViewProps> { const before = pt[1] < rect.top + rect.height / 2; const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); this._header.current!.className = "treeView-header"; - if (inside) this._header.current!.className += " treeView-header-inside"; - else if (before) this._header.current!.className += " treeView-header-above"; - else if (!before) this._header.current!.className += " treeView-header-below"; + if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) { + if (inside) this._header.current!.className += " treeView-header-inside"; + else if (before) this._header.current!.className += " treeView-header-above"; + else if (!before) this._header.current!.className += " treeView-header-below"; + } e.stopPropagation(); } public static makeTextBullet() { const bullet = Docs.Create.TextDocument("-text-", { layout: CollectionView.LayoutString("data"), - title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, + title: "-title-", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline", x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10 @@ -266,23 +276,25 @@ export class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); } const docDragData = de.complete.docDragData; - if (docDragData) { - e.stopPropagation(); + if (docDragData && pt[0] < rect.left + rect.width) { if (docDragData.draggedDocuments[0] === this.doc) return true; - this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document); + if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { + e.stopPropagation(); + } } } dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) { const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd; + const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd; const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false; const addDoc = !inside ? parentAddDoc : (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean); const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument; if (canAdd) { - UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); + return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); } + return false; } refTransform = (ref: HTMLDivElement | undefined | null) => { @@ -432,7 +444,7 @@ export class TreeView extends React.Component<TreeViewProps> { </div> </ul>; } - return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout" + return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout" } get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @@ -581,6 +593,7 @@ export class TreeView extends React.Component<TreeViewProps> { } titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth())); + return18 = () => 18; /** * Renders the EditableView title element for placement into the tree. */ @@ -636,10 +649,10 @@ export class TreeView extends React.Component<TreeViewProps> { moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} - NativeHeight={() => 18} + NativeHeight={this.return18} NativeWidth={this.titleWidth} PanelWidth={this.titleWidth} - PanelHeight={() => 18} + PanelHeight={this.return18} contextMenuItems={this.contextMenuItems} renderDepth={1} isContentActive={this.props.isContentActive} @@ -679,6 +692,7 @@ export class TreeView extends React.Component<TreeViewProps> { renderBulletHeader = (contents: JSX.Element, editing: boolean) => { return <> <div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader" + style={{ width: "max-content" }} ref={this._header} onClick={this.ignoreEvent} onPointerDown={this.ignoreEvent} @@ -691,7 +705,7 @@ export class TreeView extends React.Component<TreeViewProps> { } - renderEmbeddedDocument = (asText: boolean) => { + renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => { const layout = StrCast(Doc.LayoutField(this.layoutDoc)); const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name); const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth; @@ -704,8 +718,8 @@ export class TreeView extends React.Component<TreeViewProps> { NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined} - isContentActive={asText ? this.props.isContentActive : returnFalse} - isDocumentActive={asText ? this.props.isContentActive : returnFalse} + isContentActive={isActive} + isDocumentActive={isActive} styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} hideTitle={asText} fitContentsToDoc={returnTrue} @@ -749,7 +763,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderDocumentAsHeader() { return <> {this.renderBullet} - {this.renderEmbeddedDocument(true)} + {this.renderEmbeddedDocument(true, this.props.isContentActive)} </>; } @@ -776,13 +790,12 @@ export class TreeView extends React.Component<TreeViewProps> { return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles <div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`} ref={this.createTreeDropTarget} - onDrop={this.onTreeDrop} //onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document onKeyDown={this.onKeyDown}> <li className="collection-child"> {hideTitle && this.doc.type !== DocumentType.RTF ? - this.renderEmbeddedDocument(false) : + this.renderEmbeddedDocument(false, returnFalse) : this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)} </li> </div>; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index bb4cae8c6..9cc887e3d 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -5,9 +5,8 @@ import { Id } from "../../../../fields/FieldSymbols"; import { List } from "../../../../fields/List"; import { NumCast } from "../../../../fields/Types"; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { CurrentUserUtils } from "../../../util/CurrentUserUtils"; import { LinkManager } from "../../../util/LinkManager"; -import { ColorScheme } from "../../../util/SettingsManager"; +import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from "../../../util/SnappingManager"; import { DocumentView } from "../../nodes/DocumentView"; import "./CollectionFreeFormLinkView.scss"; @@ -154,28 +153,44 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const atop = this.visibleY(adiv); const btop = this.visibleY(bdiv); if (!a.width || !b.width) return undefined; + const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; + const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 }; + const acentX = (a.left + a.right) / 2; + const acentY = (a.top + a.bottom) / 2; + const bcentX = (b.left + b.right) / 2; + const bcentY = (b.top + b.bottom) / 2; + const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) || + ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1); + const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) || + ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1); const atop2 = this.visibleY(adiv); const btop2 = this.visibleY(bdiv); const aleft = this.visibleX(adiv); const bleft = this.visibleX(bdiv); const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top; - const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y); - const pt1 = [apt.point.x, apt.point.y]; - const pt2 = [bpt.point.x, bpt.point.y]; - const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)]; - const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)]; + const pt1 = [aleft + a.width / 2, atop + a.height / 2]; + const pt2 = [bleft + b.width / 2, btop + b.width / 2]; + const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2]; + const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2]; const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1])); const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1])); const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; - const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen]; - const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen]; + const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] : + Math.abs(acentY - aDocBounds.top) < 0.01 || + Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0]; + const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] : + Math.abs(bcentY - bDocBounds.top) < 0.01 || + Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0]; + const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1; + const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1; + const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen]; + const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen]; const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc); const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc); const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX); const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY); - return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 }; + return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] }; } render() { @@ -199,7 +214,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const strokeWidth = linkSize === -1 ? "3px" : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + "px"; return !a.width || !b.width || ((!this.props.LinkDocs[0].linkDisplay) && !aActive && !bActive) ? (null) : (<> - <path className="collectionfreeformlinkview-linkLine" style={{ opacity: this._opacity, /*strokeDasharray: "2 2",*/ stroke, strokeWidth }} + <path className="collectionfreeformlinkview-linkLine" style={{ pointerEvents: "all", opacity: this._opacity, strokeDasharray: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? "2 2" : undefined, stroke, strokeWidth }} + onClick={() => SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true)} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} /> {textX === undefined ? (null) : <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown} > {Field.toString(this.props.LinkDocs[0].description as any as Field)} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index df690da49..ceee4051b 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -153,7 +153,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); } @computed get cachedGetTransform(): Transform { - return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); + return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } @action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set; @@ -172,11 +172,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY); zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1)); - contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; + contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; getTransform = () => this.cachedGetTransform.copy(); getLocalTransform = () => this.cachedGetLocalTransform.copy(); getContainerTransform = () => this.cachedGetContainerTransform.copy(); - getTransformOverlay = () => this.getContainerTransform().translate(1, 1); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); isAnyChildContentActive = () => this.props.isAnyChildContentActive(); addLiveTextBox = (newBox: Doc) => { @@ -228,7 +227,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; - const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); + const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y); const z = NumCast(refDoc.z); const x = (z ? xpo : xp) - docDragData.offset[0]; const y = (z ? ypo : yp) - docDragData.offset[1]; @@ -967,7 +966,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P else if (this.props.isContentActive(true) && !this.Document._isGroup) { e.stopPropagation(); e.preventDefault(); - this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? } } @@ -1174,13 +1173,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P rootSelected={childData ? this.rootSelected : returnFalse} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} - ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform} + ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform} PanelWidth={childLayout[WidthSym]} PanelHeight={childLayout[HeightSym]} docFilters={this.childDocFilters} docRangeFilters={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isContentActive={returnFalse} + isContentActive={emptyFunction} isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} focus={this.focusDocument} addDocTab={this.addDocTab} @@ -1199,7 +1198,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P bringToFront={this.bringToFront} showTitle={this.props.childShowTitle} dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} - pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" : + pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" : (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents} jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this @@ -1567,7 +1566,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } onPointerOver = (e: React.PointerEvent) => { - (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey); e.stopPropagation(); } @@ -1640,6 +1638,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} + isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable} transform={this.contentTransform} zoomScaling={this.zoomScaling} presPaths={BoolCast(this.Document.presPathView)} @@ -1748,6 +1747,7 @@ interface CollectionFreeFormViewPannableContentsProps { progressivize?: boolean; presPinView?: boolean; isAnnotationOverlay: boolean | undefined; + isAnnotationOverlayScrollable: boolean | undefined; } @observer diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 65c345547..ec1cbadd5 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas'; import { List } from '../../../../fields/List'; import { makeInterface } from '../../../../fields/Schema'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils'; +import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; @@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu LayoutTemplateString={this.props.childLayoutString} freezeDimensions={this.props.childFreezeDimensions} renderDepth={this.props.renderDepth + 1} - isContentActive={returnFalse} + isContentActive={emptyFunction} PanelWidth={width} PanelHeight={height} rootSelected={this.rootSelected} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 30836854a..a2d51e2e7 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas'; import { List } from '../../../../fields/List'; import { makeInterface } from '../../../../fields/Schema'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; -import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils'; +import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils'; import { DragManager, dropActionType } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; @@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument) ScreenToLocalTransform={dxf} focus={this.props.focus} docFilters={this.childDocFilters} - isContentActive={returnFalse} + isContentActive={emptyFunction} docRangeFilters={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} ContainingCollectionDoc={this.props.CollectionView?.props.Document} diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 9fe18d118..273e609ca 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider"; interface ResizerProps { width: number; styleProvider?: StyleProviderFunc; - isContentActive?: () => boolean; + isContentActive?: () => boolean | undefined; columnUnitLength(): number | undefined; toLeft?: Doc; toRight?: Doc; diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx index 5478bf709..006ef4df6 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView"; interface ResizerProps { height: number; styleProvider?: StyleProviderFunc; - isContentActive?: () => boolean; + isContentActive?: () => boolean | undefined; columnUnitLength(): number | undefined; toTop?: Doc; toBottom?: Doc; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx index 1306b79cb..dc35b5749 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx @@ -225,7 +225,7 @@ export interface KeysDropdownProps { fieldKey: string; ContainingCollectionDoc: Doc | undefined; ContainingCollectionView: Opt<CollectionView>; - active?: (outsideReaction?: boolean) => boolean; + active?: (outsideReaction?: boolean) => boolean | undefined; openHeader: (column: any, screenx: number, screeny: number) => void; col: SchemaHeaderField; icon: IconProp; diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx index bc5a9559f..2219345f6 100644 --- a/src/client/views/collections/collectionSchema/SchemaTable.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx @@ -68,7 +68,7 @@ export interface SchemaTableProps { addDocument?: (document: Doc | Doc[]) => boolean; moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; ScreenToLocalTransform: () => Transform; - active: (outsideReaction: boolean | undefined) => boolean; + active: (outsideReaction: boolean | undefined) => boolean | undefined; onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void; addDocTab: (document: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 1ec7bf72a..9fcd45e72 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -175,7 +175,7 @@ position: absolute; bottom: 0; width: 100%; - overflow-y: scroll; + overflow-y: auto; transform-origin: bottom left; opacity: 0.1; transition: opacity 0.5s; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c9b246c10..c8a32a911 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -138,6 +138,7 @@ export interface DocumentViewSharedProps { hideLinkButton?: boolean; hideCaptions?: boolean; ignoreAutoHeight?: boolean; + forceAutoHeight?: boolean; disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. pointerEvents?: string; scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document @@ -224,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps componentWillUnmount() { this.cleanupHandlers(true); } componentDidMount() { this.setupHandlers(); } - componentDidUpdate() { this.setupHandlers(); } + //componentDidUpdate() { this.setupHandlers(); } setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { @@ -415,6 +416,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. + ffview?.setupDragLines(false); } } @@ -824,13 +826,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); isContentActive = (outsideReaction?: boolean) => { - return CurrentUserUtils.SelectedTool !== InkTool.None || + return this.props.isContentActive() === false ? false : ( + CurrentUserUtils.SelectedTool !== InkTool.None || SnappingManager.GetIsDragging() || this.props.rootSelected() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._componentView?.isAnyChildContentActive?.() || - this.props.isContentActive() ? true : false; + this.props.isContentActive()) ? true : undefined; } @computed get contents() { TraceMobx(); @@ -1255,7 +1258,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { position: this.props.Document.isInkMask ? "absolute" : undefined, transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`, width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`, - height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : + height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` : `${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index ee81e106a..943b9f153 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -18,7 +18,7 @@ export interface FieldViewProps extends DocumentViewSharedProps { scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further select: (isCtrlPressed: boolean) => void; - isContentActive: (outsideReaction?: boolean) => boolean; + isContentActive: (outsideReaction?: boolean) => boolean | undefined; isDocumentActive?: () => boolean; isSelected: (outsideReaction?: boolean) => boolean; scaling?: () => number; diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 2041c7399..fb8e89da9 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -225,7 +225,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true }); Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox - newFacet._textBoxPadding = 4; + newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4; const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`; newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" }); } else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) { diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index b82d16677..879a63248 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -15,7 +15,7 @@ const LinkDocument = makeInterface(documentSchema); @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(LinkDocument) { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); } - isContentActiveFunc = () => this.isContentActive() ? true : false; + isContentActiveFunc = () => this.isContentActive(); render() { if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true); return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 424083dac..2e29c0656 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -166,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { docViewPath={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} isDocumentActive={returnFalse} - isContentActive={returnFalse} + isContentActive={emptyFunction} addDocument={returnFalse} removeDocument={returnFalse} addDocTab={returnFalse} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 7ad96bf05..0c631e5f9 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -313,7 +313,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl PanelHeight={this.formattedPanelHeight} isAnnotationOverlay={true} select={emptyFunction} - isContentActive={returnFalse} + isContentActive={emptyFunction} scaling={returnOne} xPadding={25} yPadding={10} diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss index 4871599b8..f0d7bd2f3 100644 --- a/src/client/views/nodes/VideoBox.scss +++ b/src/client/views/nodes/VideoBox.scss @@ -48,10 +48,18 @@ width: 100%; z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt position: absolute; + video { + width: auto; + height: 100%; + display: flex; + margin: auto; + } } .videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen { - height: Auto; + width: 100%; + height: 100%; + left: 0px; } .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 440ccf638..615d595c0 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -307,12 +307,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get content() { const field = Cast(this.dataDoc[this.fieldKey], VideoField); const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive"; - const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; + const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive; return !field ? <div key="loading">Loading</div> : - <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}> - <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}> + <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}> + <div className={classname}> <video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }} onCanPlay={this.videoLoad} controls={VideoBox._nativeControls} onPlay={() => this.Play()} @@ -457,11 +456,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get youtubeContent() { this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; - const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); + const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : ""); const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0))); return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`} onPointerLeave={this.updateTimecode} - onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390} + onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390} src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index aa53f751d..9d0402075 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -123,7 +123,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); } @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); } - @computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; } + @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; } @computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); } @computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); } @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); } @@ -1140,10 +1140,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field); - if (startupText) { - const { state: { tr }, dispatch } = this._editorView; - dispatch(tr.insertText(startupText)); + const { state, dispatch } = this._editorView; + if (!rtfField) { + const startupText = Field.toString(this.dataDoc[fieldKey] as Field); + if (startupText) { + dispatch(state.tr.insertText(startupText)); + } else if (!FormattedTextBox.LiveTextUndo) { + selectAll(this._editorView!.state, (tr) => { + this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" }))); + }); + } } (this._editorView as any).TextView = this; } @@ -1174,8 +1180,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), ...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])]; } } @@ -1565,7 +1571,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp setHeight={this.setSidebarHeight} fitContentsToDoc={this.fitToBox} noSidebar={true} - fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : this.SidebarKey} />; + fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />; }; return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> @@ -1581,10 +1587,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; - const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const selPad = Math.min(margins, 10); - const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0); - const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : ""; + const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); + const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); + const selPad = ((selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0); + const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? "-selected" : ""; const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return (styleFromString?.height === "0px" ? (null) : <div className="formattedTextBox-cont" @@ -1628,7 +1634,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp onScroll={this.onScroll} onDrop={this.ondrop} > <div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget} style={{ - padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`), + padding: StrCast(this.layoutDoc._textBoxPadding), + paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), + paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), + paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), + paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined }} /> diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 3612bd7c4..fe297782c 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc'; +import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc'; import { documentSchema } from "../../../fields/documentSchemas"; import { Id } from '../../../fields/FieldSymbols'; import { createSchema, makeInterface } from '../../../fields/Schema'; @@ -14,6 +14,8 @@ import "./SearchBox.scss"; import { DocumentManager } from '../../util/DocumentManager'; import { DocUtils } from '../../documents/Documents'; import { Tooltip } from "@material-ui/core"; +import { DictationOverlay } from '../DictationOverlay'; +import { CollectionSchemaBooleanCell } from '../collections/collectionSchema/CollectionSchemaCells'; export const searchSchema = createSchema({ Document: Doc @@ -22,6 +24,10 @@ export const searchSchema = createSchema({ type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>; const SearchBoxDocument = makeInterface(documentSchema, searchSchema); +const DAMPENING_FACTOR = 0.9; +const MAX_ITERATIONS = 25; +const ERROR = 0.03; + export interface SearchBoxProps extends FieldViewProps { linkSearch: boolean; linkFrom?: (() => Doc | undefined) | undefined; @@ -40,7 +46,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc @observable _searchString = ""; @observable _docTypeString = "all"; - @observable _results: [Doc, string[]][] = []; + @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>(); + @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>(); + @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); + @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @observable _selectedResult: Doc | undefined = undefined; @observable _deletedDocsStatus: boolean = false; @observable _onlyAliases: boolean = true; @@ -110,11 +119,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc }); makeLink = action((linkTo: Doc) => { - console.log(linkTo.title); if (this.props.linkFrom) { const linkFrom = this.props.linkFrom(); if (linkFrom) { - console.log(linkFrom.title); DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }, "Link"); } } @@ -204,7 +211,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const collection = CollectionDockingView.Instance; query = query.toLowerCase(); - this._results = []; + this._results.clear(); this._selectedResult = undefined; if (collection !== undefined) { @@ -216,16 +223,114 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const hlights = new Set<string>(); SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key)); blockedKeys.forEach(key => hlights.delete(key)); - Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]); + + if (Array.from(hlights.keys()).length > 0) { + this._results.set(doc, Array.from(hlights.keys())); + } } docIDs.push(doc[Id]); }); } + + this.computePageRanks(); + } + + /** + * This method initializes the page rank of every document to the reciprocal + * of the number of documents in the collection. + */ + @action + initializePageRanks() { + this._pageRanks.clear(); + this._linkedDocsOut.clear(); + + this._results.forEach((_, doc) => { + this._linkedDocsIn.set(doc, new Set()); + }); + + this._results.forEach((_, doc) => { + this._pageRanks.set(doc, 1.0 / this._results.size); + + if (Doc.GetProto(doc)[DirectLinksSym].size == 0) { + this._linkedDocsOut.set(doc, new Set(this._results.keys())); + + this._results.forEach((_, linkedDoc) => { + this._linkedDocsIn.get(linkedDoc)?.add(doc); + }); + } + else { + let linkedDocSet: Set<Doc> = new Set(); + + Doc.GetProto(doc)[DirectLinksSym].forEach((link) => { + let d1 = link?.anchor1 as Doc; + let d2 = link?.anchor2 as Doc; + if (doc == d1 && this._results.has(d2)) { + linkedDocSet.add(d2); + this._linkedDocsIn.get(d2)?.add(doc); + } + else if (doc == d2 && this._results.has(d1)) { + linkedDocSet.add(d1); + this._linkedDocsIn.get(d1)?.add(doc); + } + }) + + this._linkedDocsOut.set(doc, linkedDocSet); + } + }); + } + + /** + * This method runs one complete iteration of the page rank algorithm. It + * returns true iff all page ranks have converged (i.e. changed by less than + * the _error value), which means that the algorithm should terminate. + * + * @return true if page ranks have converged; false otherwise + */ + @action + pageRankIteration(): boolean { + let converged = true; + const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size; + + let nextPageRanks: Map<Doc, number> = new Map<Doc, number>(); + + this._results.forEach((_, doc) => { + let nextPageRank = pageRankFromAll; + + this._linkedDocsIn.get(doc)?.forEach((linkedDoc) => { + nextPageRank += DAMPENING_FACTOR * (this._pageRanks.get(linkedDoc) ?? 0) / (this._linkedDocsOut.get(linkedDoc)?.size ?? 1); + }); + + nextPageRanks.set(doc, nextPageRank); + + if (Math.abs(nextPageRank - (this._pageRanks.get(doc) ?? 0)) > ERROR) { + converged = false; + } + }); + + this._pageRanks = nextPageRanks; + + return converged; + } + + /** + * This method performs the page rank algorithm on the graph of documents + * that match the search query. Vertices are documents and edges are links + * between documents. + */ + @action + computePageRanks() { + this.initializePageRanks(); + + for (let i = 0; i < MAX_ITERATIONS; i++) { + if (this.pageRankIteration()) { + break; + } + } } /** * @param {Doc} doc - doc for which keys are returned - * + * * This method returns a list of a document doc's keys. */ static documentKeys(doc: Doc) { @@ -244,7 +349,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const query = StrCast(this._searchString); Doc.SetSearchQuery(query); - this._results = []; + this._results.clear(); if (query) { this.searchCollection(query); @@ -256,16 +361,16 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc * brushes and highlights. All search matches are cleared as well. */ resetSearch = action(() => { - this._results.forEach(result => { - Doc.UnBrushDoc(result[0]); - Doc.UnHighlightDoc(result[0]); + this._results.forEach((_, doc) => { + Doc.UnBrushDoc(doc); + Doc.UnHighlightDoc(doc); Doc.ClearSearchMatches(); }); }); /** * @param {Doc} doc - doc to be selected - * + * * This method selects a doc by either jumping to it (centering/zooming in on it) * or opening it in a new tab. */ @@ -292,8 +397,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc const isLinkSearch: boolean = this.props.linkSearch; + const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)) // sorted by page rank + + const resultsJSX = Array(); - const results = this._results.map(result => { + sortedResults.forEach((result) => { var className = "searchBox-results-scroll-view-result"; if (this._selectedResult === result[0]) { @@ -305,7 +413,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc if (this._docTypeString === "all" || this._docTypeString === result[0].type) { validResults++; - return ( + resultsJSX.push( <Tooltip key={result[0][Id]} placement={"right"} title={<><div className="dash-tooltip">{title}</div></>}> <div onClick={isLinkSearch ? () => this.makeLink(result[0]) : @@ -326,12 +434,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc </Tooltip> ); } - - return null; }); - results.filter(result => result); - return ( <div style={{ pointerEvents: "all" }} className="searchBox-container"> <div className="searchBox-bar" > @@ -345,7 +449,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc {`${validResults}` + " result" + (validResults === 1 ? "" : "s")} </div> <div className="searchBox-results-scroll-view"> - {results} + {resultsJSX} </div> </div> </div > diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index 71ddda866..88221732e 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -94,7 +94,7 @@ export class AudioUpload extends React.Component { PanelHeight={() => 400} renderDepth={0} isDocumentActive={returnTrue} - isContentActive={returnFalse} + isContentActive={emptyFunction} focus={emptyFunction} layerProvider={undefined} styleProvider={() => "rgba(0,0,0,0)"} diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 652804126..d732a6e2f 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -211,7 +211,7 @@ export class MobileInterface extends React.Component { PanelHeight={this.returnHeight} renderDepth={0} isDocumentActive={returnTrue} - isContentActive={returnFalse} + isContentActive={emptyFunction} focus={DocUtils.DefaultFocus} styleProvider={this.whitebackground} layerProvider={undefined} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 54b71e8ce..f13580865 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -332,7 +332,7 @@ export namespace DashUploadUtils { } }; - const parseExifData = async (source: string): Promise<Upload.EnrichedExifData> => { + const parseExifData = async (source: string) => { const image = await request.get(source, { encoding: null }); const { data, error } = await new Promise(resolve => { new ExifImage({ image }, (error, data) => { @@ -343,9 +343,8 @@ export namespace DashUploadUtils { resolve({ data, error: reason }); }); }); - return { data: await exifr.parse(image) as any, error }; //data && bufferConverterRec(data); - return { data, error }; + return { data: await exifr.parse(image), error }; }; const { pngs, jpgs, webps, tiffs } = AcceptableMedia; diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index fdc65188f..cde95526f 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -44,7 +44,7 @@ export namespace Upload { } export interface EnrichedExifData { - data: ExifData; + data: ExifData & ExifData["gps"]; error?: string; } |