import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { IconButton } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl } from '../../fields/util'; import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; import './DocumentDecorations.scss'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { InkStrokeProperties } from './InkStrokeProperties'; import { LightboxView } from './LightboxView'; import { DocumentView, OpenWhereMod } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); import _ = require('lodash'); @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> { 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 _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border private _snapX = 0; _snapY = 0; // last snapped location of resize border private _dragHeights = new Map(); private _inkDragDocs: { doc: Doc; x: number; y: number; width: number; height: number }[] = []; @observable private _accumulatedTitle = ''; @observable private _titleControlString: string = '#title'; @observable private _editingTitle = 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 pushIcon: IconProp = 'arrow-alt-circle-up'; @observable public pullIcon: IconProp = 'arrow-alt-circle-down'; @observable public pullColor: string = 'white'; @observable private _isRotating: boolean = false; @observable private _isRounding: boolean = false; @observable private _isResizing: boolean = false; @observable private showLayoutAcl: boolean = false; constructor(props: any) { super(props); DocumentDecorations.Instance = this; reaction( () => SelectionManager.Views().slice(), action(docs => { this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing this._editingTitle = false; }) ); document.addEventListener( // show decorations whenever pointer moves outside of selection bounds. 'pointermove', action(e => { if (this.Bounds.x || this.Bounds.y || this.Bounds.r || this.Bounds.b) { if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { this._showNothing = false; } } }) ); } @computed get Bounds() { if (LinkFollower.IsFollowing || DocumentView.ExploreMode) return { x: 0, y: 0, r: 0, b: 0 }; const views = SelectionManager.Views(); return views .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), c: views.length === 1 ? rect.center : undefined, }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as { X: number; Y: number } | undefined } ); } @action titleBlur = () => { this._editingTitle = false; if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) { this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith('#')) { const titleFieldKey = this._titleControlString.substring(1); UndoManager.RunInBatch( () => titleFieldKey && SelectionManager.Views().forEach(d => { if (titleFieldKey === 'title') { d.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-'); if (StrCast(d.rootDoc.title).startsWith('@') && !this._accumulatedTitle.startsWith('@')) { Doc.RemoveDocFromList(Doc.MyPublishedDocs, undefined, d.rootDoc); } if (!StrCast(d.rootDoc.title).startsWith('@') && this._accumulatedTitle.startsWith('@')) { Doc.AddDocToList(Doc.MyPublishedDocs, undefined, d.rootDoc); } } //@ts-ignore const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle; if (titleField.toString().startsWith('')) { const title = titleField.toString().replace(/\.?/, ''); const curKey = Doc.LayoutFieldKey(d.rootDoc); if (curKey !== title) { if (title) { if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') { d.rootDoc.layout_fieldKey = `layout_${title}`; d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title); d.rootDoc[`${title}_nativeWidth`] = d.rootDoc[`${title}_nativeHeight`] = 0; } } else { d.rootDoc.layout_fieldKey = undefined; } } } else { Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); } }), 'edit title' ); } }; titleEntered = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { e.stopPropagation(); (e.target as any).blur(); } }; @action onContainerDown = (e: React.PointerEvent): void => { const first = SelectionManager.Views()[0]; const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { setupMoveUpEvents( this, e, e => this.onBackgroundMove(true, e), e => {}, emptyFunction ); } }; @action onTitleDown = (e: React.PointerEvent): void => { const first = SelectionManager.Views()[0]; const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) { setupMoveUpEvents( this, e, e => this.onBackgroundMove(true, e), e => {}, action(e => { !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString); this._editingTitle = true; this._keyinput.current && setTimeout(this._keyinput.current.focus); }) ); } }; onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const first = SelectionManager.Views()[0]; const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc); if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) { return false; } const dragDocView = SelectionManager.Views()[0]; const containers = new Set(); SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer))); if (containers.size > 1) return false; const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData( SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction ); dragData.offset = dragDocView.props.ScreenToLocalTransform().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( SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { dragComplete: action(e => { dragData.canEmbed && SelectionManager.DeselectAll(); this._hidden = false; }), hideSource: true, } ); return true; }; _deleteAfterIconify = false; _iconifyBatch: UndoManager.Batch | undefined; onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { const views = SelectionManager.Views() .slice() .filter(v => v && v.props.renderDepth > 0); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; var iconifyingCount = views.length; const finished = action((force?: boolean) => { if ((force || --iconifyingCount === 0) && this._iconifyBatch) { if (this._deleteAfterIconify) { views.forEach(iconView => { Doc.setNativeView(iconView.props.Document); if (iconView.props.Document.activeFrame) { iconView.props.Document.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.props.Document); } }); SelectionManager.DeselectAll(); } this._iconifyBatch?.end(); this._iconifyBatch = undefined; } }); if (!this._iconifyBatch) { (document.activeElement as any).blur?.(); this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying'); } else { 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, [SelectionManager.Views().slice(-1)[0].rootDoc]); return true; }, emptyFunction, this.onMaximizeClick, false, false ); }; onMaximizeClick = (e: any): void => { const selectedDocs = SelectionManager.Views(); if (selectedDocs.length) { if (e.ctrlKey) { // open an embedding in a new tab with Ctrl Key CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc); embedding.embedContainer = undefined; embedding.x = -embedding[Width]() / 2; embedding.y = -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(selectedDocs[0].rootDoc, OpenWhereMod.right); } else { var openDoc = selectedDocs[0].rootDoc; if (openDoc.layout_fieldKey === 'layout_icon') { openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); Doc.deiconifyView(openDoc); } LightboxView.SetLightboxDoc( openDoc, undefined, selectedDocs.slice(1).map(view => view.rootDoc) ); } } SelectionManager.DeselectAll(); }; onIconifyClick = (): void => { SelectionManager.Views().forEach(dv => dv?.iconify()); SelectionManager.DeselectAll(); }; onSelectorClick = () => SelectionManager.Views()?.[0]?.props.docViewPath?.().lastElement()?.select(false); /** * Handles setting up events when user clicks on the border radius editor * @param e PointerEvent */ @action onRadiusDown = (e: React.PointerEvent): void => { this._isRounding = DocumentView.Interacting = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); // Call util move event function setupMoveUpEvents( this, // target e, // pointerEvent (e, down) => { const x = this.Bounds.x + 3; const y = this.Bounds.y + 3; const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); let dist = Math.sqrt((e.clientX - x) * (e.clientX - x) + (e.clientY - y) * (e.clientY - y)); if (e.clientX < x && e.clientY < y) dist = 0; SelectionManager.Views() .map(dv => dv.props.Document) .map(doc => { const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); const ratio = dist / maxDist; const radius = Math.min(1, ratio) * docMax; doc.layout_borderRounding = `${radius}px`; }); return false; }, // moveEvent action(e => { DocumentView.Interacting = this._isRounding = false; this._resizeUndo?.end(); }), // upEvent e => {}, // clickEvent, true ); }; @action onLockDown = (e: React.PointerEvent): void => { // Call util move event function setupMoveUpEvents( this, // target e, // pointerEvent returnFalse, // moveEvent emptyFunction, // upEvent e => { UndoManager.RunInBatch( () => SelectionManager.Views().map(dv => { dv.rootDoc._lockedPosition = !dv.rootDoc._lockedPosition; dv.rootDoc._pointerEvents = dv.rootDoc._lockedPosition ? 'none' : undefined; }), 'toggleBackground' ); } // clickEvent ); }; setRotateCenter = (seldocview: DocumentView, rotCenter: number[]) => { const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]); const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI); seldocview.rootDoc.rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width); seldocview.rootDoc.rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height); }; @action onRotateCenterDown = (e: React.PointerEvent): void => { this._isRotating = true; const seldocview = SelectionManager.Views()[0]; setupMoveUpEvents( this, e, action((e: PointerEvent, down: number[], delta: number[]) => { this.setRotateCenter(seldocview, [this.rotCenter[0] + delta[0], this.rotCenter[1] + delta[1]]); return false; }), // moveEvent action(action(() => (this._isRotating = false))), // upEvent action((e, doubleTap) => { if (doubleTap) { seldocview.rootDoc.rotation_centerX = 0.5; seldocview.rootDoc.rotation_centerY = 0.5; } }) ); }; @action onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; const rotateUndo = UndoManager.StartBatch('drag rotation'); const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); const centerPoint = this.rotCenter.slice(); const infos = new Map(); const seldocview = SelectionManager.Views()[0]; SelectionManager.Views().forEach(dv => { const accumRot = (NumCast(dv.rootDoc._rotation) / 180) * Math.PI; const localRotCtr = dv.props.ScreenToLocalTransform().transformPoint(rcScreen.X, rcScreen.Y); const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.rootDoc.width) / 2, localRotCtr[1] - NumCast(dv.rootDoc.height) / 2]; const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); const unrotatedDocPos = { x: NumCast(dv.rootDoc.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.rootDoc.y) + localRotCtrOffset[1] - startRotCtr.y }; infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot }); }); const infoRot = (angle: number, isAbs = false) => { SelectionManager.Views().forEach( action(dv => { const { unrotatedDocPos, startRotCtr, accumRot } = infos.get(dv.rootDoc)!; const endRotCtr = Utils.rotPt(startRotCtr.x, startRotCtr.y, isAbs ? angle : accumRot + angle); infos.set(dv.rootDoc, { unrotatedDocPos, startRotCtr, accumRot: isAbs ? angle : accumRot + angle }); dv.rootDoc.x = infos.get(dv.rootDoc)!.unrotatedDocPos.x - (endRotCtr.x - startRotCtr.x); dv.rootDoc.y = infos.get(dv.rootDoc)!.unrotatedDocPos.y - (endRotCtr.y - startRotCtr.y); dv.rootDoc._rotation = ((isAbs ? 0 : NumCast(dv.rootDoc._rotation)) + (angle * 180) / Math.PI) % 360; // Rotation between -360 and 360 }) ); }; setupMoveUpEvents( this, e, (e: PointerEvent, down: number[], delta: number[]) => { const previousPoint = { X: e.clientX, Y: e.clientY }; const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; const 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.rootDoc._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(e => (this._showRotCenter = !this._showRotCenter)) // clickEvent ); }; @action onPointerDown = (e: React.PointerEvent): void => { setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); DocumentView.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them this._resizeHdlId = e.currentTarget.className; const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX; this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY; this._resizeUndo = UndoManager.StartBatch('drag resizing'); this._snapX = e.pageX; this._snapY = e.pageY; const ffviewSet = new Set(); SelectionManager.Views().forEach(docView => { const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && ffviewSet.add(ffview); this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); }); Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); }; onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { const first = SelectionManager.Views()[0]; const effectiveAcl = GetEffectiveAcl(first.rootDoc); if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false; if (!first) return false; let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; var fixedAspect = Doc.NativeAspect(first.layoutDoc); const resizeHdl = this._resizeHdlId.split(' ')[0]; if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) { // 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 = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); thisPt = DragManager.snapDragAspect(drag, fixedAspect); } else { thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); } move[0] = thisPt.x - this._snapX; move[1] = thisPt.y - this._snapY; this._snapX = thisPt.x; this._snapY = thisPt.y; let dragBottom = false, dragRight = false, dragBotRight = false, dragTop = false; let dXin = 0, dYin = 0, dWin = 0, dHin = 0; switch (this._resizeHdlId.split(' ')[0]) { case '': break; case 'documentDecorations-topLeftResizer': dXin = -1; dYin = -1; dWin = -move[0]; dHin = -move[1]; break; case 'documentDecorations-topRightResizer': dWin = move[0]; dYin = -1; dHin = -move[1]; break; case 'documentDecorations-topResizer': dYin = -1; dHin = -move[1]; dragTop = true; break; case 'documentDecorations-bottomLeftResizer': dXin = -1; dWin = -move[0]; dHin = move[1]; break; case 'documentDecorations-bottomRightResizer': dWin = move[0]; dHin = move[1]; dragBotRight = true; break; case 'documentDecorations-bottomResizer': dHin = move[1]; dragBottom = true; break; case 'documentDecorations-leftResizer': dXin = -1; dWin = -move[0]; break; case 'documentDecorations-rightResizer': dWin = move[0]; dragRight = true; break; } const isGroup = first.rootDoc._isGroup ? first.rootDoc : undefined; const scaleViews = isGroup ? DocListCast(isGroup.data).map(doc => DocumentManager.Instance.getFirstDocumentView(doc)!) : SelectionManager.Views(); const aggBounds = aggregateBounds(scaleViews.map(view => view.rootDoc) as any, 0, 0); const refWidth = aggBounds.r - aggBounds.x; const refHeight = aggBounds.b - aggBounds.y; const scaleRefPt = first.props .ScreenToLocalTransform() .inverse() .transformPoint( NumCast(isGroup?._xPadding) + (dXin ? refWidth : 0), // NumCast(isGroup?._yPadding) + (dYin ? refHeight : 0) ); scaleViews.forEach( action((docView: DocumentView) => { if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); if (dXin !== 0 || dYin !== 0 || dWin !== 0 || dHin !== 0) { const doc = docView.rootDoc; const refCent = docView.props.ScreenToLocalTransform().transformPoint(scaleRefPt[0], scaleRefPt[1]); if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight) && doc._nativeWidth !== undefined) { doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth; } const nwidth = docView.nativeWidth; const nheight = docView.nativeHeight; const docwidth = NumCast(doc._width); let docheight = (hgt => (!hgt || isNaN(hgt) ? 20 : hgt))(NumCast(doc._height) || (nheight / nwidth) * docwidth); let dW = docwidth * (dWin / refWidth); let dH = docheight * (dHin / refHeight); const scale = docView.props.ScreenToLocalTransform().Scale; const modifyNativeDim = (e.ctrlKey && doc.nativeDimModifiable) || (doc.layout_forceReflow && !dragBottom && !dragTop) || (doc.nativeHeightUnfrozen && (dragBottom || dragTop || e.ctrlKey)); if (nwidth && nheight) { if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) { docheight = (nheight / nwidth) * docwidth; } if (modifyNativeDim && !dragBottom && !dragTop) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction if (Math.abs(dW) > Math.abs(dH)) dH = (dW * nheight) / nwidth; else dW = (dH * nwidth) / nheight; } } let actualdW = Math.max(docwidth + dW * scale, 20); let actualdH = Math.max(docheight + dH * scale, 20); let dX = !dWin ? 0 : (scale * refCent[0] * -dWin) / refWidth; let dY = !dHin ? 0 : (scale * refCent[1] * -dHin) / refHeight; const preserveNativeDim = !doc._nativeHeightUnfrozen && !doc._nativeDimModifiable; const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { if (Doc.NativeWidth(doc)) { doc._nativeWidth = (actualdW / (docwidth || 1)) * Doc.NativeWidth(doc); } } else { if (!doc._layout_fitWidth || preserveNativeDim) { actualdH = (nheight / nwidth) * actualdW; dYin && (dY = -dW * scale * (nheight / nwidth)); doc._height = actualdH; } else if (!modifyNativeDim || dragBotRight) { doc._height = actualdH; } } doc._width = actualdW; } else { if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._layout_fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) doc._nativeHeight = (actualdH / (docheight || 1)) * Doc.NativeHeight(doc); doc._layout_autoHeight = false; } else { if (!doc._layout_fitWidth || preserveNativeDim) { actualdW = (nwidth / nheight) * actualdH; dXin && (dX = -dH * scale * (nwidth / nheight)); doc._width = actualdW; } else if (!modifyNativeDim || dragBotRight) { doc._width = actualdW; } } if (!modifyNativeDim) { actualdH = (nheight / nwidth) * NumCast(doc._width); //, actualdH); } doc._height = actualdH; } } else { const rotCtr = [docwidth / 2, docheight / 2]; const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI); const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '_scrollHeight']))) * docView.NativeDimScaling(); dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); dW && (doc._width = actualdW); dH && (doc._layout_autoHeight = false); const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI); doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]); } doc.x = NumCast(doc.x) + dX; doc.y = NumCast(doc.y) + dY; doc._layout_modificationDate = new DateField(); } const val = this._dragHeights.get(docView.layoutDoc); if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); }) ); return false; }; @action onPointerUp = (e: PointerEvent): void => { this._resizeHdlId = ''; DocumentView.Interacting = false; this._resizeUndo?.end(); SnappingManager.clearSnapLines(); // detect layout_autoHeight gesture and apply SelectionManager.Views() .map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) .forEach(pair => (pair.doc._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.GetProto(doc).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, }) ) ); Doc.SetNativeWidth(doc, undefined); Doc.SetNativeHeight(doc, undefined); }); }; @computed get selectionTitle(): string { if (SelectionManager.Views().length === 1) { const selected = SelectionManager.Views()[0]; if (selected.ComponentView?.getTitle?.()) { return selected.ComponentView.getTitle(); } if (this._titleControlString.startsWith('=')) { return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ''; } if (this._titleControlString.startsWith('#')) { return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || '-unset-'; } return this._accumulatedTitle; } return SelectionManager.Views().length > 1 ? '-multiple-' : '-unset-'; } @computed get hasIcons() { return SelectionManager.Views().some(docView => docView.rootDoc.layout_fieldKey === 'layout_icon'); } @observable _showRotCenter = false; @observable _rotCenter = [0, 0]; @computed get rotCenter() { if (SelectionManager.Views().length) { const seldocview = SelectionManager.Views()[0]; const loccenter = Utils.rotPt( NumCast(seldocview.rootDoc.rotation_centerX) * NumCast(seldocview.layoutDoc._width), NumCast(seldocview.rootDoc.rotation_centerY) * NumCast(seldocview.layoutDoc._height), (NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI ); return seldocview.props .ScreenToLocalTransform() .inverse() .transformPoint(loccenter.x + NumCast(seldocview.layoutDoc._width) / 2, loccenter.y + NumCast(seldocview.layoutDoc._height) / 2); } return this._rotCenter; } @observable _showNothing = true; render() { const { b, r, x, y } = this.Bounds; const bounds = { b, r, x, y }; const seldocview = SelectionManager.Views().lastElement(); if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { setTimeout(action(() => (this._showNothing = true))); return null; } // sharing const acl = GetEffectiveAcl(!this.showLayoutAcl ? Doc.GetProto(seldocview.rootDoc) : seldocview.rootDoc); const docShareMode = HierarchyMapping.get(acl)!.name; const shareMode = StrCast(docShareMode); var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image; // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; const hideResizers = ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = hideDecorations || seldocview.props.hideOpenButton || seldocview.rootDoc.layout_hideOpenButton || SelectionManager.Views().some(docView => docView.rootDoc._dragOnlyWithinContainer || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = hideDecorations || this._isRounding || this._isRotating || seldocview.props.hideDeleteButton || seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DocData]) : AclEdit; return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== 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, e => click!(e)))}>
); 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; 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)); const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView; const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate? const rotation = SelectionManager.Views().length == 1 ? NumCast(seldocview.rootDoc._rotation) : 0; const resizerScheme = ''; // Radius constants const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; const borderRadius = numberValue(StrCast(seldocview.rootDoc.layout_borderRounding)); const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.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}   */}
) : (
); const titleArea = this._editingTitle ? ( !hideTitle && this.titleBlur()} onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} onKeyDown={hideTitle ? emptyFunction : this.titleEntered} onPointerDown={e => e.stopPropagation()} /> ) : (
{ e.stopPropagation; }}> {hideTitle ? null : ( {this.selectionTitle} )} {sharingMenu} {!useLock ? null : ( toggle ability to interact with document
} placement="top">
e.preventDefault()}>
)}
); return (
{ e.preventDefault(); e.stopPropagation(); }} /> {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? null : (
{hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => 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 : ( <>
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} />
e.preventDefault()} /> {seldocview.props.renderDepth <= 1 || !seldocview.props.docViewPath().lastElement() ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')} )} {useRounding && (
e.preventDefault()} /> )} {hideDocumentButtonBar ? null : (
)}
{useRotation && ( <>
{this._isRotating ? null : ( tap to set rotate center, drag to rotate
}>
e.preventDefault()}> } color={Colors.LIGHT_GRAY} />
)}
{!this._showRotCenter ? null : (
e.preventDefault()} /> )} )}
)}
); } }