diff options
Diffstat (limited to 'src')
36 files changed, 751 insertions, 936 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index dd27f2dab..e8ecef231 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -558,7 +558,7 @@ export class CurrentUserUtils { _width: 60, _height: 60, watchedDocuments, - onClick: ScriptField.MakeScript(click, { scriptContext: "any" }), system: true + onClick: ScriptField.MakeScript(click, { scriptContext: "any" }) })); const userDoc = menuBtns[menuBtns.length - 1]; userDoc.userDoc = doc; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 67e05f8d0..637e219d5 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -10,6 +10,8 @@ import { LightboxView } from '../views/LightboxView'; import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView'; import { Scripting } from './Scripting'; import { CurrentUserUtils } from './CurrentUserUtils'; +import { TabDocView } from '../views/collections/TabDocView'; +import { UndoManager } from './UndoManager'; export class DocumentManager { @@ -219,4 +221,12 @@ export class DocumentManager { } } -Scripting.addGlobal(function DocFocus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, { willZoom: true })); });
\ No newline at end of file +Scripting.addGlobal(function DocFocus(doc: any) { + const dv = DocumentManager.Instance.getDocumentView(doc); + if (dv && dv?.props.Document === doc) dv.props.focus(doc, { willZoom: true }); + else { + const context = Cast(doc.context, Doc, null); + CollectionDockingView.AddSplit(context || doc, "right") && context && + setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc)); + } +});
\ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 7b4d43793..19f1f8d15 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -13,7 +13,7 @@ import * as globalCssVariables from "../views/globalCssVariables.scss"; import { UndoManager } from "./UndoManager"; import { SnappingManager } from "./SnappingManager"; -export type dropActionType = "alias" | "copy" | "move" | "same" | "none" | undefined; // undefined = move +export type dropActionType = "alias" | "copy" | "move" | "same" | "none" | undefined; // undefined = move, "same" = move but don't call removeDropProperties export function SetupDrag( _reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc> | undefined, diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 569ad8ab4..05fb9f378 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -148,12 +148,15 @@ export namespace UndoManager { } }); - export function ClearTempBatch() { - tempEvents = undefined; - } export function RunInTempBatch<T>(fn: () => T) { tempEvents = []; - return runInAction(fn); + try { + const success = runInAction(fn); + if (!success) UndoManager.UndoTempBatch(); + return success; + } finally { + tempEvents = undefined; + } } //TODO Make this return the return value export function RunInBatch<T>(fn: () => T, batchName: string) { diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b800ba777..ef1228976 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -156,15 +156,15 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T // if the moved document is already in this overlay collection nothing needs to be done. // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. @action.bound - moveDocument(doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean { - return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false; + moveDocument(doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean { + return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc, annotationKey) ? addDocument(doc) : false; } @action.bound - addDocument(doc: Doc | Doc[]): boolean { + addDocument(doc: Doc | Doc[], annotationKey?: string): boolean { const docs = doc instanceof Doc ? [doc] : doc; docs.map(doc => doc.context = Doc.GetProto(doc).annotationOn = this.props.Document); const targetDataDoc = this.props.Document[DataSym]; - const docList = DocListCast(targetDataDoc[this.annotationKey]); + const docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]); const added = docs.filter(d => !docList.includes(d)); const effectiveAcl = GetEffectiveAcl(this.dataDoc); @@ -184,12 +184,12 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T } if (effectiveAcl === AclAddonly) { - added.map(doc => Doc.AddDocToList(targetDataDoc, this.annotationKey, doc)); + added.map(doc => Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc)); } else { added.map(doc => doc.context = this.props.Document); - (targetDataDoc[this.annotationKey] as List<Doc>).push(...added); - targetDataDoc[this.annotationKey + "-lastModified"] = new DateField(new Date(Date.now())); + (targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>).push(...added); + targetDataDoc[(annotationKey ?? this.annotationKey) + "-lastModified"] = new DateField(new Date(Date.now())); } } } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7f1023a4a..2391a21e6 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -566,7 +566,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b (collectionAcl === AclAdmin || collectionAcl === AclEdit || docAcl === AclAdmin); }); const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection); - const closeIcon = !canDelete ? (null) : ( + const closeIcon = !canDelete ? <div></div> : ( <Tooltip title={<div className="dash-tooltip">Close</div>} placement="top"> <div className="documentDecorations-closeButton" onClick={this.onCloseClick}> <FontAwesomeIcon className="documentdecorations-times" icon={"times"} size="lg" /> diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 828a2eb78..f1cecd272 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -3,8 +3,6 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; -import { SchemaHeaderField } from '../../fields/SchemaHeaderField'; -import { DragManager } from '../util/DragManager'; import "./EditableView.scss"; export interface EditableProps { @@ -12,16 +10,13 @@ export interface EditableProps { * Called to get the initial value for editing * */ GetValue(): string | undefined; - /** * Called to apply changes * @param value - The string entered by the user to set the value to * @returns `true` if setting the value was successful, `false` otherwise * */ SetValue(value: string, shiftDown?: boolean, enterKey?: boolean): boolean; - OnFillDown?(value: string): void; - OnTab?(shift?: boolean): void; OnEmpty?(): void; @@ -45,15 +40,12 @@ export interface EditableProps { }; oneLine?: boolean; editing?: boolean; - onClick?: (e: React.MouseEvent) => boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; textCallback?: (char: string) => boolean; showMenuOnLoad?: boolean; - HeadingObject?: SchemaHeaderField | undefined; toggle?: () => void; - color?: string | undefined; - onDrop?: any; + background?: string | undefined; placeholder?: string; fullWidth?: boolean; // used in PropertiesView to make the whole key:value input box clickable } @@ -65,6 +57,8 @@ export interface EditableProps { */ @observer export class EditableView extends React.Component<EditableProps> { + private _ref = React.createRef<HTMLDivElement>(); + private _inputref = React.createRef<HTMLInputElement>(); @observable _editing: boolean = false; constructor(props: EditableProps) { @@ -72,17 +66,6 @@ export class EditableView extends React.Component<EditableProps> { this._editing = this.props.editing ? true : false; } - // @action - // componentDidUpdate(nextProps: EditableProps) { - // // this is done because when autosuggest is turned on, the suggestions are passed in as a prop, - // // so when the suggestions are passed in, and no editing prop is passed in, it used to set it - // // to false. this will no longer do so -syip - // if (nextProps.editing && nextProps.editing !== this._editing) { - // this._editing = nextProps.editing; - // EditableView.loadId = ""; - // } - // } - @action componentDidUpdate() { if (this._editing && this.props.editing === false) { @@ -92,19 +75,10 @@ export class EditableView extends React.Component<EditableProps> { } } - @action - componentDidMount() { - if (this._ref.current && this.props.onDrop) { - DragManager.MakeDropTarget(this._ref.current, this.props.onDrop.bind(this)); - } - } - @action componentWillUnmount() { this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); } - _didShow = false; - @action onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { switch (e.key) { @@ -150,7 +124,7 @@ export class EditableView extends React.Component<EditableProps> { e.nativeEvent.stopPropagation(); if (this._ref.current && this.props.showMenuOnLoad) { this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y); - } else if (!this.props.onClick?.(e)) { + } else { this._editing = true; this.props.isEditingCallback?.(true); } @@ -159,7 +133,7 @@ export class EditableView extends React.Component<EditableProps> { } @action - private finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) { + finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean, enterKey: boolean) { if (this.props.SetValue(value, shiftDown, enterKey)) { this._editing = false; this.props.isEditingCallback?.(false,); @@ -173,9 +147,7 @@ export class EditableView extends React.Component<EditableProps> { } } - stopPropagation(e: React.SyntheticEvent) { - e.stopPropagation(); - } + stopPropagation(e: React.SyntheticEvent) { e.stopPropagation(); } @action setIsFocused = (value: boolean) => { @@ -184,8 +156,6 @@ export class EditableView extends React.Component<EditableProps> { return wasFocused !== this._editing; } - _ref = React.createRef<HTMLDivElement>(); - _inputref = React.createRef<HTMLInputElement>(); renderEditor() { return this.props.autosuggestProps ? <Autosuggest @@ -204,14 +174,13 @@ export class EditableView extends React.Component<EditableProps> { }} /> : <input className="editableView-input" ref={this._inputref} + style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} + placeholder={this.props.placeholder} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} - onKeyDown={this.onKeyDown} autoFocus={true} - onKeyPress={e => e.stopPropagation()} - onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} - onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} - style={{ display: this.props.display, fontSize: this.props.fontSize, minWidth: 20 }} - placeholder={this.props.placeholder} + onKeyDown={this.onKeyDown} + onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} />; } @@ -226,13 +195,13 @@ export class EditableView extends React.Component<EditableProps> { </div> : this.renderEditor(); } - setTimeout(() => this.props.autosuggestProps?.resetValue(), 0); + setTimeout(() => this.props.autosuggestProps?.resetValue()); return this.props.contents instanceof ObjectField ? (null) : <div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`} ref={this._ref} style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: "17px", whiteSpace: "nowrap", height: this.props.height || "auto", maxHeight: this.props.maxHeight }} onClick={this.onClick} placeholder={this.props.placeholder}> - <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }} >{ - this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} + <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }} > + {this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()} </span> </div>; } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d4b8e9406..33eae97a3 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -147,13 +147,14 @@ export class KeyManager { break; case "delete": case "backspace": - if (document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA") { - return { stopPropagation: false, preventDefault: false }; + if (document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA") { + const selected = SelectionManager.Views().slice(); + UndoManager.RunInBatch(() => { + SelectionManager.DeselectAll(); + selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document)); + }, "delete"); + return { stopPropagation: true, preventDefault: true }; } - - const selected = SelectionManager.Views().slice(); - UndoManager.RunInBatch(() => selected.map(dv => !dv.props.Document._stayInCollection && dv.props.removeDocument?.(dv.props.Document)), "delete"); - SelectionManager.DeselectAll(); break; case "arrowleft": UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge(-1, 0)), "nudge left"); break; case "arrowright": UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(1, 0)), "nudge right"); break; diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index e967a5b07..babc518ff 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, observable } from 'mobx'; +import { action, computed, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import "normalize.css"; import * as React from 'react'; @@ -13,7 +13,7 @@ import { SelectionManager } from '../util/SelectionManager'; import { Transform } from '../util/Transform'; import { TabDocView } from './collections/TabDocView'; import "./LightboxView.scss"; -import { DocumentView } from './nodes/DocumentView'; +import { DocumentView, ViewAdjustment } from './nodes/DocumentView'; import { DefaultStyleProvider } from './StyleProvider'; interface LightboxViewProps { @@ -28,32 +28,33 @@ export class LightboxView extends React.Component<LightboxViewProps> { @computed public static get LightboxDoc() { return this._doc; } @observable private static _doc: Opt<Doc>; @observable private static _docTarget: Opt<Doc>; - @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target @observable private static _docFilters: string[] = []; // filters - private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, transition: Opt<string> }>; + @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target + private static _savedState: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number> }>; private static _history: Opt<{ doc: Doc, target?: Doc }[]> = []; private static _future: Opt<Doc[]> = []; private static _docView: Opt<DocumentView>; - static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number>, transition: Opt<string> }> }[] = []; + static path: { doc: Opt<Doc>, target: Opt<Doc>, history: Opt<{ doc: Doc, target?: Doc }[]>, future: Opt<Doc[]>, saved: Opt<{ panX: Opt<number>, panY: Opt<number>, scale: Opt<number> }> }[] = []; @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[]) { + if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { + this.LightboxDoc._panX = this._savedState.panX; + this.LightboxDoc._panY = this._savedState.panY; + this.LightboxDoc._viewScale = this._savedState.scale; + this.LightboxDoc._viewTransition = undefined; + } if (!doc) { this._docFilters && (this._docFilters.length = 0); - if (this.LightboxDoc) { - this.LightboxDoc._panX = this._savedState?.panX; - this.LightboxDoc._panY = this._savedState?.panY; - this.LightboxDoc._viewScale = this._savedState?.scale; - this.LightboxDoc._viewTransition = this._savedState?.transition; - } this._future = this._history = []; } else { TabDocView.PinDoc(doc, { hidePresBox: true }); this._history ? this._history.push({ doc, target }) : this._history = [{ doc, target }]; - this._savedState = { - panX: Cast(doc._panX, "number", null), - panY: Cast(doc._panY, "number", null), - scale: Cast(doc._viewScale, "number", null), - transition: Cast(doc._viewTransition, "string", null) - }; + if (doc !== LightboxView.LightboxDoc) { + this._savedState = { + panX: Cast(doc._panX, "number", null), + panY: Cast(doc._panY, "number", null), + scale: Cast(doc._viewScale, "number", null), + }; + } } if (future) { this._future = future.slice().sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)).sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length); @@ -92,6 +93,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { } // adds a cookie to the lightbox view - the cookie becomes part of a filter which will display any documents whose cookie metadata field matches this cookie + @action public static SetCookie(cookie: string) { if (this.LightboxDoc && cookie) { this._docFilters = (f => this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f])(`cookies:${cookie}:provide`); @@ -103,8 +105,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + "-annotations"]), ...(LightboxView._future ?? []) - ] - .sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))); + ].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))); } docFilters = () => LightboxView._docFilters || []; addDocTab = LightboxView.AddDocTab; @@ -113,16 +114,16 @@ export class LightboxView extends React.Component<LightboxViewProps> { const target = LightboxView._docTarget = LightboxView._future?.pop(); const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); if (docView && target) { - docView.focus(target, { willZoom: true, scale: 0.9 }); - if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target: LightboxView._docTarget }); + docView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 }); + if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); } else { if (!target && LightboxView.path.length) { const saved = LightboxView._savedState; - if (LightboxView.LightboxDoc) { - LightboxView.LightboxDoc._panX = saved?.panX; - LightboxView.LightboxDoc._panY = saved?.panY; - LightboxView.LightboxDoc._viewScale = saved?.scale; - LightboxView.LightboxDoc._viewTransition = saved?.transition; + if (LightboxView.LightboxDoc && saved) { + LightboxView.LightboxDoc._panX = saved.panX; + LightboxView.LightboxDoc._panY = saved.panY; + LightboxView.LightboxDoc._viewScale = saved.scale; + LightboxView.LightboxDoc._viewTransition = undefined; } const pop = LightboxView.path.pop(); if (pop) { @@ -149,16 +150,19 @@ export class LightboxView extends React.Component<LightboxViewProps> { return; } const { doc, target } = LightboxView._history?.lastElement(); - const docView = target && DocumentManager.Instance.getLightboxDocumentView(target); - if (docView && target) { - LightboxView._doc = doc; - LightboxView._docTarget = target || doc; - if (LightboxView._future?.lastElement() !== previous.target || previous.doc) LightboxView._future?.push(previous.target || previous.doc); - docView.focus(target, { willZoom: true, scale: 0.9 }); - } else { - LightboxView._doc = doc; - LightboxView._docTarget = target || doc; + const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc); + if (docView) { + LightboxView._docTarget = undefined; + const focusSpeed = 1000; + doc._viewTransition = `transform ${focusSpeed}ms`; + if (!target) docView.ComponentView?.shrinkWrap?.(); + else docView.focus(target, { willZoom: true, scale: 0.9 }); + setTimeout(() => doc._viewTransition = undefined, focusSpeed); } + else { + LightboxView.SetLightboxDoc(doc, target); + } + if (LightboxView._future?.lastElement() !== previous.target || previous.doc) LightboxView._future?.push(previous.target || previous.doc); LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links).map(link => { const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!); return opp?.TourMap ? opp : undefined; @@ -188,6 +192,8 @@ export class LightboxView extends React.Component<LightboxViewProps> { setTimeout(LightboxView.Next); } + future = () => LightboxView._future; + tourMap = () => LightboxView._tourMap; fitToBox = () => LightboxView._docTarget === LightboxView.LightboxDoc; render() { let downx = 0, downy = 0; @@ -207,14 +213,13 @@ export class LightboxView extends React.Component<LightboxViewProps> { }}> <DocumentView ref={action((r: DocumentView | null) => { LightboxView._docView = r !== null ? r : undefined; - setTimeout(action(() => { - const vals = r?.ComponentView?.freeformData?.(); - if (vals && r) { - r.layoutDoc._panX = vals.panX; - r.layoutDoc._panY = vals.panY; - r.layoutDoc._viewScale = vals.scale; - } - r && (LightboxView._docTarget = undefined); + r && setTimeout(action(() => { + const target = LightboxView._docTarget; + const doc = LightboxView._doc; + const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target); + if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.(); + else target && targetView?.focus(target, { willZoom: true, scale: 0.9, instant: true }); + LightboxView._docTarget = undefined; })); })} Document={LightboxView.LightboxDoc} @@ -252,13 +257,25 @@ export class LightboxView extends React.Component<LightboxViewProps> { e.stopPropagation(); LightboxView.Next(); })} - {this.navBtn("50%", 0, 0, "chevron-down", - () => LightboxView.LightboxDoc && LightboxView._future?.length ? "" : "none", e => { - e.stopPropagation(); - this.stepInto(); - }, - StrCast(LightboxView._tourMap?.lastElement()?.TourMap) - )} + <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} /> </div>; } +} +interface LightboxTourBtnProps { + navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element; + tourMap: () => Opt<Doc[]>; + future: () => Opt<Doc[]>; + stepInto: () => void; +} +@observer +export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> { + render() { + return this.props.navBtn("50%", 0, 0, "chevron-down", + () => LightboxView.LightboxDoc && this.props.future()?.length ? "" : "none", e => { + e.stopPropagation(); + this.props.stepInto(); + }, + StrCast(this.props.tourMap()?.lastElement()?.TourMap) + ) + } }
\ No newline at end of file diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index 03afbe7bf..6e8f9a2df 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -58,6 +58,36 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this._height = this._width = 0; document.addEventListener("pointermove", this.onSelectMove); document.addEventListener("pointerup", this.onSelectEnd); + + AnchorMenu.Instance.Highlight = this.highlight; + /** + * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation. + * It also initiates a Drag/Drop interaction to place the text annotation. + */ + AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { + e.preventDefault(); + e.stopPropagation(); + const targetCreator = () => { + const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100); + FormattedTextBox.SelectOnLoad = target[Id]; + return target; + }; + const anchorCreator = () => { + const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color + annoDoc.isLinkButton = true; // prevents link button from showing up --- maybe not a good thing? + this.props.addDocument(annoDoc); + return annoDoc; + }; + DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, anchorCreator, targetCreator), e.pageX, e.pageY, { + dragComplete: e => { + if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { + e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "Annotation"); + e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.rootDoc; + } + e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument + } + }); + }); } componentWillUnmount() { document.removeEventListener("pointermove", this.onSelectMove); @@ -154,36 +184,6 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { AnchorMenu.Instance.Marquee = { left: this._left, top: this._top, width: this._width, height: this._height }; } - AnchorMenu.Instance.Highlight = this.highlight; - /** - * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation. - * It also initiates a Drag/Drop interaction to place the text annotation. - */ - AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { - e.preventDefault(); - e.stopPropagation(); - const targetCreator = () => { - const target = CurrentUserUtils.GetNewTextDoc("Note linked to " + this.props.rootDoc.title, 0, 0, 100, 100); - FormattedTextBox.SelectOnLoad = target[Id]; - return target; - }; - const anchorCreator = () => { - const annoDoc = this.highlight("rgba(173, 216, 230, 0.75)"); // hyperlink color - annoDoc.isLinkButton = true; // prevents link button from showing up --- maybe not a good thing? - this.props.addDocument(annoDoc); - return annoDoc; - }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.rootDoc, anchorCreator, targetCreator), e.pageX, e.pageY, { - dragComplete: e => { - if (!e.aborted && e.annoDragData && e.annoDragData.annotationDocument && e.annoDragData.dropDocument && !e.linkDocument) { - e.linkDocument = DocUtils.MakeLink({ doc: e.annoDragData.annotationDocument }, { doc: e.annoDragData.dropDocument }, "Annotation"); - e.annoDragData.annotationDocument.isPushpin = e.annoDragData?.dropDocument.annotationOn === this.props.rootDoc; - } - e.linkDocument && e.annoDragData?.linkDropCallback?.(e as { linkDocument: Doc });// bcz: typescript can't figure out that this is valid even though we tested e.linkDocument - } - }); - }); - if (this._width > 10 || this._height > 10) { // configure and show the annotation/link menu if a the drag region is big enough const marquees = this.props.mainCont.getElementsByClassName("marqueeAnnotator-dragBox"); if (marquees?.length) { // copy the temporary marquee to allow for multiple selections (not currently available though). diff --git a/src/client/views/Touchable.tsx b/src/client/views/Touchable.tsx index bb9e108cb..789756a78 100644 --- a/src/client/views/Touchable.tsx +++ b/src/client/views/Touchable.tsx @@ -11,7 +11,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { private holdMoveDisposer?: InteractionUtils.MultiTouchEventDisposer; private holdEndDisposer?: InteractionUtils.MultiTouchEventDisposer; - protected abstract _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _touchDrag: boolean = false; protected prevPoints: Map<number, React.Touch> = new Map<number, React.Touch>(); @@ -86,19 +85,13 @@ export abstract class Touchable<T = {}> extends React.Component<T> { if (!InteractionUtils.IsDragging(this.prevPoints, myTouches, 5) && !this._touchDrag) return; this._touchDrag = true; switch (myTouches.length) { - case 1: - this.handle1PointerMove(te, me); - break; - case 2: - this.handle2PointersMove(te, me); - break; + case 1: this.handle1PointerMove(te, me); break; + case 2: this.handle2PointersMove(te, me); break; } for (const pt of me.touches) { - if (pt) { - if (this.prevPoints.has(pt.identifier)) { - this.prevPoints.set(pt.identifier, pt); - } + if (pt && this.prevPoints.has(pt.identifier)) { + this.prevPoints.set(pt.identifier, pt); } } } @@ -166,7 +159,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { this.removeHoldEndListeners(); this.addHoldMoveListeners(); this.addHoldEndListeners(); - } addMoveListeners = () => { @@ -174,21 +166,12 @@ export abstract class Touchable<T = {}> extends React.Component<T> { document.addEventListener("dashOnTouchMove", handler); this.moveDisposer = () => document.removeEventListener("dashOnTouchMove", handler); } - - removeMoveListeners = () => { - this.moveDisposer && this.moveDisposer(); - } - addEndListeners = () => { const handler = (e: Event) => this.onTouchEnd(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); document.addEventListener("dashOnTouchEnd", handler); this.endDisposer = () => document.removeEventListener("dashOnTouchEnd", handler); } - removeEndListeners = () => { - this.endDisposer && this.endDisposer(); - } - addHoldMoveListeners = () => { const handler = (e: Event) => this.handle1PointerHoldMove(e, (e as CustomEvent<InteractionUtils.MultiTouchEvent<TouchEvent>>).detail); document.addEventListener("dashOnTouchHoldMove", handler); @@ -201,21 +184,16 @@ export abstract class Touchable<T = {}> extends React.Component<T> { this.holdEndDisposer = () => document.removeEventListener("dashOnTouchHoldEnd", handler); } - removeHoldMoveListeners = () => { - this.holdMoveDisposer && this.holdMoveDisposer(); - } - - removeHoldEndListeners = () => { - this.holdEndDisposer && this.holdEndDisposer(); - } - + removeMoveListeners = () => this.moveDisposer?.(); + removeEndListeners = () => this.endDisposer?.(); + removeHoldMoveListeners = () => this.holdMoveDisposer?.(); + removeHoldEndListeners = () => this.holdEndDisposer?.(); handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { // e.stopPropagation(); // me.touchEvent.stopPropagation(); } - handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => { e.stopPropagation(); me.touchEvent.stopPropagation(); @@ -226,7 +204,6 @@ export abstract class Touchable<T = {}> extends React.Component<T> { me.touchEvent.preventDefault(); } - handleHandDown = (e: React.TouchEvent) => { // e.stopPropagation(); // e.preventDefault(); diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 652293ed6..5c8b491eb 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -9,7 +9,6 @@ position: absolute; top: 15%; height: 60%; - width: 100%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 9b1e3b80d..c5a05da00 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -40,7 +40,7 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume @computed get content() { const currentIndex = NumCast(this.layoutDoc._itemIndex); const displayDoc = (childPair: { layout: Doc, data: Doc }) => { - const script = ScriptField.MakeScript("child._showCaption = 'caption'", { child: Doc.name }, { child: childPair.layout }); + const script = ScriptField.MakeScript("this._showCaption = 'caption'", { this: Doc.name }); const onChildClick = script && (() => script); return <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit} onDoubleClick={this.onChildDoubleClick} @@ -63,8 +63,8 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? "-active" : ""} ${index}`} style={index === currentIndex ? - { opacity: '1', transform: 'scale(1.3)' } : - { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none' }}> + { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : + { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}> {displayDoc(childPair)} </div>); })); @@ -166,10 +166,10 @@ export class CollectionCarousel3DView extends CollectionSubView(Carousel3DDocume render() { const index = NumCast(this.layoutDoc._itemIndex); - const translateX = 33 * (1 - index); + const translateX = this.panelWidth() * (1 - index); return <div className="collectionCarousel3DView-outer" onClick={this.onClick} onPointerDown={this.onPointerDown} ref={this.createDashEventsTarget}> - <div className="carousel-wrapper" style={{ transform: `translateX(calc(${translateX}%` }}> + <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}> {this.content} </div> {this.props.Document._chromeStatus !== "replaced" ? this.buttons : (null)} diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index ff69c7d05..4bdd39194 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -6,7 +6,7 @@ import { Doc, DocListCast, DataSym } from "../../../fields/Doc"; import { PastelSchemaPalette, SchemaHeaderField } from "../../../fields/SchemaHeaderField"; import { ScriptField } from "../../../fields/ScriptField"; import { StrCast, NumCast } from "../../../fields/Types"; -import { numberRange, setupMoveUpEvents, emptyFunction } from "../../../Utils"; +import { numberRange, setupMoveUpEvents, emptyFunction, returnEmptyString } from "../../../Utils"; import { Docs } from "../../documents/Documents"; import { DragManager } from "../../util/DragManager"; import { CompileScript } from "../../util/Scripting"; @@ -249,14 +249,6 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap)))); const style = this.props.parent; const chromeStatus = this.props.parent.props.Document._chromeStatus; - const newEditableViewProps = { - GetValue: () => "", - SetValue: this.addDocument, - textCallback: this.textCallback, - contents: "+ NEW", - HeadingObject: this.props.headingObject, - toggle: this.toggleVisibility, - }; const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled'); const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `; return this.collapsed ? (null) : @@ -267,7 +259,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr //width: style.columnWidth / style.numGroupColumns, padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px` }}> - <EditableView {...newEditableViewProps} /> + <EditableView + GetValue={returnEmptyString} + SetValue={this.addDocument} + textCallback={this.textCallback} + contents={"+ NEW"} + toggle={this.toggleVisibility} /> </div> : null } <div className={`collectionStackingView-masonryGrid`} @@ -288,17 +285,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr const noChrome = this.props.parent.props.Document._chromeStatus === "disabled"; const key = StrCast(this.props.parent.props.Document._pivotField); const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - const headerEditableViewProps = { - GetValue: () => evContents, - SetValue: this.headingChanged, - contents: evContents, - oneLine: true, - HeadingObject: this.props.headingObject, - toggle: this.toggleVisibility, - }; + const editableHeaderView = <EditableView + GetValue={() => evContents} + SetValue={this.headingChanged} + contents={evContents} + oneLine={true} + toggle={this.toggleVisibility} />; return this.props.parent.props.Document.miniHeaders ? <div className="collectionStackingView-miniHeader"> - <EditableView {...headerEditableViewProps} /> + {editableHeaderView} </div> : !this.props.headingObject ? (null) : <div className="collectionStackingView-sectionHeader" ref={this._headerRef} > @@ -306,7 +301,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this.color : "lightgrey" }}> - {noChrome ? evContents : <EditableView {...headerEditableViewProps} />} + {noChrome ? evContents : editableHeaderView} {noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 423c94005..591b4161e 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -560,7 +560,6 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu @computed get isText() { return this.selectedDoc?.type === DocumentType.RTF || (RichTextMenu.Instance?.view as any) ? true : false; } - @computed get webBoxUrl() { return this.selectedDocumentView?.ComponentView?.url?.(); } @undoBatch @action @@ -733,58 +732,6 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </div>; } - - onWebUrlDrop = (e: React.DragEvent) => { - const { dataTransfer } = e; - const html = dataTransfer.getData("text/html"); - const uri = dataTransfer.getData("text/uri-list"); - const url = uri || html || this.webBoxUrl || ""; - const newurl = url.startsWith(window.location.origin) ? - url.replace(window.location.origin, this.webBoxUrl?.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; - this.submitWebUrl(newurl); - e.stopPropagation(); - } - onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { - e.key === "Enter" && this.submitWebUrl(this._keyInput.current!.value); - e.stopPropagation(); - } - submitWebUrl = (url: string) => this.selectedDocumentView?.ComponentView?.submitURL?.(url); - webUrlForward = () => this.selectedDocumentView?.ComponentView?.forward?.(); - webUrlBack = () => this.selectedDocumentView?.ComponentView?.back?.(); - - private _keyInput = React.createRef<HTMLInputElement>(); - - @computed get urlEditor() { - return ( - <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()} > - <input className="collectionMenu-urlInput" key={this.webBoxUrl} - placeholder="ENTER URL" - defaultValue={this.webBoxUrl} - onDrop={this.onWebUrlDrop} - onDragOver={e => e.preventDefault()} - onKeyDown={this.onWebUrlValueKeyDown} - onClick={(e) => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> - <button className="submitUrl" onClick={() => this.submitWebUrl(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={this.webUrlBack}> - <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> - </button> - <button className="submitUrl" onClick={this.webUrlForward}> - <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> - </button> - </div> - </div> - ); - } - - @observable viewType = this.selectedDoc?._viewType; render() { @@ -811,9 +758,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionMenu </> : null} - {!this.props.isOverlay || this.document.type !== DocumentType.WEB || this.isText || this.props.isDoc ? (null) : - this.urlEditor - } + {!this.selectedDocumentView?.ComponentView?.menuControls ? (null) : this.selectedDocumentView?.ComponentView?.menuControls?.()} {!this.isText ? <> {this.drawButtons} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index ec6458d00..0182c2625 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -10,7 +10,7 @@ import { ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { ImageField } from "../../../fields/URLField"; import { TraceMobx } from "../../../fields/util"; -import { emptyFunction, setupMoveUpEvents, returnFalse } from "../../../Utils"; +import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentType } from "../../documents/DocumentTypes"; import { DragManager } from "../../util/DragManager"; @@ -294,22 +294,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.yMargin, 5); const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); const evContents = heading ? heading : this.props?.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`; - const headerEditableViewProps = { - GetValue: () => evContents, - SetValue: this.headingChanged, - contents: evContents, - oneLine: true, - HeadingObject: this.props.headingObject, - toggle: this.toggleVisibility, - }; - const newEditableViewProps = { - GetValue: () => "", - SetValue: this.addDocument, - textCallback: this.textCallback, - contents: "+ NEW", - HeadingObject: this.props.headingObject, - toggle: this.toggleVisibility, - }; const headingView = this.props.headingObject ? <div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef} style={{ @@ -325,7 +309,12 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""} style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit" }}> - <EditableView {...headerEditableViewProps} /> + <EditableView + GetValue={() => evContents} + SetValue={this.headingChanged} + contents={evContents} + oneLine={true} + toggle={this.toggleVisibility} /> {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) : <div className="collectionStackingView-sectionColor"> <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}> @@ -373,7 +362,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled' && type !== DocumentType.PRES) ? <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: style.columnWidth / style.numGroupColumns, marginBottom: 10 }}> - <EditableView {...newEditableViewProps} menuCallback={this.menuCallback} /> + <EditableView + GetValue={returnEmptyString} + SetValue={this.addDocument} + textCallback={this.textCallback} + contents={"+ NEW"} + toggle={this.toggleVisibility} + menuCallback={this.menuCallback} /> </div> : null} </div> } diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index cd91cbf63..1f6322997 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -8,7 +8,7 @@ import { RichTextField } from "../../../fields/RichTextField"; import { listSpec } from "../../../fields/Schema"; import { ComputedField, ScriptField } from "../../../fields/ScriptField"; import { Cast, NumCast, StrCast } from "../../../fields/Types"; -import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils"; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents, returnEmptyString } from "../../../Utils"; import { Docs, DocUtils } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { Scripting } from "../../util/Scripting"; @@ -62,11 +62,10 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { async componentDidMount() { this.props.setContentView?.(this); - const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); + //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); "; - const childText = "openInLightbox(self, shiftKey); "; runInAction(() => { - this._childClickedScript = ScriptField.MakeScript(childText, { this: Doc.name, shiftKey: "boolean" }, { detailView: detailView! }); + this._childClickedScript = ScriptField.MakeScript("openInLightbox(self, shiftKey)", { this: Doc.name, shiftKey: "boolean" });//, { detailView: detailView! }); this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" }); }); } @@ -193,22 +192,22 @@ export class CollectionTimeView extends CollectionSubView(doc => doc) { } @computed get pivotKeyUI() { - const newEditableViewProps = { - GetValue: () => "", - SetValue: (value: any) => { - if (value?.length) { - this.layoutDoc._pivotField = value; - return true; - } - return false; - }, - showMenuOnLoad: true, - contents: ":" + StrCast(this.layoutDoc._pivotField), - toggle: this.toggleVisibility, - color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; - }; return <div className={"pivotKeyEntry"}> - <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} /> + <EditableView + GetValue={returnEmptyString} + SetValue={(value: any) => { + if (value?.length) { + this.layoutDoc._pivotField = value; + return true; + } + return false; + }} + toggle={this.toggleVisibility} + background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb"; + contents={":" + StrCast(this.layoutDoc._pivotField)} + showMenuOnLoad={true} + display={"inline"} + menuCallback={this.menuCallback} /> </div>; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index b0c3064dc..9e312b03f 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -206,7 +206,7 @@ export class const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false; return TreeView.GetChildElements(this.treeChildren, this, this.doc, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove, - moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.styleProvider, this.props.ScreenToLocalTransform, + moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.styleProvider, returnTrue, this.props.ScreenToLocalTransform, this.outerXf, this.active, this.panelWidth, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields), BoolCast(this.doc.treeViewPreventOpen), [], this.props.onCheckedClick, this.onChildClick, this.props.treeViewSkipFields, true, this.whenActiveChanged, this.props.dontRegisterView || Cast(this.props.Document.dontRegisterChildViews, "boolean", null), this); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 9ae469930..f305174f1 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -211,14 +211,7 @@ export class CollectionView extends Touchable<CollectionViewProps> { } const first = doc instanceof Doc ? doc : doc[0]; if (!first?._stayInCollection && addDocument !== returnFalse) { - if (UndoManager.RunInTempBatch(() => this.removeDocument(doc))) { - const added = addDocument(doc); - if (!added) UndoManager.UndoTempBatch(); - else UndoManager.ClearTempBatch(); - - return added; - } - UndoManager.ClearTempBatch(); + return UndoManager.RunInTempBatch(() => this.removeDocument(doc) && addDocument(doc)); } return false; } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index c1a1a6f04..3f8794665 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -9,7 +9,6 @@ import * as ReactDOM from 'react-dom'; import { DataSym, Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../fields/Doc"; import { Id } from '../../../fields/FieldSymbols'; import { FieldId } from "../../../fields/RefField"; -import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast, StrCast } from "../../../fields/Types"; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, Utils } from "../../../Utils"; @@ -46,22 +45,14 @@ export class TabDocView extends React.Component<TabDocViewProps> { _mainCont: HTMLDivElement | null = null; _tabReaction: IReactionDisposer | undefined; @observable _activated: boolean = false; - - @observable private _panelWidth = 0; - @observable private _panelHeight = 0; - @observable private _isActive: boolean = false; - @observable private _document: Doc | undefined; - @observable private _view: DocumentView | undefined; + @observable _panelWidth = 0; + @observable _panelHeight = 0; + @observable _isActive: boolean = false; + @observable _document: Doc | undefined; + @observable _view: DocumentView | undefined; @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); } - @computed get renderBounds() { - const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; - const xbounds = bounds[2] - bounds[0]; - const ybounds = bounds[3] - bounds[1]; - const dim = Math.max(xbounds, ybounds); - return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; - } get stack() { return (this.props as any).glContainer.parent.parent; } get tab() { return (this.props as any).glContainer.tab; } @@ -221,19 +212,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { } } - /** - * Adds a document to the presentation view - **/ - @undoBatch - @action - public static UnpinDoc(doc: Doc) { - const curPres = CurrentUserUtils.ActivePresentation; - if (curPres) { - const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)); - ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]); - } - } - componentDidMount() { const selected = () => SelectionManager.Views().some(v => v.props.Document === this._document); new _global.ResizeObserver(action((entries: any) => { @@ -290,84 +268,15 @@ export class TabDocView extends React.Component<TabDocViewProps> { } } - childLayoutTemplate = () => Cast(this._document?.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this._document?._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - const doc = this._document; - const miniSize = this.returnMiniSize(); - doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * this.renderBounds.dim, this.renderBounds.l, this.renderBounds.l + this.renderBounds.dim); - doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * this.renderBounds.dim, this.renderBounds.t, this.renderBounds.t + this.renderBounds.dim); - return false; - }), emptyFunction, emptyFunction); - } getCurrentFrame = () => { return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); } - renderMiniMap() { - const miniWidth = this.PanelWidth() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; - const miniHeight = this.PanelHeight() / NumCast(this._document?._viewScale, 1) / this.renderBounds.dim * 100; - const miniLeft = 50 + (NumCast(this._document?._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; - const miniTop = 50 + (NumCast(this._document?._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; - const miniSize = this.returnMiniSize(); - return <> - <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.tabColor }}> - <CollectionFreeFormView - Document={this._document!} - CollectionView={undefined} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - parentActive={returnFalse} - docViewPath={returnEmptyDoclist} - 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 - active={returnTrue} - select={emptyFunction} - dropAction={undefined} - isSelected={returnFalse} - dontRegisterView={true} - fieldKey={Doc.LayoutFieldKey(this._document!)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - ScreenToLocalTransform={Transform.Identity} - renderDepth={0} - whenActiveChanged={emptyFunction} - focus={DocUtils.DefaultFocus} - styleProvider={TabDocView.miniStyleProvider} - layerProvider={undefined} - addDocTab={this.addDocTab} - pinToPres={TabDocView.PinDoc} - docFilters={CollectionDockingView.Instance.docFilters} - docRangeFilters={CollectionDockingView.Instance.docRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} - fitContentsToDoc={returnTrue} - /> - <div className="miniOverlay" onPointerDown={this.miniDown} > - <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} /> - </div> - </div> - - <Tooltip title={<div className="dash-tooltip">{"toggle minimap"}</div>}> - <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} - style={{ background: DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor) }} > - <FontAwesomeIcon icon={"globe-asia"} size="lg" /> - </div> - </Tooltip> - </>; - } @action focusFunc = (doc: Doc, options?: DocFocusOptions) => { - const vals = (!options?.originalTarget || options?.originalTarget === this._document) && this.view?.ComponentView?.freeformData?.(true); - if (vals && this._document) { + const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; + if (shrinkwrap && this._document) { const focusSpeed = 1000; - this._document._panX = vals.panX; - this._document._panY = vals.panY; - this._document._viewScale = vals.scale; + shrinkwrap(); this._document._viewTransition = `transform ${focusSpeed}ms`; setTimeout(action(() => { this._document!._viewTransition = undefined; @@ -387,21 +296,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { } PanelWidth = () => this._panelWidth; PanelHeight = () => this._panelHeight; + miniMapColor = () => this.tabColor; + tabView = () => this._view; - static miniStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string): any => { - if (doc) { - switch (property.split(":")[0]) { - default: return DefaultStyleProvider(doc, props, property); - case StyleProp.PointerEvents: return "none"; - case StyleProp.DocContents: - const background = doc.type === DocumentType.PDF ? "red" : doc.type === DocumentType.IMG ? "blue" : doc.type === DocumentType.RTF ? "orange" : - doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : "gray"; - return doc.type === DocumentType.COL ? - undefined : - <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: "absolute", display: "block", background }} />; - } - } - } @computed get layerProvider() { return this._document && DefaultLayerProvider(this._document); } @computed get docView() { TraceMobx(); @@ -431,14 +328,18 @@ export class TabDocView extends React.Component<TabDocViewProps> { docViewPath={returnEmptyDoclist} bringToFront={emptyFunction} pinToPres={TabDocView.PinDoc} /> - {this._document._viewType !== CollectionViewType.Freeform ? (null) : - <>{this._document.hideMinimap ? (null) : this.renderMiniMap()} - <Tooltip key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> - <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > - <FontAwesomeIcon icon={"globe-asia"} size="lg" /> - </div> - </Tooltip> - </>} + <TabMinimapView key="minimap" + addDocTab={this.addDocTab} + PanelHeight={this.PanelHeight} + PanelWidth={this.PanelWidth} + background={this.miniMapColor} + document={this._document} + tabView={this.tabView} /> + <Tooltip style={{ display: this._document?._viewType !== CollectionViewType.Freeform ? "none" : undefined }} key="ttip" title={<div className="dash-tooltip">{"toggle minimap"}</div>}> + <div className="miniMap-hidden" onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + <FontAwesomeIcon icon={"globe-asia"} size="lg" /> + </div> + </Tooltip> </>; } @@ -454,4 +355,99 @@ export class TabDocView extends React.Component<TabDocViewProps> { </div > ); } +} + +interface TabMinimapViewProps { + document: Doc; + tabView: () => DocumentView | undefined; + addDocTab: (doc: Doc, where: string) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; + background: () => string; +} +@observer +export class TabMinimapView extends React.Component<TabMinimapViewProps> { + static miniStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string): any => { + if (doc) { + switch (property.split(":")[0]) { + default: return DefaultStyleProvider(doc, props, property); + case StyleProp.PointerEvents: return "none"; + case StyleProp.DocContents: + const background = doc.type === DocumentType.PDF ? "red" : doc.type === DocumentType.IMG ? "blue" : doc.type === DocumentType.RTF ? "orange" : + doc.type === DocumentType.VID ? "purple" : doc.type === DocumentType.WEB ? "yellow" : "gray"; + return doc.type === DocumentType.COL ? + undefined : + <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: "absolute", display: "block", background }} />; + } + } + } + @computed get renderBounds() { + const bounds = this.props.tabView()?.ComponentView?.freeformData?.(true)?.bounds; + if (!bounds) return undefined; + const xbounds = bounds.r - bounds.x; + const ybounds = bounds.b - bounds.y; + const dim = Math.max(xbounds, ybounds); + return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; + } + childLayoutTemplate = () => Cast(this.props.document.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this.props.document._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + const doc = this.props.document; + const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; + const miniSize = this.returnMiniSize(); + doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); + doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + render() { + if (!this.renderBounds) return (null); + const miniWidth = this.props.PanelWidth() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; + const miniHeight = this.props.PanelHeight() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; + const miniLeft = 50 + (NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; + const miniTop = 50 + (NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; + const miniSize = this.returnMiniSize(); + return this.props.document.hideMinimap ? (null) : + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> + <CollectionFreeFormView + Document={this.props.document} + CollectionView={undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + parentActive={returnFalse} + docViewPath={returnEmptyDoclist} + 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 + active={returnTrue} + select={emptyFunction} + dropAction={undefined} + isSelected={returnFalse} + dontRegisterView={true} + fieldKey={Doc.LayoutFieldKey(this.props.document)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + ScreenToLocalTransform={Transform.Identity} + renderDepth={0} + whenActiveChanged={emptyFunction} + focus={DocUtils.DefaultFocus} + styleProvider={TabMinimapView.miniStyleProvider} + layerProvider={undefined} + addDocTab={this.props.addDocTab} + pinToPres={TabDocView.PinDoc} + docFilters={CollectionDockingView.Instance.docFilters} + docRangeFilters={CollectionDockingView.Instance.docRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + fitContentsToDoc={returnTrue} + /> + <div className="miniOverlay" onPointerDown={this.miniDown} > + <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% `, }} /> + </div> + </div>; + } }
\ No newline at end of file diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 184d5814a..0852b2a46 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -39,19 +39,20 @@ export interface TreeViewProps { containingCollection: Doc; prevSibling?: Doc; renderDepth: number; - removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; - moveDocument: DragManager.MoveFunction; dropAction: dropActionType; addDocTab: (doc: Doc, where: string) => boolean; pinToPres: (document: Doc) => void; panelWidth: () => number; panelHeight: () => number; addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean; + removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined; + moveDocument: DragManager.MoveFunction; indentDocument?: (editTitle: boolean) => void; outdentDocument?: (editTitle: boolean) => void; ScreenToLocalTransform: () => Transform; dontRegisterView?: boolean; styleProvider?: StyleProviderFunc | undefined; + layerProvider?: undefined | ((doc: Doc, assign?: boolean) => boolean); outerXf: () => { translateX: number, translateY: number }; treeView: CollectionTreeView; parentKey: string; @@ -114,11 +115,11 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get fieldKey() { TraceMobx(); const splits = StrCast(Doc.LayoutField(this.doc)).split("fieldKey={\'"); return splits.length > 1 ? splits[1].split("\'")[0] : "data"; } @computed get childDocs() { TraceMobx(); return this.childDocList(this.fieldKey); } @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().length && SelectionManager.Views()[0].props.Document === this.props.document; } childDocList(field: string) { - if (this.fileSysMode && !this.doc.isFolder) return [] as Doc[]; const layout = Doc.LayoutField(this.doc) instanceof Doc ? Doc.LayoutField(this.doc) as Doc : undefined; return ((this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields @@ -263,20 +264,15 @@ export class TreeView extends React.Component<TreeViewProps> { e.stopPropagation(); if (docDragData.draggedDocuments[0] === this.doc) return true; const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - let addDoc = parentAddDoc; - if (inside) { - const localAdd = (doc: Doc) => { - const added = Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - added && (doc.context = this.doc.context); - return added; - }; - addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce( - (flg: boolean, doc) => flg && localAdd(doc), true) || parentAddDoc(doc); - } + const canAdd = !StrCast((inside ? this.props.document : this.props.containingCollection)?.freezeChildren).includes("add") || docDragData.treeViewDoc === this.props.treeView.props.Document; + 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 = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument; - return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false); + if (canAdd) { + UndoManager.RunInTempBatch(() => docDragData.droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) : addDoc(d)) || added, false)); + } } - return false; } refTransform = (ref: HTMLDivElement | undefined | null) => { @@ -332,7 +328,7 @@ export class TreeView extends React.Component<TreeViewProps> { const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] : DocListCast(contents), this.props.treeView, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, + this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.layerProvider, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenActiveChanged, this.props.dontRegisterView, this); } else { @@ -380,14 +376,14 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderContent() { TraceMobx(); const expandKey = this.treeViewExpandedView; - if (["links", "annotations", this.fieldKey].includes(expandKey)) { + if (["links", "annotations", "aliases", this.fieldKey].includes(expandKey)) { const key = expandKey === "annotations" ? this.fieldKey + "-annotations" : expandKey; const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key); const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => { // if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't), // then the modification would be done here const ordering = StrCast(this.doc[this.fieldKey + "-sortCriteria"]); - if (ordering === "zorder") { + if (ordering === "Z") { const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering); doc.zIndex = addBefore ? (before ? NumCast(addBefore.zIndex) - 0.5 : NumCast(addBefore.zIndex) + 0.5) : 1000; docs.push(doc); @@ -399,22 +395,23 @@ export class TreeView extends React.Component<TreeViewProps> { return added; }; const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); - const docs = expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; + const docs = expandKey === "aliases" ? this.childAliases : expandKey === "links" ? this.childLinks : expandKey === "annotations" ? this.childAnnos : this.childDocs; const sortKey = `${this.fieldKey}-sortCriteria`; - return <ul key={expandKey + "more"} className={this.doc.treeViewHideTitle ? "no-indent" : ""} onClick={(e) => { - if (this.props.active()) { - !this.outlineMode && (this.doc[sortKey] = - (this.doc[sortKey] === "ascending" ? "descending" : - (this.doc[sortKey] === "descending" ? "zorder" : - (this.doc[sortKey] === "zorder" ? undefined : - "ascending")))); - e.stopPropagation(); - } - }}> + let downX = 0, downY = 0; + const sortings = ["up", "down", "Z", undefined]; + const curSort = Math.max(0, sortings.indexOf(Cast(this.doc[sortKey], "string", null))); + return <ul key={expandKey + "more"} title={"sort: " + sortings[curSort]} className={this.doc.treeViewHideTitle ? "no-indent" : ""} + onPointerDown={e => { downX = e.clientX; downY = e.clientY; e.stopPropagation(); }} + onClick={(e) => { + if (this.props.active() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) { + !this.outlineMode && (this.doc[sortKey] = sortings[(curSort + 1) % sortings.length]); + e.stopPropagation(); + } + }}> {!docs ? (null) : TreeView.GetChildElements(docs, this.props.treeView, this.layoutDoc, this.dataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.ScreenToLocalTransform, + StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, this.props.addDocTab, this.props.pinToPres, this.titleStyleProvider, this.props.layerProvider, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen, [...this.props.renderedIds, this.doc[Id]], this.props.onCheckedClick, this.props.onChildClick, this.props.skipFields, false, this.props.whenActiveChanged, this.props.dontRegisterView, this)} </ul >; @@ -425,7 +422,7 @@ export class TreeView extends React.Component<TreeViewProps> { </div> </ul>; } - return <ul>{this.renderEmbeddedDocument(false)}</ul>; + return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout" } get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @@ -464,9 +461,9 @@ export class TreeView extends React.Component<TreeViewProps> { <div className={`treeView-${this.onCheckedClick ? "checkIcon" : "expandIcon"}`}> <FontAwesomeIcon size="sm" icon={ checked === "check" ? "check" : - (checked === "x" ? "times" : checked === "unchecked" ? "square" : - !this.treeViewOpen ? "caret-right" : - "caret-down")} /> + checked === "x" ? "times" : + checked === "unchecked" ? "square" : + !this.treeViewOpen ? "caret-right" : "caret-down"} /> </div> {this.onCheckedClick ? (null) : <FontAwesomeIcon icon={iconType} />} </div> @@ -480,7 +477,8 @@ export class TreeView extends React.Component<TreeViewProps> { <span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView} onPointerDown={action(() => { if (this.fileSysMode) { - this.doc.treeViewExpandedView = this.doc.isFolder ? this.fieldKey : this.treeViewExpandedView === "layout" ? "fields" : "layout"; + this.doc.treeViewExpandedView = this.doc.isFolder ? this.fieldKey : this.treeViewExpandedView === "layout" ? "fields" : + this.treeViewExpandedView === "fields" ? "aliases" : "layout"; } else if (this.treeViewOpen) { this.doc.treeViewExpandedView = this.treeViewLockExpandedView ? this.doc.treeViewExpandedView : this.treeViewExpandedView === this.fieldKey ? (Doc.UserDoc().noviceMode || this.outlineMode ? "layout" : "fields") : @@ -552,6 +550,7 @@ export class TreeView extends React.Component<TreeViewProps> { oneLine={true} display={"inline-block"} editing={true} + background={"#7089bb"} contents={StrCast(this.doc.title)} height={12} sizeToContent={true} @@ -571,7 +570,7 @@ export class TreeView extends React.Component<TreeViewProps> { : <DocumentView key="title" ref={action((r: any) => { this._docRef = r ? r : undefined; - if (this._docRef && TreeView._editTitleOnLoad?.id === this.props.document[Id] && TreeView._editTitleOnLoad.parent == this.props.parentTreeView) { + if (this._docRef && TreeView._editTitleOnLoad?.id === this.props.document[Id] && TreeView._editTitleOnLoad.parent === this.props.parentTreeView) { this._docRef.select(false); this.setEditTitle(this._docRef); TreeView._editTitleOnLoad = undefined; @@ -581,7 +580,7 @@ export class TreeView extends React.Component<TreeViewProps> { DataDoc={undefined} scriptContext={this} styleProvider={this.titleStyleProvider} - layerProvider={undefined} + layerProvider={this.props.layerProvider} docViewPath={returnEmptyDoclist} treeViewDoc={this.props.treeView.props.Document} addDocument={undefined} @@ -674,7 +673,7 @@ export class TreeView extends React.Component<TreeViewProps> { renderDepth={this.props.renderDepth + 1} rootSelected={returnTrue} styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} - layerProvider={undefined} + layerProvider={this.props.layerProvider} docViewPath={this.props.treeView.props.docViewPath} docFilters={returnEmptyFilter} docRangeFilters={returnEmptyFilter} @@ -703,7 +702,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBorder() { const sorting = this.doc[`${this.fieldKey}-sortCriteria`]; return <div className={`treeView-border${this.outlineMode ? "outline" : ""}`} - style={{ borderColor: sorting === undefined ? undefined : sorting === "ascending" ? "crimson" : sorting === "descending" ? "blue" : "green" }}> + style={{ borderColor: sorting === undefined ? undefined : sorting === "up" ? "crimson" : sorting === "down" ? "blue" : "green" }}> {!this.treeViewOpen ? (null) : this.renderContent} </div>; } @@ -751,10 +750,10 @@ export class TreeView extends React.Component<TreeViewProps> { } }; docs.sort(function (d1, d2): 0 | 1 | -1 { - const a = (criterion === "ascending" ? d2 : d1); - const b = (criterion === "ascending" ? d1 : d2); - const first = a[criterion === "zorder" ? "zIndex" : "title"]; - const second = b[criterion === "zorder" ? "zIndex" : "title"]; + const a = (criterion === "up" ? d2 : d1); + const b = (criterion === "up" ? d1 : d2); + const first = a[criterion === "Z" ? "zIndex" : "title"]; + const second = b[criterion === "Z" ? "zIndex" : "title"]; if (typeof first === 'number' && typeof second === 'number') return (first - second) > 0 ? 1 : -1; if (typeof first === 'string' && typeof second === 'string') return sortAlphaNum(first, second); return criterion ? 1 : -1; @@ -772,12 +771,13 @@ export class TreeView extends React.Component<TreeViewProps> { parentCollectionDoc: Doc | undefined, parentPrevSibling: Doc | undefined, add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, - remove: ((doc: Doc | Doc[]) => boolean), + remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, dropAction: dropActionType, addDocTab: (doc: Doc, where: string) => boolean, pinToPres: (document: Doc) => void, styleProvider: undefined | StyleProviderFunc, + layerProvider: undefined | ((doc: Doc, assign?: boolean) => boolean), screenToLocalXf: () => Transform, outerXf: () => { translateX: number, translateY: number }, active: (outsideReaction?: boolean) => boolean, @@ -818,7 +818,7 @@ export class TreeView extends React.Component<TreeViewProps> { newParent.treeViewOpen = true; child.context = treeView.Document; } - } + }; const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); const outdent = parentCollectionDoc?._viewType !== CollectionViewType.Tree ? undefined : ((editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, parentPrevSibling, parentTreeView instanceof TreeView ? parentTreeView.props.parentTreeView : undefined)); const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); @@ -841,6 +841,7 @@ export class TreeView extends React.Component<TreeViewProps> { removeDoc={StrCast(containingCollection.freezeChildren).includes("remove") ? undefined : remove} addDocument={addDocument} styleProvider={styleProvider} + layerProvider={layerProvider} panelWidth={rowWidth} panelHeight={rowHeight} dontRegisterView={dontRegisterView} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index a05c25c9b..eb0538c41 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -191,7 +191,7 @@ .collectionfreeformview-container { // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. touch-action: none; - + transform-origin: top left; .collectionfreeformview-placeholder { background: gray; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 58288a7b1..4040362d8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -13,7 +13,7 @@ import { ScriptField } from "../../../../fields/ScriptField"; import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { TraceMobx } from "../../../../fields/util"; import { GestureUtils } from "../../../../pen-gestures/GestureUtils"; -import { aggregateBounds, intersectRect, returnFalse, setupMoveUpEvents, Utils } from "../../../../Utils"; +import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from "../../../../Utils"; import { CognitiveServices } from "../../../cognitive_services/CognitiveServices"; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from "../../../documents/Documents"; @@ -57,26 +57,21 @@ export const panZoomSchema = createSchema({ _currentTimecode: "number", _timecodeToShow: "number", _currentFrame: "number", - arrangeInit: ScriptField, _useClusters: "boolean", - fitToBox: "boolean", + _viewTransition: "string", _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set - _viewTransition: "string", - scrollHeight: "number", - fitX: "number", - fitY: "number", - fitW: "number", - fitH: "number" + _fitToBox: "boolean", + scrollHeight: "number" // this will be set when the collection is an annotation overlay for a PDF/Webpage }); type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof collectionSchema, typeof documentSchema, typeof pageSchema]>; const PanZoomDocument = makeInterface(panZoomSchema, collectionSchema, documentSchema, pageSchema); export type collectionFreeformViewProps = { - forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox) + parentActive: (outsideReaction: boolean) => boolean; + annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox) viewDefDivClick?: ScriptField; childPointerEvents?: boolean; - parentActive: (outsideReaction: boolean) => boolean; scaleField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; @@ -86,6 +81,7 @@ export type collectionFreeformViewProps = { export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, Partial<collectionFreeformViewProps>>(PanZoomDocument) { public get displayName() { return "CollectionFreeFormView(" + this.props.Document.title?.toString() + ")"; } // this makes mobx trace() statements more descriptive + private _lastNudge: any; private _lastX: number = 0; private _lastY: number = 0; private _downX: number = 0; @@ -95,57 +91,52 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P private _wordPalette: Map<string, string> = new Map<string, string>(); private _clusterDistance: number = 75; private _hitCluster: number = -1; - private _layoutComputeReaction: IReactionDisposer | undefined; - private _boundsReaction: IReactionDisposer | undefined; + private _disposers: { [name: string]: IReactionDisposer } = {}; private _layoutPoolData = new ObservableMap<string, PoolData>(); private _layoutSizeData = new ObservableMap<string, { width?: number, height?: number }>(); private _cachedPool: Map<string, PoolData> = new Map(); private _lastTap = 0; - private _nudgeTime = 0; - private _thumbIdentifier?: number; - - @observable private _hLines: number[] | undefined; - @observable private _vLines: number[] | undefined; - @observable private _pullCoords: number[] = [0, 0]; - @observable private _pullDirection: string = ""; - @observable private _showAnimTimeline = false; - @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. + + private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } + private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } + private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables + @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 + @observable _hLines: number[] | undefined; + @observable _vLines: number[] | undefined; + @observable _pullCoords: number[] = [0, 0]; + @observable _pullDirection: string = ""; + @observable _showAnimTimeline = false; @observable _clusterSets: (Doc[])[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeRef = React.createRef<HTMLDivElement>(); - @observable _focusFilters: Opt<string[]>; // fields that get overriden by focus anchor - @observable _focusRangeFilters: Opt<string[]>; // fields that get overriden by focus anchor + @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it + @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it + @observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. + @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } + @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } @computed get backgroundActive() { return this.props.layerProvider?.(this.layoutDoc) === false && (this.props.ContainingCollectionView?.active() || this.props.active()); } @computed get fitToContentVals() { return { - panX: (this.contentBounds.x + this.contentBounds.r) / 2, - panY: (this.contentBounds.y + this.contentBounds.b) / 2, + bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 }, scale: !this.childDocs.length ? 1 : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) - } || undefined; + }; } @computed get fitToContent() { return (this.props.fitContentsToDoc?.() || this.Document._fitToBox) && !this.isAnnotationOverlay; } - @computed get parentScaling() { return 1; } @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } @computed get nativeWidth() { return this.fitToContent ? 0 : Doc.NativeWidth(this.Document); } @computed get nativeHeight() { return this.fitToContent ? 0 : Doc.NativeHeight(this.Document); } - private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; } - private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; } - private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } - private panX = () => this.freeformData()?.panX ?? NumCast(this.Document._panX); - private panY = () => this.freeformData()?.panY ?? NumCast(this.Document._panY); - private zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1)) / this.parentScaling; @computed get cachedCenteringShiftX(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / this.parentScaling / scaling; // shift so pan position is at center of window for non-overlay collections + return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; - return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / this.parentScaling / scaling;// shift so pan position is at center of window for non-overlay collections + return this.props.isAnnotationOverlay ? 0 : this.props.PanelHeight() / 2 / scaling;// shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -157,23 +148,44 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform); } - private centeringShiftX = () => this.cachedCenteringShiftX; - private centeringShiftY = () => this.cachedCenteringShiftY; - private getTransform = () => this.cachedGetTransform.copy(); - private getLocalTransform = () => this.cachedGetLocalTransform.copy(); - private getContainerTransform = () => this.cachedGetContainerTransform.copy(); - private getTransformOverlay = () => this.getContainerTransform().translate(1, 1); - private addLiveTextBox = (newBox: Doc) => { + onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); + parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.props.parentActive?.(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; + elementFunc = () => this._layoutElements; + shrinkWrap = () => { + const vals = this.fitToContentVals; + this.layoutDoc._panX = vals.bounds.cx; + this.layoutDoc._panY = vals.bounds.cy; + this.layoutDoc._viewScale = vals.scale; + } + freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined; + freeformDocFilters = () => this._focusFilters || this.docFilters(); + freeformRangeDocFilters = () => this._focusRangeFilters || this.docRangeFilters(); + reverseNativeScaling = () => this.fitToContent ? true : false; + 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)`; + 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); + addLiveTextBox = (newBox: Doc) => { FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed this.addDocument(newBox); } - - addDocument = action((newBox: Doc | Doc[]) => { + selectDocuments = (docs: Doc[]) => { + SelectionManager.DeselectAll(); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); + } + addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - retVal = this.props.addDocument?.(newBox) || false; - retVal && this.bringToFront(newBox); - retVal && this.updateCluster(newBox); + if (retVal = this.props.addDocument?.(newBox) || false) { + this.bringToFront(newBox); + this.updateCluster(newBox); + } } else { retVal = this.props.addDocument?.(newBox) || false; // bcz: deal with clusters @@ -194,25 +206,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } return retVal; - }); - - private selectDocuments = (docs: Doc[]) => { - SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true)); - } - public isCurrent(doc: Doc) { - const dispTime = NumCast(doc._timecodeToShow, -1); - const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); - const curTime = NumCast(this.Document._currentTimecode, -1); - return dispTime === -1 || ((curTime - dispTime) >= -1e-4 && curTime <= endTime); - } - - public getActiveDocuments = () => { - return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); - } - - onExternalDrop = (e: React.DragEvent) => { - return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); } updateGroupBounds = () => { @@ -234,6 +227,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this.layoutDoc.y = pbounds.y; } + isCurrent(doc: Doc) { + const dispTime = NumCast(doc._timecodeToShow, -1); + const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); + const curTime = NumCast(this.Document._currentTimecode, -1); + return dispTime === -1 || ((curTime - dispTime) >= -1e-4 && curTime <= endTime); + } + @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { if (!this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false; @@ -274,18 +274,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @undoBatch - @action internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) { - const dragDoc = annoDragData.dropDocument!; - const dropPos = [NumCast(dragDoc.x), NumCast(dragDoc.y)]; - dragDoc.x = xp - annoDragData.offset[0] + (NumCast(dragDoc.x) - dropPos[0]); - dragDoc.y = yp - annoDragData.offset[1] + (NumCast(dragDoc.y) - dropPos[1]); - this.bringToFront(dragDoc); + annoDragData.dropDocument!.x = xp - annoDragData.offset[0]; + annoDragData.dropDocument!.y = yp - annoDragData.offset[1]; + this.bringToFront(annoDragData.dropDocument!); return true; } @undoBatch - @action internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { if (linkDragData.linkSourceDocument === this.props.Document || this.props.Document.annotationOn) return false; if (!linkDragData.linkSourceDocument.context || StrCast(Cast(linkDragData.linkSourceDocument.context, Doc, null)?.type) === DocumentType.COL) { @@ -302,7 +298,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); if (this.isAnnotationOverlay !== true && de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); @@ -311,6 +306,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return false; } + onExternalDrop = (e: React.DragEvent) => { + return (pt => super.onExternalDrop(e, { x: pt[0], y: pt[1] }))(this.getTransform().transformPoint(e.pageX, e.pageY)); + } + pickCluster(probe: number[]) { return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => { const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1); @@ -332,11 +331,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (ptsParent) { const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === cluster); const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!); - const de = new DragManager.DocumentDragData(eles); - de.moveDocument = this.props.moveDocument; const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 }; + const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? "alias" : undefined); + de.moveDocument = this.props.moveDocument; de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); - de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined; DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, ptsParent.clientX, ptsParent.clientY, { hideSource: !de.dropAction }); return true; } @@ -431,13 +429,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P styleProp = colors[cluster % colors.length]; const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor); // override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document - set && set.filter(s => !StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); - set && set.filter(s => StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); + set?.filter(s => !StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); + set?.filter(s => StrListCast(s.layers).includes(StyleLayers.Background)).map(s => styleProp = StrCast(s.backgroundColor)); } } //else if (doc && NumCast(doc.group, -1) !== -1) styleProp = "gray"; return styleProp; } + trySelectCluster = (addToSel: boolean) => { + if (this._hitCluster !== -1) { + !addToSel && SelectionManager.DeselectAll(); + const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster); + this.selectDocuments(eles); + return true; + } + return false; + } + @action onPointerDown = (e: React.PointerEvent): void => { if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || (Doc.GetSelectedTool() === InkTool.Highlighter || Doc.GetSelectedTool() === InkTool.Pen)) { @@ -610,14 +618,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onClick = (e: React.MouseEvent) => { if ((Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3)) { - if (Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a double click - if (e.shiftKey) { - if (this.layoutDoc.targetScale) { - this.scaleAtPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); - } - e.stopPropagation(); - e.preventDefault(); - } + if (e.shiftKey && Date.now() - this._lastTap < 300) { // reset zoom of freeform view to 1-to-1 on a shift + double click + this.layoutDoc.targetScale && this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); + e.stopPropagation(); + e.preventDefault(); } this._lastTap = Date.now(); } @@ -626,33 +630,24 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P @action pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => { const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, undefined, true); + this.setPan((this.Document._panX || 0) - dx, (this.Document._panY || 0) - dy, 0, true); this._lastX = e.clientX; this._lastY = e.clientY; } @action onPointerMove = (e: PointerEvent): void => { + if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - if (this.props.active(true)) { - e.stopPropagation(); - } - return; - } - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - return; - } - if (!e.cancelBubble) { - if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag + if (this.props.active(true)) e.stopPropagation(); + } else if (!e.cancelBubble) { if (Doc.GetSelectedTool() === InkTool.None) { if (this.tryDragCluster(e, this._hitCluster)) { - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - return; } - (!MarqueeView.DragMarquee || e.altKey) && this.pan(e); + else this.pan(e); } e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers e.preventDefault(); @@ -662,8 +657,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); - const pt = myTouches[0]; - if (pt) { + if (myTouches[0]) { if (Doc.GetSelectedTool() === InkTool.None) { if (this.tryDragCluster(e, this._hitCluster)) { e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers @@ -672,7 +666,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P document.removeEventListener("pointerup", this.onPointerUp); return; } - this.pan(pt); + this.pan(myTouches[0]); } } // e.stopPropagation(); @@ -777,10 +771,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P cleanUpInteractions = () => { switch (this._pullDirection) { - case "left": - case "right": - case "top": - case "bottom": + case "left": case "right": case "top": case "bottom": CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), this._pullDirection); } @@ -806,8 +797,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { deltaScale = 1 / invTransform.Scale; } - const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); @@ -821,24 +812,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming e.stopPropagation(); } - else if (this.props.active(true)) { - if (!e.ctrlKey && MarqueeView.DragMarquee) { - this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true); - e.stopPropagation(); - e.preventDefault(); - } - else if (!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? - } - + else if (this.props.active(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.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]); } @action - setPan(panX: number, panY: number, panType: string = "None", clamp: boolean = false) { + setPan(panX: number, panY: number, panTime: number = 0, clamp: boolean = false) { if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); @@ -862,7 +845,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || CurrentUserUtils.OverlayDocs.includes(this.Document)) { - this.Document._viewTransition = panType; + this._viewTransition = panTime; const scale = this.getLocalTransform().inverse().Scale; const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY)); @@ -871,7 +854,21 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - bringToFront = action((doc: Doc, sendToBack?: boolean) => { + @action + nudge = (x: number, y: number, nudgeTime: number = 500) => { + if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || + this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... + this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), + NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), nudgeTime, true); + this._lastNudge && clearTimeout(this._lastNudge); + this._lastNudge = setTimeout(action(() => this._viewTransition = 0), nudgeTime); + return true; + } + return false; + } + + @action + bringToFront = (doc: Doc, sendToBack?: boolean) => { if (sendToBack || StrListCast(doc.layers).includes(StyleLayers.Background)) { doc.zIndex = 0; } else if (doc.isInkMask) { @@ -886,12 +883,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } doc.zIndex = zlast + 1; } - }); + } - scaleAtPt(docpt: number[], scale: number) { + @action + zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) { if (this.Document._isGroup) return; + setTimeout(action(() => this._viewTransition = 0), this._viewTransition = transitionTime); // set transition to be smooth, then reset const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); - this.Document._viewTransition = "transform 500ms"; this.layoutDoc[this.scaleFieldKey] = scale; const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; @@ -916,27 +914,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P HistoryUtil.pushState(state); } } - const LightboxState = LightboxView.GetSavedState(this.props.Document); SelectionManager.DeselectAll(); if (this.props.Document.scrollHeight) { this.props.focus(doc, options); } else { const xfToCollection = options?.docTransform ?? Transform.Identity(); const layoutdoc = Doc.Layout(doc); - const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey], transition: this.Document._viewTransition }; + const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: this.Document[this.scaleFieldKey] }; const newState = HistoryUtil.getState(); const cantTransform = this.props.isAnnotationOverlay || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); - const { panX, panY, scale } = cantTransform ? savedState : this.setPanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined); + const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(layoutdoc, xfToCollection, options?.willZoom ? options?.scale || .75 : undefined); if (!cantTransform) { // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection newState.initializers![this.Document[Id]] = { panX: panX, panY: panY }; HistoryUtil.pushState(newState); } // focus on the document in the collection const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== undefined); - const focusSpeed = didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0; + const focusSpeed = options?.instant ? 0 : didMove ? (doc.focusSpeed !== undefined ? Number(doc.focusSpeed) : 500) : 0; // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active... if (didMove) { - this.setPan(panX, panY, `transform ${focusSpeed}ms`, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow + this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow scale && (this.Document[this.scaleFieldKey] = scale); } Doc.BrushDoc(doc); @@ -947,11 +944,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P doc.hidden && Doc.UnHighlightDoc(doc); const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing; if (resetView) { - const restoreState = LightboxView.LightboxDoc !== this.props.Document && LightboxState ? LightboxState : savedState; - this.Document._panX = restoreState.panX; - this.Document._panY = restoreState.panY; - this.Document[this.scaleFieldKey] = restoreState.scale; - this.Document._viewTransition = restoreState.transition; + const restoreState = !LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document && savedState; + if (typeof restoreState !== "boolean") { + this.Document._panX = restoreState.panX; + this.Document._panY = restoreState.panY; + this.Document[this.scaleFieldKey] = restoreState.scale; + } + runInAction(() => this._viewTransition = 0); } return resetView; }; @@ -963,14 +962,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1); this.props.focus(cantTransform ? doc : this.rootDoc, { - ...options, docTransform: xf, + ...options, + docTransform: xf, afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), Math.max(0, focusSpeed - (Date.now() - startTime)))) }); } } - setPanIntoView = (doc: Doc, xf: Transform, scale?: number) => { + calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { const pw = this.props.PanelWidth() / NumCast(this.layoutDoc._viewScale, 1); const ph = this.props.PanelHeight() / NumCast(this.layoutDoc._viewScale, 1); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); @@ -982,81 +982,81 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (scale) { const maxZoom = 2; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context - return { panX: (bounds.left + bounds.right) / 2, panY: (bounds.top + bounds.bot) / 2, scale: Math.min(maxZoom, scale * Math.min(this.props.PanelWidth() / Math.abs(pt2[0] - pt[0]), this.props.PanelHeight() / Math.abs(pt2[1] - pt[1]))) }; - } else if ((screen.right - screen.left) < (bounds.right - bounds.left) || + } + if ((screen.right - screen.left) < (bounds.right - bounds.left) || (screen.bot - screen.top) < (bounds.bot - bounds.top)) { return { panX: (bounds.left + bounds.right) / 2, panY: (bounds.top + bounds.bot) / 2, scale: Math.min(this.props.PanelHeight() / (bounds.bot - bounds.top), this.props.PanelWidth() / (bounds.right - bounds.left)) / 1.1 }; - } else { - return { - panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), - }; } - } - - onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); - onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); - parentActive = (outsideReaction: boolean) => this.props.active(outsideReaction) || this.props.parentActive?.(outsideReaction) || this.backgroundActive || this.layoutDoc._viewType === CollectionViewType.Pile ? true : false; - getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps { return { - addDocument: this.props.addDocument, - removeDocument: this.props.removeDocument, - moveDocument: this.props.moveDocument, - pinToPres: this.props.pinToPres, - whenActiveChanged: this.props.whenActiveChanged, - parentActive: this.parentActive, - docViewPath: this.props.docViewPath, - DataDoc: childData, - Document: childLayout, - ContainingCollectionView: this.props.CollectionView, - ContainingCollectionDoc: this.props.Document, - LayoutTemplate: childLayout.z ? undefined : this.props.childLayoutTemplate, - LayoutTemplateString: childLayout.z ? undefined : this.props.childLayoutString, - rootSelected: childData ? this.rootSelected : returnFalse, - onClick: this.onChildClickHandler, - onDoubleClick: this.onChildDoubleClickHandler, - ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform, - PanelWidth: childLayout[WidthSym], - PanelHeight: childLayout[HeightSym], - docFilters: this.freeformDocFilters, - docRangeFilters: this.freeformRangeDocFilters, - searchFilterDocs: this.searchFilterDocs, - focus: this.focusDocument, - styleProvider: this.getClusterColor, - layerProvider: this.props.layerProvider, - freezeDimensions: this.props.childFreezeDimensions, - dropAction: StrCast(this.props.Document.childDropAction) as dropActionType, - bringToFront: this.bringToFront, - addDocTab: this.addDocTab, - renderDepth: this.props.renderDepth + 1, - dontRegisterView: this.props.dontRegisterView, + panX: cx + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), + panY: cy + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot), }; } + getChildDocView(entry: PoolData) { + const childLayout = entry.pair.layout; + const childData = entry.pair.data; + const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); + return <CollectionFreeFormDocumentView key={childLayout[Id] + (entry.replica || "")} + DataDoc={childData} + Document={childLayout} + renderDepth={this.props.renderDepth + 1} + replica={entry.replica} + ContainingCollectionView={this.props.CollectionView} + ContainingCollectionDoc={this.props.Document} + CollectionFreeFormView={this} + LayoutTemplate={childLayout.z ? undefined : this.props.childLayoutTemplate} + LayoutTemplateString={childLayout.z ? undefined : this.props.childLayoutString} + rootSelected={childData ? this.rootSelected : returnFalse} + onClick={this.onChildClickHandler} + onDoubleClick={this.onChildDoubleClickHandler} + ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform} + PanelWidth={childLayout[WidthSym]} + PanelHeight={childLayout[HeightSym]} + docFilters={this.freeformDocFilters} + docRangeFilters={this.freeformRangeDocFilters} + searchFilterDocs={this.searchFilterDocs} + focus={this.focusDocument} + addDocTab={this.addDocTab} + addDocument={this.props.addDocument} + removeDocument={this.props.removeDocument} + moveDocument={this.props.moveDocument} + pinToPres={this.props.pinToPres} + whenActiveChanged={this.props.whenActiveChanged} + parentActive={this.parentActive} + docViewPath={this.props.docViewPath} + styleProvider={this.getClusterColor} + layerProvider={this.props.layerProvider} + dataProvider={this.childDataProvider} + sizeProvider={this.childSizeProvider} + freezeDimensions={this.props.childFreezeDimensions} + dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + bringToFront={this.bringToFront} + dontRegisterView={this.props.dontRegisterView} + pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" : + (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} + jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} + //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this + />; + } + addDocTab = action((doc: Doc, where: string) => { if (where === "inParent") { - if (doc instanceof Doc) { + ((doc instanceof Doc) ? [doc] : doc).forEach(doc => { const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; - return this.props.addDocument?.(doc) || false; - } else { - (doc as any as Doc[]).forEach(doc => { - const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); - doc.x = pt[0]; - doc.y = pt[1]; - }); - return this.props.addDocument?.(doc) || false; - } + }); + return this.props.addDocument?.(doc) || false; } if (where === "inPlace" && this.layoutDoc.isInPlaceContainer) { this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]); @@ -1065,12 +1065,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return this.props.addDocTab(doc, where); }); - getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc, docs: Doc[], state: any }): PoolData { + getCalculatedPositions(params: { pair: { layout: Doc, data?: Doc }, index: number, collection: Doc }): PoolData { const layoutDoc = Doc.Layout(params.pair.layout); + const { z, color, zIndex } = params.pair.layout; const { x, y, opacity } = this.Document._currentFrame === undefined ? { x: params.pair.layout.x, y: params.pair.layout.y, opacity: this.props.styleProvider?.(params.pair.layout, this.props, StyleProp.Opacity) } : - CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame || 0); - const { z, color, zIndex } = params.pair.layout; + CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document._currentFrame); return { x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"), transition: StrCast(layoutDoc.dataTransition), opacity: this.Document.editing ? 1 : Cast(opacity, "number", null), @@ -1078,15 +1078,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }; } - viewDefsToJSX = (views: ViewDefBounds[]) => { - return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); - } - onViewDefDivClick = (e: React.MouseEvent, payload: any) => { (this.props.viewDefDivClick || ScriptCast(this.props.Document.onViewDefDivClick))?.script.run({ this: this.props.Document, payload }); e.stopPropagation(); } - private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { + + viewDefsToJSX = (views: ViewDefBounds[]) => { + return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!); + } + + viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> { const { x, y, z } = viewDef; const color = StrCast(viewDef.color); const width = Cast(viewDef.width, "number"); @@ -1133,24 +1134,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } doFreeformLayout(poolData: Map<string, PoolData>) { - const layoutDocs = this.childLayoutPairs.map(pair => pair.layout); - const initResult = this.Document.arrangeInit?.script.run({ docs: layoutDocs, collection: this.Document }, console.log); - const state = initResult?.success ? initResult.result.scriptState : undefined; - const elements = initResult?.success ? this.viewDefsToJSX(initResult.result.views) : []; - - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => { - const pos = this.getCalculatedPositions({ pair, index: i, collection: this.Document, docs: layoutDocs, state }); - poolData.set(pair.layout[Id], pos); - }); - return elements; + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => + poolData.set(pair.layout[Id], this.getCalculatedPositions({ pair, index: i, collection: this.Document }))); + return [] as ViewDefResult[]; } @computed get doInternalLayoutComputation() { TraceMobx(); - const newPool = new Map<string, PoolData>(); - const engine = this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); - switch (engine) { + switch (this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine)) { case "pass": return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) }; case "timeline": return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) }; case "pivot": return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) }; @@ -1177,24 +1169,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); - const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry => elements.push({ - ele: <CollectionFreeFormDocumentView - key={entry[1].pair.layout[Id] + (entry[1].replica || "")} - {...this.getChildDocumentViewProps(entry[1].pair.layout, entry[1].pair.data)} - replica={entry[1].replica} - CollectionFreeFormView={this} - dataProvider={this.childDataProvider} - sizeProvider={this.childSizeProvider} - layerProvider={this.props.layerProvider} - pointerEvents={this.backgroundActive || this.props.childPointerEvents ? - "all" : - (this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : undefined} - jitterRotation={NumCast(this.props.Document._jitterRotation) || ((Doc.UserDoc().renderStyle === "comic" ? 10 : 0))} - //fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this - freezeDimensions={BoolCast(this.props.childFreezeDimensions)} - />, + ele: this.getChildDocView(entry[1]), bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica) })); @@ -1207,8 +1184,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return elements; } - freeformDocFilters = () => this._focusFilters || this.docFilters(); - freeformRangeDocFilters = () => this._focusRangeFilters || this.docRangeFilters(); @action setViewSpec = (anchor: Doc, preview: boolean) => { if (preview) { @@ -1230,7 +1205,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); const proto = Doc.GetProto(anchor); proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType; - proto[ViewSpecPrefix + "_docFilters"] = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]); + proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]); if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) { Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor); } else { @@ -1238,36 +1213,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } return anchor; } - freeformData = (force?: boolean) => this.fitToContent || force ? this.fitToContentVals : undefined; + @action componentDidMount() { super.componentDidMount?.(); this.props.setContentView?.(this); - this._layoutComputeReaction = reaction(() => this.doLayoutComputation, + this._disposers.layoutComputation = reaction(() => this.doLayoutComputation, (elements) => this._layoutElements = elements || [], { fireImmediately: true, name: "doLayout" }); - if (!this.props.isAnnotationOverlay) { - this._boundsReaction = reaction(() => this.contentBounds, - bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => { - const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]); - if (rbounds[0] !== bounds.x || rbounds[1] !== bounds.y || rbounds[2] !== bounds.r || rbounds[3] !== bounds.b) { - this.Document._renderContentBounds = new List<number>([bounds.x, bounds.y, bounds.r, bounds.b]); - } - })); - } this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } componentWillUnmount() { - this._layoutComputeReaction?.(); - this._boundsReaction?.(); + Object.values(this._disposers).forEach(disposer => disposer?.()); this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any); } - @computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); } - elementFunc = () => this._layoutElements; - @action onCursorMove = (e: React.PointerEvent) => { // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); @@ -1293,7 +1255,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P e.stopPropagation(); } - promoteCollection = undoBatch(action(() => { + @undoBatch + promoteCollection = () => { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); @@ -1302,40 +1265,26 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }); this.props.addDocTab(childDocs as any as Doc, "inParent"); this.props.ContainingCollectionView?.removeDocument(this.props.Document); - })); + } @undoBatch - layoutDocsInGrid = action(() => { - const docs = this.childLayoutPairs; - const startX = this.Document._panX || 0; - let x = startX; - let y = this.Document._panY || 0; - let i = 0; - const width = Math.max(...docs.map(doc => NumCast(doc.layout._width))); - const height = Math.max(...docs.map(doc => NumCast(doc.layout._height))); - docs.forEach(pair => { - pair.layout.x = x; - pair.layout.y = y; - x += width + 20; - if (++i === 6) { - i = 0; - x = startX; - y += height + 20; - } + layoutDocsInGrid = () => { + const docs = this.childLayoutPairs.map(pair => pair.layout); + const width = Math.max(...docs.map(doc => NumCast(doc._width))) + 20; + const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20; + const dim = Math.ceil(Math.sqrt(docs.length)); + docs.forEach((doc, i) => { + doc.x = (this.Document._panX || 0) + (i % dim) * width - width * dim / 2; + doc.y = (this.Document._panY || 0) + Math.floor(i / dim) * height - height * dim / 2; }); - }); + } @undoBatch - @action - toggleNativeDimensions = () => { - Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight); - } + toggleNativeDimensions = () => Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight) @undoBatch @action - toggleLockTransform = (): void => { - this.layoutDoc._lockedTransform = this.layoutDoc._lockedTransform ? undefined : true; - } + toggleLockTransform = () => this.layoutDoc._lockedTransform = this.layoutDoc._lockedTransform ? undefined : true onContextMenu = (e: React.MouseEvent) => { if (this.props.isAnnotationOverlay || this.props.Document.annotationOn || !ContextMenu.Instance) return; @@ -1437,39 +1386,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P e.stopPropagation(); } - private childViews = () => { - const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; - return [ - ...children, - ...this.views, - ]; - } - children = () => { - const eles: JSX.Element[] = []; - eles.push(...this.childViews()); - eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />); - return eles; - } - - @computed get placeholder() { - return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> - </div>; + const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : []; + return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />]; } - nudge = action((x: number, y: number) => { - if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || - this.props.ContainingCollectionDoc._panX !== undefined) { // bcz: this isn't ideal, but want to try it out... - this.setPan(NumCast(this.layoutDoc._panX) + this.props.PanelWidth() / 2 * x / this.zoomScaling(), - NumCast(this.layoutDoc._panY) + this.props.PanelHeight() / 2 * (-y) / this.zoomScaling(), "transform 500ms", true); - this._nudgeTime = Date.now(); - setTimeout(() => (Date.now() - this._nudgeTime >= 500) && (this.Document._viewTransition = undefined), 500); - return true; - } - return false; - }); - chooseGridSpace = (gridSpace: number): number => { const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); @@ -1486,8 +1407,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P ref={(el) => { const ctx = el?.getContext('2d'); if (ctx) { - const Cx = this.centeringShiftX() % renderGridSpace; - const Cy = this.centeringShiftY() % renderGridSpace; + const Cx = this.cachedCenteringShiftX % renderGridSpace; + const Cy = this.cachedCenteringShiftY % renderGridSpace; ctx.lineWidth = Math.min(1, Math.max(0.5, this.zoomScaling())); ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); ctx.clearRect(0, 0, w, h); @@ -1508,14 +1429,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P }} />; } - trySelectCluster = (addToSel: boolean) => { - if (this._hitCluster !== -1) { - !addToSel && SelectionManager.DeselectAll(); - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster); - this.selectDocuments(eles); - return true; - } - return false; + @computed get placeholder() { + return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}> + <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span> + </div>; } @computed get marqueeView() { @@ -1536,14 +1453,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P {this.layoutDoc["_backgroundGrid-show"] && (!SnappingManager.GetIsDragging() || !Doc.UserDoc().showSnapLines) ? this.backgroundGrid : (null)} <CollectionFreeFormViewPannableContents isAnnotationOverlay={this.isAnnotationOverlay} - centeringShiftX={this.centeringShiftX} - centeringShiftY={this.centeringShiftY} + transform={this.contentTransform} + zoomScaling={this.zoomScaling} presPaths={BoolCast(this.Document.presPathView)} progressivize={BoolCast(this.Document.editProgressivize)} presPinView={BoolCast(this.Document.presPinView)} - transition={Cast(this.layoutDoc._viewTransition, "string", null)} - viewDefDivClick={this.props.viewDefDivClick} - zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}> + transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, "string", null)} + viewDefDivClick={this.props.viewDefDivClick}> {this.children} </CollectionFreeFormViewPannableContents> </div> @@ -1552,14 +1468,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } @computed get contentScaling() { - if (this.props.isAnnotationOverlay && !this.props.forceScaling) return 0; + if (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent) return 0; const nw = this.nativeWidth; const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; return wscale < hscale ? wscale : hscale; } - @computed get backgroundEvents() { return this.props.layerProvider?.(this.layoutDoc) === false && SnappingManager.GetIsDragging(); } render() { TraceMobx(); @@ -1575,14 +1490,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P onContextMenu={this.onContextMenu} style={{ pointerEvents: this.backgroundEvents ? "all" : undefined, - transform: this.contentScaling ? `scale(${this.contentScaling})` : "", - transformOrigin: this.contentScaling ? "left top" : "", - width: this.contentScaling ? `${100 / this.contentScaling}%` : "", - height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() + transform: `scale(${this.contentScaling || 1})`, + width: `${100 / (this.contentScaling || 1)}%`, + height: this.isAnnotationOverlay && this.Document.scrollHeight ? this.Document.scrollHeight : `${100 / (this.contentScaling || 1)}%`// : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}> {this.Document._freeformLOD && !this.props.active() && !this.props.isAnnotationOverlay && this.props.renderDepth > 0 ? this.placeholder : this.marqueeView} - {!this.props.noOverlay ? <CollectionFreeFormOverlayView elements={this.elementFunc} /> : (null)} + {this.props.noOverlay ? (null) : <CollectionFreeFormOverlayView elements={this.elementFunc} />} <div className={"pullpane-indicator"} style={{ @@ -1628,10 +1542,7 @@ class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOv } interface CollectionFreeFormViewPannableContentsProps { - centeringShiftX: () => number; - centeringShiftY: () => number; - panX: () => number; - panY: () => number; + transform: () => string; zoomScaling: () => number; viewDefDivClick?: ScriptField; children: () => JSX.Element[]; @@ -1644,67 +1555,45 @@ interface CollectionFreeFormViewPannableContentsProps { @observer class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps>{ - @observable private _drag: string = ''; + @observable _drag: string = ''; //Adds event listener so knows pointer is down and moving onPointerDown = (e: React.PointerEvent): void => { e.stopPropagation(); e.preventDefault(); - const corner = e.target as any; - if (corner) this._drag = corner.id; - const rect = document.getElementById(this._drag); - if (rect) { - setupMoveUpEvents(e.target, e, this.onPointerMove, (e) => { }, (e) => { }); - } - } - - //Removes all event listeners - onPointerUp = (e: PointerEvent): void => { - e.stopPropagation(); - e.preventDefault(); - this._drag = ""; - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); + this._drag = (e.target as any)?.id ?? ""; + document.getElementById(this._drag) && setupMoveUpEvents(e.target, e, this.onPointerMove, emptyFunction, emptyFunction); } //Adjusts the value in NodeStore @action onPointerMove = (e: PointerEvent) => { const doc = document.getElementById('resizable'); - const rect = doc!.getBoundingClientRect(); - const toNumber = (original: number, delta: number): number => { - return original + (delta * this.props.zoomScaling()); - }; + const toNumber = (original: number, delta: number) => original + (delta * this.props.zoomScaling()); if (doc) { - const height = doc.offsetHeight; - const width = doc.offsetWidth; - const top = doc.offsetTop; - const left = doc.offsetLeft; switch (this._drag) { - case "": break; case "resizer-br": - doc.style.width = toNumber(width, e.movementX) + 'px'; - doc.style.height = toNumber(height, e.movementY) + 'px'; + doc.style.width = toNumber(doc.offsetWidth, e.movementX) + 'px'; + doc.style.height = toNumber(doc.offsetHeight, e.movementY) + 'px'; break; case "resizer-bl": - doc.style.width = toNumber(width, -e.movementX) + 'px'; - doc.style.height = toNumber(height, e.movementY) + 'px'; - doc.style.left = toNumber(left, e.movementX) + 'px'; + doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; + doc.style.height = toNumber(doc.offsetHeight, e.movementY) + 'px'; + doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; break; case "resizer-tr": - doc.style.width = toNumber(width, -e.movementX) + 'px'; - doc.style.height = toNumber(height, -e.movementY) + 'px'; - doc.style.top = toNumber(top, e.movementY) + 'px'; + doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; + doc.style.height = toNumber(doc.offsetHeight, -e.movementY) + 'px'; + doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; case "resizer-tl": - doc.style.width = toNumber(width, -e.movementX) + 'px'; - doc.style.height = toNumber(height, -e.movementY) + 'px'; - doc.style.top = toNumber(top, e.movementY) + 'px'; - doc.style.left = toNumber(left, e.movementX) + 'px'; + doc.style.width = toNumber(doc.offsetWidth, -e.movementX) + 'px'; + doc.style.height = toNumber(doc.offsetHeight, -e.movementY) + 'px'; + doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; + doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; case "resizable": - doc.style.top = toNumber(top, e.movementY) + 'px'; - doc.style.left = toNumber(left, e.movementX) + 'px'; + doc.style.top = toNumber(doc.offsetTop, e.movementY) + 'px'; + doc.style.left = toNumber(doc.offsetLeft, e.movementX) + 'px'; } - // this.updateAll(height, width, top, left); return false; } return true; @@ -1715,17 +1604,17 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF const activeItem = PresBox.Instance.activeItem; // const targetDoc = PresBox.Instance.targetDoc; if (activeItem && activeItem.presPinView && activeItem.id) { - const vfLeft: number = NumCast(activeItem.presPinViewX); - const vfTop: number = NumCast(activeItem.presPinViewY); - const vfWidth: number = 100; - const vfHeight: number = 100; + const left = NumCast(activeItem.presPinViewX); + const top = NumCast(activeItem.presPinViewY); + const width = 100; + const height = 100; return !this.props.presPinView ? (null) : - <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width: vfWidth, height: vfHeight, top: vfTop, left: vfLeft, position: 'absolute' }}> + <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width, height, top, left, position: 'absolute' }}> <div className='resizers' key={'resizer' + activeItem.id}> - <div className='resizer top-left' onPointerDown={this.onPointerDown}></div> - <div className='resizer top-right' onPointerDown={this.onPointerDown}></div> - <div className='resizer bottom-left' onPointerDown={this.onPointerDown}></div> - <div className='resizer bottom-right' onPointerDown={this.onPointerDown}></div> + <div className='resizer top-left' onPointerDown={this.onPointerDown} /> + <div className='resizer top-right' onPointerDown={this.onPointerDown} /> + <div className='resizer bottom-left' onPointerDown={this.onPointerDown} /> + <div className='resizer bottom-right' onPointerDown={this.onPointerDown} /> </div> </div>; } @@ -1745,16 +1634,13 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF <div key="presorder">{PresBox.Instance.order}</div> <svg key="svg" className={presPaths}> <defs> - <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" - orient="auto" overflow="visible"> + <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> </marker> - <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" - orient="auto" overflow="visible"> + <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> </marker> - <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" - orient="auto" overflow="visible"> + <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" orient="auto" overflow="visible"> <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> </marker> </defs> @@ -1763,16 +1649,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF </>; } - render() { - // trace(); - const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); - const cenx = this.props.centeringShiftX(); - const ceny = this.props.centeringShiftY(); - const panx = -this.props.panX(); - const pany = -this.props.panY(); - const zoom = this.props.zoomScaling(); - return <div className={freeformclass} + return <div className={"collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none")} onScroll={e => { const target = e.target as any; if (getComputedStyle(target)?.overflow === "visible") { // if collection is visible, then scrolling will mess things up since there are no scroll bars @@ -1780,7 +1658,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF } }} style={{ - transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`, + transform: this.props.transform(), transition: this.props.transition, width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection //willChange: "transform" diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 36d14056b..9e442a8c8 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -28,6 +28,7 @@ import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu"; import "./MarqueeView.scss"; import React = require("react"); import { StyleLayers } from "../../StyleProvider"; +import { TreeView } from "../TreeView"; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -37,7 +38,7 @@ interface MarqueeViewProps { addLiveTextDocument: (doc: Doc) => void; isSelected: () => boolean; trySelectCluster: (addToSel: boolean) => boolean; - nudge?: (x: number, y: number) => boolean; + nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void; } @@ -45,7 +46,6 @@ interface MarqueeViewProps { export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { private _commandExecuted = false; - @observable public static DragMarquee = false; @observable _lastX: number = 0; @observable _lastY: number = 0; @observable _downX: number = 0; @@ -161,8 +161,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque slide.x = x; slide.y = y; FormattedTextBox.SelectOnLoad = slide[Id]; + TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined }; this.props.addDocument?.(slide); - //setTimeout(() => SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(slide)!, false)); e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout && !this.props.childLayoutString ? e.key : ""; @@ -219,8 +219,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = this._lastY = e.clientY; if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; - // allow marquee if right click OR alt+left click OR space bar + left click - if (e.button === 2 || (e.button === 0 && (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))))) { + // allow marquee if right click OR alt+left click + if (e.button === 2 || (e.button === 0 && e.altKey)) { // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { this.setPreviewCursor(e.clientX, e.clientY, true); // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. @@ -250,9 +250,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this } - if (e.altKey || MarqueeView.DragMarquee) { - e.preventDefault(); - } + e.altKey && e.preventDefault(); } @action @@ -289,9 +287,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } this.cleanupInteractions(true, this._commandExecuted); - if (e.altKey || MarqueeView.DragMarquee) { - e.preventDefault(); - } + e.altKey && e.preventDefault(); } clearSelection() { @@ -658,7 +654,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque style={{ overflow: (!this.props.ContainingCollectionView && this.props.isAnnotationOverlay) ? "visible" : StrCast(this.props.Document._overflow), - cursor: MarqueeView.DragMarquee && this ? "crosshair" : "hand" + cursor: "hand" }} onDragOver={e => e.preventDefault()} onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} onClick={this.onClick} onPointerDown={this.onPointerDown}> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index ee4df97ed..f07cbfbf5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -71,19 +71,18 @@ export interface DocFocusOptions { scale?: number; // percent of containing frame to zoom into document afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy + instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) } export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>; export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void; export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps | FieldViewProps>, property: string) => any; export interface DocComponentView { - getAnchor?: () => Doc; + getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus - setViewSpec?: (anchor: Doc, preview: boolean) => void; - back?: () => boolean; - forward?: () => boolean; - url?: () => string; - submitURL?: (url: string) => boolean; - freeformData?: (force?: boolean) => Opt<{ panX: number, panY: number, scale: number }>; + setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. + shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views + menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. } export interface DocumentViewSharedProps { renderDepth: number; @@ -380,8 +379,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps startDragging(x: number, y: number, dropAction: dropActionType) { if (this._mainCont.current) { - const ffview = this.props.CollectionFreeFormDocumentView; - ffview && runInAction(() => (ffview().props.CollectionFreeFormView.ChildDrag = this.props.DocumentView())); const dragData = new DragManager.DocumentDragData([this.props.Document]); const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top); @@ -389,8 +386,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; + const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.layoutDoc.onDragStart }, - () => setTimeout(action(() => ffview && (ffview().props.CollectionFreeFormView.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. + () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. } } @@ -998,8 +997,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get docViewPath() { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } - @computed get nativeWidth() { return returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); } - @computed get nativeHeight() { return returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions) || 0); } + @computed get nativeWidth() { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); } + @computed get nativeHeight() { return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions) || 0); } @computed get nativeScaling() { if (this.nativeWidth && (this.layoutDoc?._fitWidth || this.props.PanelHeight() / this.nativeHeight > this.props.PanelWidth() / this.nativeWidth)) { return this.props.PanelWidth() / this.nativeWidth; // width-limited or fitWidth diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 728e5e02f..4c3031ae2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -381,7 +381,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps, ImageD removeDocument={this.removeDocument} moveDocument={this.moveDocument} addDocument={this.addDocument} - forceScaling={true} + annotationLayerHostsContent={true} focus={this.props.focus} isSelected={this.props.isSelected} select={emptyFunction} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index ce9d8bed5..ebb953dad 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -125,12 +125,9 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { contents={contents} maxHeight={36} height={"auto"} - GetValue={() => { - return Field.toKeyValueString(props.Document, props.fieldKey); - }} + GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => - KeyValueBox.SetField(props.Document, props.fieldKey, value)}> - </EditableView> + KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> </div> </td> </tr> diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 720af6c9d..30b272a9a 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -2,7 +2,6 @@ import React = require("react"); import { observer } from "mobx-react"; import "./LinkDescriptionPopup.scss"; import { observable, action } from "mobx"; -import { EditableView } from "../EditableView"; import { LinkManager } from "../../util/LinkManager"; import { TaskCompletionBox } from "./TaskCompletedBox"; diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 435563255..ac67949f9 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -552,7 +552,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps, VideoD <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit} fieldKey={this.annotationKey} isAnnotationOverlay={true} - forceScaling={true} + annotationLayerHostsContent={true} select={emptyFunction} active={this.annotationsActive} scaling={returnOne} diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 663a8b8e4..2cd6f5f33 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -46,6 +46,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _iframeIndicatorRef = React.createRef<HTMLDivElement>(); private _iframeDragRef = React.createRef<HTMLDivElement>(); + private _keyInput = React.createRef<HTMLInputElement>(); private _ignoreScroll = false; private _initialScroll: Opt<number>; @observable private _marqueeing: number[] | undefined; @@ -262,6 +263,52 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps, WebDocum return true; } + menuControls = () => this.urlEditor; + onWebUrlDrop = (e: React.DragEvent) => { + const { dataTransfer } = e; + const html = dataTransfer.getData("text/html"); + const uri = dataTransfer.getData("text/uri-list"); + const url = uri || html || this._url || ""; + const newurl = url.startsWith(window.location.origin) ? + url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || "") : url; + this.submitURL(newurl); + e.stopPropagation(); + } + onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { + e.key === "Enter" && this.submitURL(this._keyInput.current!.value); + e.stopPropagation(); + } + + @computed get urlEditor() { + return ( + <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()} > + <input className="collectionMenu-urlInput" key={this._url} + placeholder="ENTER URL" + defaultValue={this._url} + onDrop={this.onWebUrlDrop} + onDragOver={e => e.preventDefault()} + onKeyDown={this.onWebUrlValueKeyDown} + onClick={(e) => { + this._keyInput.current!.select(); + e.stopPropagation(); + }} + ref={this._keyInput} + /> + <div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", maxWidth: "250px", }}> + <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> + GO + </button> + <button className="submitUrl" onClick={this.back}> + <FontAwesomeIcon icon="caret-left" size="lg"></FontAwesomeIcon> + </button> + <button className="submitUrl" onClick={this.forward}> + <FontAwesomeIcon icon="caret-right" size="lg"></FontAwesomeIcon> + </button> + </div> + </div> + ); + } + editToggleBtn() { return <Tooltip title={<div className="dash-tooltip" >{`${this.props.Document.isAnnotating ? "Exit" : "Enter"} annotation mode`}</div>}> <div className="webBox-annotationToggle" diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index da7ed9ac7..b4fbda9f4 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -105,13 +105,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp private _disposers: { [name: string]: IReactionDisposer } = {}; private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; - private _pause: boolean = false; private _ignoreScroll = false; @computed get _recording() { return this.dataDoc?.audioState === "recording"; } - set _recording(value) { - this.dataDoc.audioState = value ? "recording" : undefined; - } + set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; } public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; @@ -1651,6 +1648,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); fitToBox = () => this.props.Document._fitToBox; + sidebarAddDocument = (doc: Doc | Doc[]) => this.addDocument(doc, this.SidebarKey); + sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); + sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); @computed get sidebarCollection() { const collectionProps: SubCollectionViewProps & collectionFreeformViewProps = { ...OmitKeys(this.props, ["NativeWidth", "NativeHeight"]).omit, @@ -1662,16 +1662,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp yMargin: 0, chromeStatus: "enabled", scaleField: this.SidebarKey + "-scale", - isAnnotationOverlay: true, + isAnnotationOverlay: false, fieldKey: this.SidebarKey, fitContentsToDoc: this.fitToBox, select: emptyFunction, active: this.annotationsActive, scaling: this.sidebarContentScaling, whenActiveChanged: this.whenActiveChanged, - removeDocument: this.removeDocument, - moveDocument: this.moveDocument, - addDocument: this.addDocument, + removeDocument: this.sidebarRemDocument, + moveDocument: this.sidebarMoveDocument, + addDocument: this.sidebarAddDocument, CollectionView: undefined, ScreenToLocalTransform: this.sidebarScreenToLocal, renderDepth: this.props.renderDepth + 1, diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 7687690b2..ca6dc87ae 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -385,10 +385,12 @@ export class PDFViewer extends ViewBoxAnnotatableComponent<IViewerProps, PdfDocu this._setPreviewCursor?.(e.clientX, e.clientY, true); } if (!e.altKey && e.button === 0 && this.active(true)) { + this._marqueeing = [e.clientX, e.clientY]; if (e.target && ((e.target as any).className.includes("endOfContent") || ((e.target as any).parentElement.className !== "textLayer"))) { - this._marqueeing = [e.clientX, e.clientY]; // if texLayer is hit, then we select text instead of using a marquee document.addEventListener("pointermove", this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called } else { + // if textLayer is hit, then we select text instead of using a marquee so clear out the marquee. + setTimeout(action(() => this._marqueeing = undefined), 100); // bcz: hack .. anchor menu is setup within MarqueeAnnotator so we need to at least create the marqueeAnnotator even though we aren't using it. // clear out old marquees and initialize menu for new selection AnchorMenu.Instance.Status = "marquee"; this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx index bebd71dcf..ee7dd1fc1 100644 --- a/src/debug/Viewer.tsx +++ b/src/debug/Viewer.tsx @@ -138,8 +138,7 @@ class DebugViewer extends React.Component<{ field: FieldResult, setValue(value: return <p>Unrecognized field type</p>; } - return <EditableView GetValue={() => Field.toScriptString(field)} SetValue={this.props.setValue} - contents={content}></EditableView>; + return <EditableView GetValue={() => Field.toScriptString(field)} SetValue={this.props.setValue} contents={content} />; } } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 4cc0925a9..69d51e545 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -26,6 +26,7 @@ import { AudioField, ImageField, PdfField, VideoField, WebField } from "./URLFie import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from "./util"; import JSZip = require("jszip"); import { FilterBox } from "../client/views/nodes/FilterBox"; +import { prefix } from "@fortawesome/free-regular-svg-icons"; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -196,11 +197,7 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym](clear?: boolean) { - const self = this[SelfProxy]; - runInAction(() => clear && Array.from(Object.keys(self)).forEach(key => delete self[key])); - return this.___fields; - } + public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [WidthSym] = () => NumCast(this[SelfProxy]._width); public [HeightSym] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `DOC-"${this[Self][Id]}"-`; @@ -235,10 +232,11 @@ export class Doc extends RefField { const sameAuthor = this.author === Doc.CurrentUserEmail; if (set) { for (const key in set) { - if (!key.startsWith("fields.")) { + const fprefix = "fields."; + if (!key.startsWith(fprefix)) { continue; } - const fKey = key.substring(7); + const fKey = key.substring(fprefix.length); const fn = async () => { const value = await SerializationHelper.Deserialize(set[key]); const prev = GetEffectiveAcl(this); @@ -251,9 +249,6 @@ export class Doc extends RefField { if (prev === AclPrivate && GetEffectiveAcl(this) !== AclPrivate) { DocServer.GetRefField(this[Id], true); } - // if (prev !== AclPrivate && GetEffectiveAcl(this) === AclPrivate) { - // this[FieldsSym](true); - // } }; if (sameAuthor || fKey.startsWith("acl") || DocServer.getFieldWriteMode(fKey) !== DocServer.WriteMode.Playground) { delete this[CachedUpdates][fKey]; diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index e98498489..d6950d46a 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -142,7 +142,9 @@ export default class UploadManager extends ApiManager { const field = doc.fields[key]; if (field === undefined || field === null) { continue; } - if (field.__type === "proxy" || field.__type === "prefetch_proxy") { + if (field.__type === "Doc") { + mapFn(field); + } else if (field.__type === "proxy" || field.__type === "prefetch_proxy") { field.fieldId = getId(field.fieldId); } else if (field.__type === "script" || field.__type === "computed") { if (field.captures) { @@ -189,22 +191,23 @@ export default class UploadManager extends ApiManager { } }); const json = zip.getEntry("doc.json"); - let docs: any; try { const data = JSON.parse(json.getData().toString("utf8")); - docs = data.docs; - id = data.id; - docs = Object.keys(docs).map(key => docs[key]); + const datadocs = data.docs; + id = getId(data.id); + const docs = Object.keys(datadocs).map(key => datadocs[key]); docs.forEach(mapFn); - await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { - err && console.log(err); - res(); - }, true)))); + await Promise.all(docs.map((doc: any) => new Promise(res => { + Database.Instance.replace(doc.id, doc, (err, r) => { + err && console.log(err); + res(); + }, true); + }))); } catch (e) { console.log(e); } unlink(path_2, () => { }); } SolrManager.update(); - res.send(JSON.stringify(id ? getId(id) : "error")); + res.send(JSON.stringify(id || "error")); } catch (e) { console.log(e); } resolve(); }); diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 6850f0601..7e3c4da82 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -299,7 +299,7 @@ export namespace WebSocket { diff.diff.$set = diff.diff.$remFromSet; delete diff.diff.$remFromSet; const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const remListItems = diff.diff.$set[updatefield].fields; - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields || []; + const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((f: any) => f !== null) || []; diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)); const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; |