import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { IconButton } from 'browndash-components'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaUndo } from 'react-icons/fa'; import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; import { Utils, emptyFunction, numberValue } from '../../Utils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { InkStrokeProperties } from './InkStrokeProperties'; import { InkingStroke } from './InkingStroke'; import { ObservableReactComponent } from './ObservableReactComponent'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { Colors } from './global/globalEnums'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentView } from './nodes/DocumentView'; import { ImageBox } from './nodes/ImageBox'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; interface DocumentDecorationsProps { PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number; } @observer export class DocumentDecorations extends ObservableReactComponent { // eslint-disable-next-line no-use-before-define static Instance: DocumentDecorations; private _resizeHdlId = ''; private _keyinput = React.createRef(); private _resizeBorderWidth = 16; private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; private _offset = { x: 0, y: 0 }; // offset from click pt to inner edge of resize border private _snapPt = { x: 0, y: 0 }; // last snapped location of resize border private _inkDragDocs: { doc: Doc; x: number; y: number; width: number; height: number }[] = []; private _interactionLock?: boolean; @observable _showNothing = true; @observable private _accumulatedTitle = ''; @observable private _titleControlString: string = '$title'; @observable private _editingTitle = false; @observable private _hidden = false; @observable private _isRotating: boolean = false; @observable private _isRounding: boolean = false; @observable private _showLayoutAcl: boolean = false; @observable private _showRotCenter = false; // whether to show a draggable green dot that represents the center of rotation @observable private _rotCenter = [0, 0]; // the center of rotation in object coordinates (0,0) = object center (not top left!) constructor(props: React.PropsWithChildren) { super(props); makeObservable(this); DocumentDecorations.Instance = this; document.addEventListener('pointermove', // show decorations whenever pointer moves outside of selection bounds. action(e => { let inputting = false; if (this._titleControlString.startsWith('$')) { const titleFieldKey = this._titleControlString.substring(1); if (DocumentView.Selected()[0]?.Document[titleFieldKey] !== this._accumulatedTitle) { inputting = true; } } const center = {x: (this.Bounds.x+this.Bounds.r)/2, y: (this.Bounds.y+this.Bounds.b)/2}; const {x,y} = Utils.rotPt(e.clientX - center.x, e.clientY - center.y, NumCast(DocumentView.Selected().lastElement()?.screenToViewTransform().Rotate)); (this._showNothing = !inputting && !DocumentButtonBar.Instance?._tooltipOpen && !(this.Bounds.x !== Number.MAX_VALUE && // (this.Bounds.x > center.x+x || this.Bounds.r < center.x+x || this.Bounds.y > center.y+y || this.Bounds.b < center.y+y ))); })); // prettier-ignore } @computed get ClippedBounds() { const bounds = { ...this.Bounds }; const leftBounds = this._props.boundsLeft; const topBounds = DocumentView.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; const borderRadiusDraggerWidth = 15; 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)); return bounds; } @computed get Bounds() { return (SnappingManager.IsLinkFollowing || SnappingManager.ExploreMode) ? { x: 0, y: 0, r: 0, b: 0 } : DocumentView.Selected() .filter(dv => dv._props.renderDepth > 0) .map(dv => dv.getBounds) .reduce((bounds, rect) => !rect ? bounds : { x: Math.min(rect.left, bounds.x), y: Math.min(rect.top, bounds.y), r: Math.max(rect.right, bounds.r), b: Math.max(rect.bottom, bounds.b)}, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE }); // prettier-ignore } @action titleBlur = () => { if (this._accumulatedTitle.startsWith('$')) { this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith('$')) { if (this._accumulatedTitle.startsWith('-->#')) { DocumentView.SelectedDocs().forEach(doc => { doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`); }); } const titleFieldKey = this._titleControlString.substring(1); UndoManager.RunInBatch( () => titleFieldKey && DocumentView.Selected().forEach(dv => { if (titleFieldKey === 'title') { dv.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-'); } Doc.SetField(dv.Document, titleFieldKey, this._accumulatedTitle); }), 'edit title' ); } }; titleEntered = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.stopPropagation(); (e.target as any).blur(); } }; onContainerDown = (e: React.PointerEvent) => { const effectiveLayoutAcl = GetEffectiveAcl(DocumentView.Selected()[0].Document); if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) { setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(true, moveEv), emptyFunction, emptyFunction); e.stopPropagation(); } }; onTitleDown = (e: React.PointerEvent) => { const effectiveLayoutAcl = GetEffectiveAcl(DocumentView.SelectedDocs()[0]); if (effectiveLayoutAcl === AclAdmin || effectiveLayoutAcl === AclEdit || effectiveLayoutAcl === AclAugment) { setupMoveUpEvents( this, e, moveEv => this.onBackgroundMove(true, moveEv), emptyFunction, action(() => { const selected = DocumentView.SelectedDocs().length === 1 ? DocumentView.SelectedDocs()[0] : undefined; !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('$') ? (selected && Field.toKeyValueString(selected, this._titleControlString.substring(1))) || '-unset-' : this._titleControlString); this._editingTitle = true; this._keyinput.current && setTimeout(this._keyinput.current.focus); }) ); e.stopPropagation(); } }; onBackgroundDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, moveEv => this.onBackgroundMove(false, moveEv), emptyFunction, emptyFunction); e.stopPropagation(); }; @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = DocumentView.Selected()[0]; const effectiveLayoutAcl = GetEffectiveAcl(dragDocView.Document); if (effectiveLayoutAcl !== AclAdmin && effectiveLayoutAcl !== AclEdit && effectiveLayoutAcl !== AclAugment) { return false; } const containers = new Set(); DocumentView.Selected().forEach(v => containers.add(DocCast(v.Document.embedContainer))); if (containers.size > 1) return false; const { left, top } = dragDocView.getBounds || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData(DocumentView.SelectedDocs(), dragDocView._props.dropAction); dragData.offset = dragDocView.screenToContentsTransform().transformDirection(e.x - left, e.y - top); dragData.moveDocument = dragDocView._props.moveDocument; dragData.removeDocument = dragDocView._props.removeDocument; dragData.isDocDecorationMove = true; dragData.canEmbed = dragTitle; this._hidden = true; DragManager.StartDocumentDrag( DocumentView.Selected().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { dragComplete: action(() => { this._hidden = false; }), hideSource: true, } ); return true; }; _deleteAfterIconify = false; _iconifyBatch: UndoManager.Batch | undefined; onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { const views = DocumentView.Selected().filter(v => v && v._props.renderDepth > 0); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = !!(forceDeleteOrIconify || this._iconifyBatch); let iconifyingCount = views.length; const finished = action((force?: boolean) => { if ((force || --iconifyingCount === 0) && this._iconifyBatch) { if (this._deleteAfterIconify) { views.forEach(iconView => { const iconViewDoc = iconView.Document; Doc.setNativeView(iconViewDoc); if (iconViewDoc.activeFrame) { iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. } else { iconView._props.removeDocument?.(iconView.Document); } }); views.forEach(DocumentView.DeselectView); } this._iconifyBatch?.end(); this._iconifyBatch = undefined; } }); if (!this._iconifyBatch) { (document.activeElement as any).blur?.(); this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying'); } else { // eslint-disable-next-line no-param-reassign forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes } if (forceDeleteOrIconify) finished(forceDeleteOrIconify); else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); }; onMaximizeDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, () => DragManager.StartWindowDrag?.(e, [DocumentView.SelectedDocs().lastElement()]) ?? false, emptyFunction, this.onMaximizeClick, false, false); e.stopPropagation(); }; onMaximizeClick = (e: any): void => { const selView = DocumentView.Selected()[0]; if (selView) { if (e.ctrlKey) { // open an embedding in a new tab with Ctrl Key CollectionDockingView.AddSplit(Doc.BestEmbedding(selView.Document), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key const embedding = Doc.MakeEmbedding(selView.Document); embedding.embedContainer = undefined; embedding.x = -NumCast(embedding._width) / 2; embedding.y = -NumCast(embedding._height) / 2; CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab CollectionDockingView.ToggleSplit(selView.Document, OpenWhereMod.right); } else { let openDoc = selView.Document; if (openDoc.layout_fieldKey === 'layout_icon') { openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); Doc.deiconifyView(openDoc); } selView._props.addDocTab(openDoc, OpenWhere.lightboxAlways); } } DocumentView.DeselectAll(); }; onIconifyClick = (): void => { DocumentView.Selected().forEach(dv => dv?.iconify()); DocumentView.DeselectAll(); }; onSelectContainerDocClick = () => DocumentView.Selected()?.[0]?.containerViewPath?.().lastElement()?.select(false); /** * sets up events when user clicks on the border radius editor */ @action onRadiusDown = (e: React.PointerEvent): void => { SnappingManager.SetIsResizing(DocumentView.SelectedDocs().lastElement()?.[Id]); this._isRounding = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); setupMoveUpEvents( this, e, moveEv => { const [x, y] = [this.Bounds.x + 3, this.Bounds.y + 3]; const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const dist = moveEv.clientX < x && moveEv.clientY < y ? 0 : Math.sqrt((moveEv.clientX - x) * (moveEv.clientX - x) + (moveEv.clientY - y) * (moveEv.clientY - y)); DocumentView.SelectedDocs().forEach(doc => { const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box doc._layout_borderRounding = `${radius}px`; }); return false; }, action(() => { SnappingManager.SetIsResizing(undefined); this._isRounding = false; this._resizeUndo?.end(); }), // upEvent emptyFunction, true ); e.stopPropagation(); }; @action onLockDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, e, returnFalse, // don't care about move or up event, emptyFunction, // just care about whether we get a click event () => UndoManager.RunInBatch(() => DocumentView.Selected().forEach(dv => Doc.toggleLockedPosition(dv.Document)), 'toggleBackground') ); e.stopPropagation(); }; setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => { const selDoc = seldocview.Document; const newloccentern = seldocview.screenToContentsTransform().transformPoint(rotCenter[0], rotCenter[1]); const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.Document._rotation) / 180) * Math.PI); selDoc._rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width); selDoc._rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height); }; @action onRotateCenterDown = (e: React.PointerEvent): void => { this._isRotating = true; const seldocview = DocumentView.Selected()[0]; setupMoveUpEvents( this, e, (moveEv: PointerEvent, down: number[], delta: number[]) => // return false to keep getting events this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]) as any as boolean, action(() => { this._isRotating = false; }), // upEvent action(() => { seldocview.Document._rotation_centerX = seldocview.Document._rotation_centerY = 0; }), true ); // prettier-ignore e.stopPropagation(); }; @action onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; const rotateUndo = UndoManager.StartBatch('drag rotation'); const selectedInk = DocumentView.Selected().filter(i => i.ComponentView instanceof InkingStroke); const centerPoint = this.rotCenter.slice(); const infos = new Map(); const seldocview = DocumentView.Selected()[0]; DocumentView.Selected().forEach(dv => { const accumRot = (NumCast(dv.Document._rotation) / 180) * Math.PI; const localRotCtr = dv.screenToViewTransform().transformPoint(rcScreen.X, rcScreen.Y); const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document.width) / 2, localRotCtr[1] - NumCast(dv.Document.height) / 2]; const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); const unrotatedDocPos = { x: NumCast(dv.Document.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.Document.y) + localRotCtrOffset[1] - startRotCtr.y }; infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot }); }); const infoRot = (angle: number, isAbs = false) => { DocumentView.Selected().forEach( action(dv => { const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.Document)!; const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle); infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle }); dv.Document.x = infos.get(dv.Document)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x); dv.Document.y = infos.get(dv.Document)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y); dv.Document._rotation = ((isAbs ? 0 : NumCast(dv.Document._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 }) ); }; setupMoveUpEvents( this, e, (moveEv: PointerEvent, down: number[], delta: number[]) => { const previousPoint = { X: moveEv.clientX, Y: moveEv.clientY }; const movedPoint = { X: moveEv.clientX - delta[0], Y: moveEv.clientY - delta[1] }; const deltaAng = InkStrokeProperties.angleChange(movedPoint, previousPoint, rcScreen); if (selectedInk.length) { deltaAng && InkStrokeProperties.Instance.rotateInk(selectedInk, deltaAng, rcScreen); this.setRotateCenter(seldocview, centerPoint); } else { infoRot(deltaAng); } return false; }, // moveEvent action(() => { const oldRotation = NumCast(seldocview.Document._rotation); const diff = oldRotation - Math.round(oldRotation / 45) * 45; if (Math.abs(diff) < 5) { if (selectedInk.length) { InkStrokeProperties.Instance.rotateInk(selectedInk, ((Math.round(oldRotation / 45) * 45 - oldRotation) / 180) * Math.PI, rcScreen); } else { infoRot(((Math.round(oldRotation / 45) * 45) / 180) * Math.PI, true); } } if (selectedInk.length) { this.setRotateCenter(seldocview, centerPoint); } this._isRotating = false; rotateUndo?.end(); }), // upEvent action(() => { this._showRotCenter = !this._showRotCenter; }) // clickEvent ); }; @action onPointerDown = (e: React.PointerEvent): void => { SnappingManager.SetIsResizing(DocumentView.Selected().lastElement()?.Document[Id]); // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); e.stopPropagation(); const id = (this._resizeHdlId = e.currentTarget.className); const pad = id.includes('Left') || id.includes('Right') ? Number(getComputedStyle(e.target as any).width.replace('px', '')) / 2 : 0; const bounds = e.currentTarget.getBoundingClientRect(); this._offset = { x: id.toLowerCase().includes('left') ? bounds.right - e.clientX - pad : bounds.left - e.clientX + pad, // y: id.toLowerCase().includes('top') ? bounds.bottom - e.clientY - pad : bounds.top - e.clientY + pad, }; this._resizeUndo = UndoManager.StartBatch('drag resizing'); this._snapPt = { x: e.pageX, y: e.pageY }; DocumentView.Selected().forEach(docView => CollectionFreeFormView.from(docView)?.dragStarting(false, false)); }; projectDragToAspect = (e: PointerEvent, docView: DocumentView, fixedAspect: number) => { // need to generalize for bl and tr drag handles const project = (p: number[], a: number[], b: number[]) => { const atob = [b[0] - a[0], b[1] - a[1]]; const atop = [p[0] - a[0], p[1] - a[1]]; const len = atob[0] * atob[0] + atob[1] * atob[1]; let dot = atop[0] * atob[0] + atop[1] * atob[1]; const t = dot / len; dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); return [a[0] + atob[0] * t, a[1] + atob[1] * t]; }; const tl = docView.screenToContentsTransform().inverse().transformPoint(0, 0); return project([e.clientX + this._offset.x, e.clientY + this._offset.y], tl, [tl[0] + fixedAspect, tl[1] + 1]); }; onPointerMove = (e: PointerEvent): boolean => { const first = DocumentView.Selected()[0]; const effectiveAcl = GetEffectiveAcl(first.Document); if (!(effectiveAcl === AclAdmin || effectiveAcl === AclEdit || effectiveAcl === AclAugment)) return false; if (!first) return false; const fixedAspect = Doc.NativeAspect(first.layoutDoc); const dragHdl = this._resizeHdlId.split(' ')[0].replace('documentDecorations-', '').replace('Resizer', ''); const thisPt = // do snapping of drag point fixedAspect && (dragHdl === 'bottomRight' || dragHdl === 'topLeft') ? DragManager.snapDragAspect(this.projectDragToAspect(e, first, fixedAspect), fixedAspect) : DragManager.snapDrag(e, -this._offset.x, -this._offset.y, this._offset.x, this._offset.y); const { scale, refPt } = this.getResizeVals(thisPt, dragHdl); !this._interactionLock && runInAction(async () => { // resize selected docs if we're not in the middle of a resize (ie, throttle input events to frame rate) this._interactionLock = true; this._snapPt = thisPt; e.ctrlKey && (DocumentView.Selected().forEach(docView => !Doc.NativeHeight(docView.Document) && docView.toggleNativeDimensions())); const hasFixedAspect = DocumentView.Selected().map(dv => dv.Document).some(this.hasFixedAspect); const scaleAspect = {x:scale.x === 1 && hasFixedAspect ? scale.y : scale.x, y: scale.x !== 1 && hasFixedAspect ? scale.x : scale.y}; DocumentView.Selected().forEach(docView => this.resizeView(docView, refPt, scaleAspect, { dragHdl, ctrlKey:e.ctrlKey })); // prettier-ignore await new Promise(res => { setTimeout(() => { res(this._interactionLock = undefined)})}); }); // prettier-ignore return false; }; // // determines how much to resize, and determines the resize reference point // getResizeVals = (thisPt: { x: number; y: number }, dragHdl: string) => { const [w, h] = [this.Bounds.r - this.Bounds.x, this.Bounds.b - this.Bounds.y]; const [moveX, moveY] = [thisPt.x - this._snapPt.x, thisPt.y - this._snapPt.y]; switch (dragHdl) { case 'topLeft': return { scale: { x: 1 - moveX / w, y: 1 -moveY / h }, refPt: [this.Bounds.r, this.Bounds.b] }; case 'topRight': return { scale: { x: 1 + moveX / w, y: 1 -moveY / h }, refPt: [this.Bounds.x, this.Bounds.b] }; case 'top': return { scale: { x: 1, y: 1 -moveY / h }, refPt: [this.Bounds.x, this.Bounds.b] }; case 'left': return { scale: { x: 1 - moveX / w, y: 1 }, refPt: [this.Bounds.r, this.Bounds.y] }; case 'bottomLeft': return { scale: { x: 1 - moveX / w, y: 1 + moveY / h }, refPt: [this.Bounds.r, this.Bounds.y] }; case 'right': return { scale: { x: 1 + moveX / w, y: 1 }, refPt: [this.Bounds.x, this.Bounds.y] }; case 'bottomRight':return { scale: { x: 1 + moveX / w, y: 1 + moveY / h }, refPt: [this.Bounds.x, this.Bounds.y] }; case 'bottom': return { scale: { x: 1, y: 1 + moveY / h }, refPt: [this.Bounds.x, this.Bounds.y] }; default: return { scale: { x: 1, y: 1 }, refPt: [this.Bounds.x, this.Bounds.y] }; } // prettier-ignore }; // // determines if anything being dragged directly or via a group has a fixed aspect ratio (in which case we resize uniformly) // hasFixedAspect = (doc: Doc): boolean => (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); // // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc // resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; ctrlKey: boolean }) => { const doc = docView.Document; if (doc.isGroup) { DocListCast(doc.data) .map(member => DocumentView.getDocumentView(member, docView)!) .forEach(member => this.resizeView(member, refPt, scale, opts)); doc.xPadding = NumCast(doc.xPadding) * scale.x; doc.yPadding = NumCast(doc.yPadding) * scale.y; } else { const refCent = docView.screenToViewTransform().transformPoint(refPt[0], refPt[1]); // fixed reference point for resize (ie, a point that doesn't move) const [nwidth, nheight] = [docView.nativeWidth, docView.nativeHeight]; const [initWidth, initHeight] = [NumCast(doc._width, 1), NumCast(doc._height)]; const modifyNativeDim = (opts.ctrlKey && doc._layout_nativeDimEditable) || // e.g., PDF or web page (doc._layout_reflowHorizontal && opts.dragHdl !== 'bottom' && opts.dragHdl !== 'top') || // eg rtf or some web pages (doc._layout_reflowVertical && (opts.dragHdl === 'bottom' || opts.dragHdl === 'top' || opts.ctrlKey)); // eg rtf, web, pdf if (nwidth && nheight && !modifyNativeDim) { // eg., dragging right resizer on PDF -- enforce native dimensions because not expliclty overridden with ctrl or bottom resize drag scale.x === 1 ? (scale.x = scale.y) : (scale.y = scale.x); } if (['right', 'left'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeWidth(doc)) { const setData = Doc.NativeWidth(doc[DocData]) === doc.nativeWidth; doc.nativeWidth = scale.x * Doc.NativeWidth(doc); if (setData) Doc.SetNativeWidth(doc[DocData], NumCast(doc.nativeWidth)); if (doc._layout_reflowVertical && !NumCast(doc.nativeHeight)) { doc._nativeHeight = (initHeight / initWidth) * nwidth; // initializes the nativeHeight for a PDF } } if (['bottom', 'top'].includes(opts.dragHdl) && modifyNativeDim && Doc.NativeHeight(doc)) { const setData = Doc.NativeHeight(doc[DocData]) === doc.nativeHeight; doc._nativeHeight = scale.y * Doc.NativeHeight(doc); if (setData) Doc.SetNativeHeight(doc[DocData], NumCast(doc._nativeHeight)); } doc._width = Math.max(1, NumCast(doc._width) * scale.x); doc._height = Math.max(1, NumCast(doc._height) * scale.y); const { deltaX, deltaY } = this.realignRefPt(doc, refCent, initWidth, initHeight); doc.x = NumCast(doc.x) + deltaX; doc.y = NumCast(doc.y) + deltaY; doc._layout_modificationDate = new DateField(); if (scale.y !== 1) { const docLayout = docView.layoutDoc; docLayout._layout_autoHeight = undefined; if (docView.layoutDoc._layout_autoHeight) { // if autoHeight is still on because of a prototype docLayout._layout_autoHeight = false; // then don't inherit, but explicitly set it to false } } } }; // This realigns the doc's resize reference point with where it was before resizing it. // This is needed, because the transformation for doc's with a rotation is screwy: // the top left of the doc is the 'origin', but the rotation happens about the center of the Doc. // So resizing a rotated doc will cause it to shift -- this counteracts that shift by determine how // the reference points shifted, and returning a translation to restore the reference point. realignRefPt = (doc: Doc, refCent: number[], initWidth: number, initHeight: number) => { const refCentPct = [refCent[0] / initWidth, refCent[1] / initHeight]; const rotRefStart = Utils.rotPt( refCent[0] - initWidth / 2, // rotate reference pointe before scaling refCent[1] - initHeight / 2, (NumCast(doc._rotation) / 180) * Math.PI ); const rotRefEnd = Utils.rotPt( refCentPct[0] * NumCast(doc._width) - NumCast(doc._width) / 2, // rotate reference point after scaling refCentPct[1] * NumCast(doc._height) - NumCast(doc._height) / 2, (NumCast(doc._rotation) / 180) * Math.PI ); return { deltaX: rotRefStart.x + initWidth / 2 - (rotRefEnd.x + NumCast(doc._width) / 2), // deltaY: rotRefStart.y + initHeight / 2 - (rotRefEnd.y + NumCast(doc._height) / 2), }; }; @action onPointerUp = (): void => { SnappingManager.SetIsResizing(undefined); SnappingManager.clearSnapLines(); this._resizeHdlId = ''; this._resizeUndo?.end(); // detect layout_autoHeight gesture and apply DocumentView.Selected().forEach(view => { NumCast(view.Document._height) < 20 && (view.layoutDoc._layout_autoHeight = true); }); // need to change points for resize, or else rotation/control points will fail. this._inkDragDocs .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { doc[DocData].data = new InkField(inkPts.map( (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth) X: NumCast(doc.x) - x + (NumCast(doc.width) * ipt.X) / width, Y: NumCast(doc.y) - y + (NumCast(doc.height) * ipt.Y) / height, }))); // prettier-ignore Doc.SetNativeWidth(doc, undefined); Doc.SetNativeHeight(doc, undefined); }); }; @computed get selectionTitle(): string { if (DocumentView.Selected().length === 1) { const selected = DocumentView.Selected()[0]; if (this._titleControlString.startsWith('$')) { return Field.toJavascriptString(selected.Document[this._titleControlString.substring(1)] as FieldType) || '-unset-'; } return this._accumulatedTitle; } return DocumentView.Selected().length > 1 ? '-multiple-' : '-unset-'; } @computed get rotCenter() { const lastView = DocumentView.Selected().lastElement(); if (lastView) { const invXf = lastView.screenToContentsTransform().inverse(); const seldoc = lastView.layoutDoc; const loccenter = Utils.rotPt(NumCast(seldoc._rotation_centerX) * NumCast(seldoc._width), NumCast(seldoc._rotation_centerY) * NumCast(seldoc._height), invXf.Rotate); return invXf.transformPoint(loccenter.x + NumCast(seldoc._width) / 2, loccenter.y + NumCast(seldoc._height) / 2); } return this._rotCenter; } render() { const { b, r, x, y } = this.Bounds; const seldocview = DocumentView.Selected().lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { setTimeout( action(() => { this._editingTitle = false; this._showNothing = true; }) ); return null; } // sharing const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document); const docShareMode = HierarchyMapping.get(acl)!.name; const shareMode = StrCast(docShareMode); const shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = SnappingManager.IsResizing || seldocview._props.hideDecorations || seldocview.Document.layout_hideDecorations; const hideResizers = ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.Document)) || hideDecorations || seldocview._props.hideResizeHandles || seldocview.Document.layout_hideResizeHandles || this._isRounding || this._isRotating; const hideTitle = this._showNothing || hideDecorations || seldocview._props.hideDecorationTitle || seldocview.Document.layout_hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview._props.hideDocumentButtonBar || seldocview.Document.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = this._showNothing || hideDecorations || seldocview._props.hideOpenButton || seldocview.Document.layout_hideOpenButton || DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = this._showNothing || hideDecorations || this._isRounding || this._isRotating || seldocview._props.hideDeleteButton || seldocview.Document.hideDeleteButton || DocumentView.Selected().some(docView => { const collectionAcl = docView.containerViewPath?.()?.lastElement() ? GetEffectiveAcl(docView.containerViewPath?.().lastElement().dataDoc) : AclEdit; return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.Document) !== AclAdmin; }); const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( {title}} placement="top">
e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, clickEv => click!(clickEv)))}>
); const bounds = this.ClippedBounds; const useLock = bounds.r - bounds.x > 135; const useRotation = !hideResizers && seldocview.Document.type !== DocumentType.EQUATION && CollectionFreeFormDocumentView.from(seldocview); // when do we want an object to not rotate? const rotation = DocumentView.Selected().length === 1 ? seldocview.screenToContentsTransform().inverse().RotateDeg : 0; // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; const borderRadius = numberValue(Cast(seldocview.Document.layout_borderRounding, 'string', null)); const docMax = Math.min(NumCast(seldocview.Document.width) / 2, NumCast(seldocview.Document.height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); const radiusHandle = (borderRadius / docMax) * maxDist; const radiusHandleLocation = Math.min(radiusHandle, maxDist); const sharingMenu = Doc.IsSharingEnabled && docShareMode ? (
  {shareSymbolIcon + ' ' + shareMode}   {/* {!Doc.noviceMode ? (
(this.showLayoutAcl = !this.showLayoutAcl))} />
Layout
) : null}   */}
) : null; const titleArea = this._editingTitle ? ( <> {r - x < 150 ? null : {this._titleControlString + ':'}} { this._editingTitle = false; this.titleBlur(); })} onChange={action(e => { !hideTitle && (this._accumulatedTitle = e.target.value); })} onKeyDown={hideTitle ? emptyFunction : this.titleEntered} onPointerDown={e => e.stopPropagation()} /> ) : (
{hideTitle ? null : ( {this.selectionTitle} )} {sharingMenu} {!useLock ? null : ( toggle ability to interact with document
} placement="top">
)} ); const centery = hideTitle ? 0 : this._titleHeight; const transformOrigin = `${50}% calc(50% + ${centery / 2}px)`; const freeformDoc = DocumentView.Selected().some(v => CollectionFreeFormDocumentView.from(v)); return (
{bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? null : (
{hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')} {titleArea} {hideOpenButton ?
: topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
{hideResizers ? null : ( <>
)} {seldocview._props.renderDepth <= 1 || !seldocview.containerViewPath?.().lastElement() ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectContainerDocClick, 'tap to select containing document')} {useRounding && (
)} {hideDocumentButtonBar || this._showNothing ? null : (
DocumentView.Selected()} />
)}
{useRotation && ( <>
{this._isRotating ? null : ( tap to set rotate center, drag to rotate
}>
e.preventDefault()}> } color={SettingsManager.userColor} />
)}
{!this._showRotCenter ? null : (
e.preventDefault()} /> )} )}
)}
); } }