diff options
Diffstat (limited to 'src/client/views/DocumentDecorations.tsx')
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 140 |
1 files changed, 95 insertions, 45 deletions
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 657d92b8a..29e088143 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -19,6 +19,7 @@ import { SelectionManager } from "../util/SelectionManager"; import { SnappingManager } from '../util/SnappingManager'; import { undoBatch, UndoManager } from "../util/UndoManager"; import { CollectionDockingView } from './collections/CollectionDockingView'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { KeyManager } from './GlobalKeyHandler'; @@ -27,8 +28,8 @@ import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { DocumentView } from "./nodes/DocumentView"; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { ImageBox } from './nodes/ImageBox'; import React = require("react"); -import { CollectionFreeFormView } from './collections/collectionFreeForm'; @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> { @@ -48,7 +49,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P @observable private _titleControlString: string = "#title"; @observable private _edtingTitle = false; @observable private _hidden = false; - + @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. @observable public Interacting = false; @observable public pushIcon: IconProp = "arrow-alt-circle-up"; @observable public pullIcon: IconProp = "arrow-alt-circle-down"; @@ -63,7 +64,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P @computed get Bounds() { const views = SelectionManager.Views(); - return views.map(dv => dv.getBounds()).reduce((bounds, rect) => + return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) => !rect ? bounds : { x: Math.min(rect.left, bounds.x), @@ -83,7 +84,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P } else if (this._titleControlString.startsWith("#")) { const titleFieldKey = this._titleControlString.substring(1); UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { - titleFieldKey === "title" && (d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-")); + if (titleFieldKey === "title") { + d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-"); + if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) { + Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) { + Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + } //@ts-ignore const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); @@ -104,7 +113,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P } } - titleEntered = (e: React.KeyboardEvent) => e.key === "Enter" && (e.target as any).blur(); + titleEntered = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + (e.target as any).blur(); + } + } @action onTitleDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { @@ -136,19 +150,36 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P return true; } - onCloseClick = () => { - const selected = SelectionManager.Views().slice(); - SelectionManager.DeselectAll(); - selected.map(dv => dv.props.removeDocument?.(dv.props.Document)); + _deleteAfterIconify = false; + _iconifyBatch: UndoManager.Batch | undefined; + onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { + if (this.canDelete) { + const views = SelectionManager.Views().slice().filter(v => v); + if (forceDeleteOrIconify === false && this._iconifyBatch) return; + this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; + if (!this._iconifyBatch) { + this._iconifyBatch = UndoManager.StartBatch("iconifying"); + } else { + forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes + } + var iconifyingCount = views.length; + const finished = action((force?: boolean) => { + if ((force || --iconifyingCount === 0) && this._iconifyBatch) { + if (this._deleteAfterIconify) { + views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); + SelectionManager.DeselectAll(); + } + this._iconifyBatch?.end(); + this._iconifyBatch = undefined; + } + }); + if (forceDeleteOrIconify) finished(forceDeleteOrIconify); + else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); + } } onMaximizeDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, () => { - DragManager.StartWindowDrag?.({ - pageX: e.pageX, - pageY: e.pageY, - preventDefault: emptyFunction, - button: 0 - }, [SelectionManager.Views().slice(-1)[0].rootDoc]); + DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]); return true; }, emptyFunction, this.onMaximizeClick, false, false); } @@ -168,7 +199,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P } else if (e.altKey) { // open same document in new tab CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); } else { - LightboxView.SetLightboxDoc(selectedDocs[0].props.Document, undefined, selectedDocs.slice(1).map(view => view.props.Document)); + var openDoc = selectedDocs[0].props.Document; + if (openDoc.layoutKey === "layout_icon") { + openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); + Doc.deiconifyView(openDoc); + } + LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document)); } } SelectionManager.DeselectAll(); @@ -194,14 +230,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P @action onRotateDown = (e: React.PointerEvent): void => { const rotateUndo = UndoManager.StartBatch("rotatedown"); - const centerPoint = { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; + const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); + const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); - const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + if (selectedInk.length) { + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); + } else { + SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI); + } return false; }, () => { @@ -339,7 +379,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P } let actualdW = Math.max(width + (dW * scale), 20); let actualdH = Math.max(height + (dH * scale), 20); - const fixedAspect = (nwidth && nheight); + const fixedAspect = (nwidth && nheight && !doc._fitWidth); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { @@ -428,20 +468,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; } + @computed get canDelete() { + return SelectionManager.Views().some(docView => { + if (docView.rootDoc.stayInCollection) return false; + const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; + //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && + return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); + }); + } + @computed get hasIcons() { + return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); + } + render() { const bounds = this.Bounds; const seldoc = SelectionManager.Views().slice(-1)[0]; if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { return (null); } - const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles; + const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup; const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); - const canDelete = SelectionManager.Views().some(docView => { - const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; - //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && - return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); - }); + const canDelete = this.canDelete; const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} @@ -451,24 +499,20 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P </Tooltip>); const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); - const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> : + const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} key="title" /> : this._edtingTitle ? <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`} - style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} onBlur={e => this.titleBlur()} onChange={action(e => this._accumulatedTitle = e.target.value)} - onKeyPress={this.titleEntered} /> : - <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} > + onKeyDown={this.titleEntered} /> : + <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} > <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span> </div>; - let inMainMenuPanel = false; - for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) { - if (node.className === "mainView-mainContent") inMainMenuPanel = true; - } - const leftBounds = inMainMenuPanel ? 0 : this.props.boundsLeft; + + const leftBounds = this.props.boundsLeft; const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; @@ -476,32 +520,34 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - const useRotation = seldoc.ComponentView instanceof InkingStroke; + const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; + const rotation = NumCast(seldoc.rootDoc._jitterRotation); + return (<div className={`documentDecorations${colorScheme}`}> <div className="documentDecorations-background" style={{ + transform: `rotate(${rotation}deg)`, + transformOrigin: "top left", width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px", left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: KeyManager.Instance.ShiftPressed || this.Interacting ? "none" : "all", + pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? "none" : "all", display: SelectionManager.Views().length <= 1 ? "none" : undefined }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} /> {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> <div className="documentDecorations-container" key="container" style={{ + transform: ` translate(${bounds.x - this._resizeBorderWidth / 2}px, ${bounds.y - this._resizeBorderWidth / 2 - this._titleHeight}px) rotate(${rotation}deg)`, + transformOrigin: `8px 26px`, width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px", height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px", - left: bounds.x - this._resizeBorderWidth / 2, - top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight, }}> - {!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")} + {!canDelete ? <div /> : topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} {titleArea} {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} {hideResizers ? (null) : <> - {SelectionManager.Views().length !== 1 || hideTitle ? (null) : - topBtn("iconify", `window-${seldoc.finalLayoutKey.includes("icon") ? "restore" : "minimize"}`, undefined, this.onIconifyClick, `${seldoc.finalLayoutKey.includes("icon") ? "De" : ""}Iconify Document`)} <div key="tl" className={`documentDecorations-topLeftResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> <div key="t" className={`documentDecorations-topResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> <div key="tr" className={`documentDecorations-topRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()} /> @@ -519,10 +565,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P onContextMenu={e => e.preventDefault()}>{useRotation && "⟲"}</div> </> } + {seldoc?.Document.type === DocumentType.FONTICON ? (null) : + <div className="link-button-container" key="links" + style={{ + transform: ` translate(${- this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + }}> + <DocumentButtonBar views={SelectionManager.Views} /> + </div>} </div > - {seldoc?.Document.type === DocumentType.FONTICON ? (null) : <div className="link-button-container" key="links" style={{ left: bounds.x - this._resizeBorderWidth / 2 + 10, top: bounds.b + this._resizeBorderWidth / 2 }}> - <DocumentButtonBar views={SelectionManager.Views} /> - </div>} </>} </div > ); |