diff options
-rw-r--r-- | src/client/util/DragManager.ts | 7 | ||||
-rw-r--r-- | src/client/util/SnappingManager.ts | 7 | ||||
-rw-r--r-- | src/client/views/collections/CollectionSubView.tsx | 18 | ||||
-rw-r--r-- | src/client/views/collections/CollectionView.tsx | 21 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 107 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentLinksButton.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 62 | ||||
-rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 90 | ||||
-rw-r--r-- | src/client/views/nodes/WebBox.tsx | 77 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 4 |
10 files changed, 248 insertions, 147 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index ea13eaa5b..9d6bd4f60 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -329,7 +329,7 @@ export namespace DragManager { DocDragData = dragData as DocumentDragData; const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); eles = eles.filter(e => e); - CanEmbed = dragData.canEmbed || false; + SnappingManager.SetCanEmbed(dragData.canEmbed || false); if (!dragDiv) { dragDiv = document.createElement('div'); dragDiv.className = 'dragManager-dragDiv'; @@ -455,7 +455,7 @@ export namespace DragManager { runInAction(() => docsBeingDragged.push(...docsToDrag)); const hideDragShowOriginalElements = (hide: boolean) => { - dragLabel.style.display = hide && !CanEmbed ? '' : 'none'; + dragLabel.style.display = hide && !SnappingManager.GetCanEmbed() ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; @@ -482,6 +482,7 @@ export namespace DragManager { SnappingManager.SetIsDragging(false); if (batch.end() && undo) UndoManager.Undo(); docsBeingDragged.length = 0; + SnappingManager.SetCanEmbed(false); }); var startWindowDragTimer: any; const moveHandler = (e: PointerEvent) => { @@ -588,7 +589,7 @@ export namespace DragManager { altKey: e.altKey, metaKey: e.metaKey, ctrlKey: e.ctrlKey, - embedKey: CanEmbed, + embedKey: SnappingManager.GetCanEmbed(), }, }; target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 3cb41ab4d..c0cd94067 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -5,6 +5,7 @@ export namespace SnappingManager { class Manager { @observable IsDragging: boolean = false; @observable IsResizing: Doc | undefined; + @observable CanEmbed: boolean = false; @observable public horizSnapLines: number[] = []; @observable public vertSnapLines: number[] = []; @action public clearSnapLines() { @@ -38,10 +39,16 @@ export namespace SnappingManager { export function SetIsResizing(doc: Doc | undefined) { runInAction(() => (manager.IsResizing = doc)); } + export function SetCanEmbed(canEmbed: boolean) { + runInAction(() => (manager.CanEmbed = canEmbed)); + } export function GetIsDragging() { return manager.IsDragging; } export function GetIsResizing() { return manager.IsResizing; } + export function GetCanEmbed() { + return manager.CanEmbed; + } } diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 09e7cdb32..8a1ba0df1 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -8,14 +8,22 @@ import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; +import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { returnFalse, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; +import { DragManager, dropActionType } from '../../util/DragManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; +import { SelectionManager } from '../../util/SelectionManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { DocComponent } from '../DocComponent'; +import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; +import { CollectionView, CollectionViewProps } from './CollectionView'; import React = require('react'); export interface SubCollectionViewProps extends CollectionViewProps { @@ -118,7 +126,7 @@ export function CollectionSubView<X>(moreProps?: X) { childDocs.forEach(d => { // dragging facets const dragged = this.props.childFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); - if (dragged && DragManager.docsBeingDragged.includes(d)) return false; + if (dragged && SnappingManager.GetCanEmbed() && DragManager.docsBeingDragged.includes(d)) return false; let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.props.Document).length > 0; if (notFiltered) { notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.props.Document).length > 0; @@ -486,11 +494,3 @@ export function CollectionSubView<X>(moreProps?: X) { return CollectionSubView; } - -import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DragManager, dropActionType } from '../../util/DragManager'; -import { SelectionManager } from '../../util/SelectionManager'; -import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; -import { CollectionView, CollectionViewProps } from './CollectionView'; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 493f40d77..6e4b9ec07 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,4 @@ -import { computed, observable, runInAction } from 'mobx'; +import { computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; @@ -78,6 +78,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; } + private reactionDisposer: IReactionDisposer | undefined; + @observable _isContentActive: boolean | undefined; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @@ -86,6 +88,21 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab runInAction(() => (this._annotationKeySuffix = returnEmptyString)); } + componentDidMount() { + // we use a reaction/observable instead of a computed value to reduce invalidations. + // There are many variables that aggregate into this boolean output - a change in any of them + // will cause downstream invalidations even if the computed value doesn't change. By making + // this a reaction, downstream invalidations only occur when the reaction value actually changes. + this.reactionDisposer = reaction( + () => (this.isAnyChildContentActive() ? true : this.props.isContentActive()), + active => (this._isContentActive = active), + { fireImmediately: true } + ); + } + componentWillUnmount() { + this.reactionDisposer?.(); + } + get collectionViewType(): CollectionViewType | undefined { const viewField = StrCast(this.layoutDoc._type_collection); if (CollectionView._safeMode) { @@ -220,7 +237,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles); childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle); childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); - isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive()); + isContentActive = (outsideReaction?: boolean) => this._isContentActive; render() { TraceMobx(); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index d80ea2cb2..2897cac0e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1280,12 +1280,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @computed get childPointerEvents() { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = DocumentView.Interacting + const pointerevents = DocumentView.Interacting ? 'none' - : this.props.childPointerEvents?.() ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); - return pointerEvents; + : this.props.childPointerEvents?.() ?? + (this.props.viewDefDivClick || // + (engine === computePassLayout.name && !this.props.isSelected(true)) || + this.isContentActive() === false + ? 'none' + : this.props.pointerEvents?.()); + console.log(`${this.rootDoc.title} pe = ` + pointerevents); + return pointerevents; } - childPointerEventsFunc = () => this.childPointerEvents; + + @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined; + childPointerEventsFunc = () => this._childPointerEvents; childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; @@ -1372,9 +1380,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }); @observable _lightboxDoc: Opt<Doc>; - getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { + getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData { const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); - const childDoc = params.pair.layout; + const childDoc = pair.layout; const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed @@ -1400,7 +1408,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: _width, height: _height, transition: StrCast(childDocLayout.dataTransition), - pair: params.pair, + pair, pointerEvents: Cast(childDoc.pointerEvents, 'string', null), replica: '', }; @@ -1480,7 +1488,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } doFreeformLayout(poolData: Map<string, PoolData>) { - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions({ pair, index: i, collection: this.Document }))); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); return [] as ViewDefResult[]; } @@ -1502,38 +1510,39 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _numLoaded = 1; _lastPoolSize = 0; - get doLayoutComputation() { - const { newPool, computedElementData } = this.doInternalLayoutComputation; + @action + doLayoutComputation = (newPool: Map<string, PoolData>, computedElementData: ViewDefResult[]) => { const array = Array.from(newPool.entries()); - let somethingChanged = array.length !== this._lastPoolSize; this._lastPoolSize = array.length; - runInAction(() => { - for (const entry of array) { - const lastPos = this._cachedPool.get(entry[0]); // last computed pos - const newPos = entry[1]; - if ( - !lastPos || - newPos.color !== lastPos.color || - newPos.backgroundColor !== lastPos.backgroundColor || - newPos.opacity !== lastPos.opacity || - newPos.x !== lastPos.x || - newPos.y !== lastPos.y || - newPos.z !== lastPos.z || - newPos.rotation !== lastPos.rotation || - newPos.zIndex !== lastPos.zIndex || - newPos.transition !== lastPos.transition || - newPos.pointerEvents !== lastPos.pointerEvents - ) { - this._layoutPoolData.set(entry[0], newPos); - somethingChanged = true; - } - if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { - this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); - somethingChanged = true; - } + for (const entry of array) { + const lastPos = this._cachedPool.get(entry[0]); // last computed pos + const newPos = entry[1]; + if ( + !lastPos || + newPos.color !== lastPos.color || + newPos.backgroundColor !== lastPos.backgroundColor || + newPos.opacity !== lastPos.opacity || + newPos.x !== lastPos.x || + newPos.y !== lastPos.y || + newPos.z !== lastPos.z || + newPos.rotation !== lastPos.rotation || + newPos.zIndex !== lastPos.zIndex || + newPos.transition !== lastPos.transition || + newPos.pointerEvents !== lastPos.pointerEvents + ) { + this._layoutPoolData.set(entry[0], newPos); } - }); - if (!somethingChanged) return undefined; + if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { + this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); + } + } + // by returning undefined, we prevent an edit being made to layoutElements when nothing has happened + // this short circuit, prevents lots of downstream mobx invalidations which would have no effect but cause + // a distinct lag at the start of dragging. + // The reason we're here in the first place without a change is that when dragging a document, + // filters are changed on the annotation layers (eg. WebBox) which invalidate the childDoc list + // for the overlay views -- however, in many cases, this filter change doesn't actually affect anything + // (e.g, no annotations, or only opaque annotations). this._cachedPool.clear(); Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); const elements = computedElementData.slice(); @@ -1551,7 +1560,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; - } + }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type) @@ -1605,10 +1614,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection { fireImmediately: true } ); + this._disposers.pointerevents = reaction( + () => { + const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); + return DocumentView.Interacting + ? 'none' + : this.props.childPointerEvents?.() ?? + (this.props.viewDefDivClick || // + (engine === computePassLayout.name && !this.props.isSelected(true)) || + this.isContentActive() === false + ? 'none' + : this.props.pointerEvents?.()); + }, + pointerevents => (this._childPointerEvents = pointerevents as any), + { fireImmediately: true } + ); + this._disposers.layoutComputation = reaction( - () => this.doLayoutComputation, - elements => elements !== undefined && (this._layoutElements = elements || []), - { fireImmediately: true, name: 'doLayout' } + () => this.doInternalLayoutComputation, + ({ newPool, computedElementData }) => (this._layoutElements = this.doLayoutComputation(newPool, computedElementData)), + { fireImmediately: true, name: 'layoutComputationReaction' } ); this._disposers.active = reaction( diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 4db0bf5fa..a26d2e9f3 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -27,6 +27,7 @@ interface DocumentLinksButtonProps { StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed) ShowCount?: boolean; scaling?: () => number; // how uch doc is scaled so that link buttons can invert it + hideCount?: () => boolean; } @observer export class DocumentLinksButton extends React.Component<DocumentLinksButtonProps, {}> { @@ -239,6 +240,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } render() { + if (this.props.hideCount?.()) return null; const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link'; const buttonTitle = 'Tap to view links; double tap to open link collection'; const title = this.props.ShowCount ? buttonTitle : menuTitle; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 3d6b53ccc..30d5a5184 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { Dropdown, DropdownType, Type } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; @@ -136,7 +136,7 @@ export interface DocComponentView { componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) - overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document + overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document (e.g, KeyValueBox always allows pointer events) fieldKey?: string; annotationKey?: string; getTitle?: () => string; @@ -272,8 +272,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this._animateScaleTime ?? 100; } public get displayName() { - return 'DocumentView(' + this.props.Document.title + ')'; + return 'DocumentViewInternal(' + this.props.Document.title + ')'; } // this makes mobx trace() statements more descriptive + public get ContentDiv() { return this._mainCont.current; } @@ -317,8 +318,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } + @observable _pointerEvents: 'none' | 'all' | 'visiblePainted' | undefined; @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { - return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents); + TraceMobx(); + return this._pointerEvents; } @computed get finalLayoutKey() { return StrCast(this.Document.layout_fieldKey, 'layout'); @@ -330,6 +333,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.NativeHeight(); } @computed get disableClickScriptFunc() { + TraceMobx(); const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable; // prettier-ignore return ( @@ -356,6 +360,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } componentDidMount() { this.setupHandlers(); + this._disposers.contentActive = reaction( + () => { + // true - if the document has been activated directly or indirectly (by having its children selected) + // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive + // undefined - it is not active, but it should be responsive to actions that might activate it or its contents (eg clicking) + return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' + ? false + : Doc.ActiveTool !== InkTool.None || SnappingManager.GetCanEmbed() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() + ? true + : undefined; + }, + active => (this._isContentActive = active), + { fireImmediately: true } + ); + this._disposers.pointerevents = reaction( + () => this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents), + pointerevents => (this._pointerEvents = pointerevents), + { fireImmediately: true } + ); } preDropFunc = (e: Event, de: DragManager.DropEvent) => { const dropAction = this.layoutDoc.dropAction as dropActionType; @@ -883,23 +906,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler); setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); - @computed get _isContentActive() { - // true - if the document has been activated directly or indirectly (by having its children selected) - // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive - // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking) - return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' - ? false - : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive() - ? true - : undefined; - } + @observable _isContentActive: boolean | undefined; + isContentActive = (): boolean | undefined => this._isContentActive; childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive @computed get _contentPointerEvents() { - if (this.props.contentPointerEvents) return this.props.contentPointerEvents; - return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents; + TraceMobx(); + return this.props.contentPointerEvents ?? + ((!this.disableClickScriptFunc && // + this.onClickHandler && + !this.props.onBrowseClick?.() && + this.isContentActive() !== true) || + this.isContentActive() === false) + ? 'none' + : this.pointerEvents; } contentPointerEvents = () => this._contentPointerEvents; @computed get contents() { @@ -1304,8 +1326,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick} - onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} - onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)} + onPointerEnter={e => (!SnappingManager.GetIsDragging() || SnappingManager.GetCanEmbed()) && Doc.BrushDoc(this.rootDoc)} + onPointerOver={e => (!SnappingManager.GetIsDragging() || SnappingManager.GetCanEmbed()) && Doc.BrushDoc(this.rootDoc)} onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)} style={{ borderRadius: this.borderRounding, @@ -1426,9 +1448,9 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : '')); } + hideLinkCount = () => this.props.renderDepth === -1 || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; @computed get linkCountView() { - const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; - return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; + return <DocumentLinksButton hideCount={this.hideLinkCount} View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />; } @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 38ff21209..e66fed84b 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,19 +1,19 @@ import React = require('react'); import { Bezier } from 'bezier-js'; -import { computed, action } from 'mobx'; +import { computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; +import { Transform } from '../../util/Transform'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { ViewBoxBaseComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; -import { Transform } from '../../util/Transform'; @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -22,9 +22,6 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { } onClickScriptDisable = returnAlways; - componentDidMount() { - this.props.setContentView?.(this); - } @computed get anchor1() { const anchor1 = DocCast(this.rootDoc.link_anchor_1); const anchor_1 = anchor1?.layout_unrendered ? DocCast(anchor1.annotationOn) : anchor1; @@ -56,45 +53,70 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { } return { left: 0, top: 0, right: 0, bottom: 0, center: undefined }; }; - render() { - if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { - const a = (this.anchor1 ?? this.anchor2)!; - const b = (this.anchor2 ?? this.anchor1)!; - - const parxf = this.props.docViewPath()[this.props.docViewPath().length - 2].ComponentView as CollectionFreeFormView; - const this_xf = parxf?.getTransform() ?? Transform.Identity; //this.props.ScreenToLocalTransform(); - const a_invXf = a.props.ScreenToLocalTransform().inverse(); - const b_invXf = b.props.ScreenToLocalTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(a.rootDoc[Width](), a.rootDoc[Height]()) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(b.rootDoc[Width](), b.rootDoc[Height]()) }; - const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; - const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; + disposer: IReactionDisposer | undefined; + componentDidMount() { + this.props.setContentView?.(this); + this.disposer = reaction( + () => { + if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { + const a = (this.anchor1 ?? this.anchor2)!; + const b = (this.anchor2 ?? this.anchor1)!; - const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; - const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); - const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); - const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; + const parxf = this.props.docViewPath()[this.props.docViewPath().length - 2].ComponentView as CollectionFreeFormView; + const this_xf = parxf?.getTransform() ?? Transform.Identity; //this.props.ScreenToLocalTransform(); + const a_invXf = a.props.ScreenToLocalTransform().inverse(); + const b_invXf = b.props.ScreenToLocalTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(a.rootDoc[Width](), a.rootDoc[Height]()) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(b.rootDoc[Width](), b.rootDoc[Height]()) }; + const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; + const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; - const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); - const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; - setTimeout( - action(() => { - this.layoutDoc.x = lx; - this.layoutDoc.y = ty; - this.layoutDoc._width = rx - lx; - this.layoutDoc._height = by - ty; - }) - ); + const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; + const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); + const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); + const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; + const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); + const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; + return { pts, lx, rx, ty, by }; + } + return undefined; + }, + params => { + this.renderProps = params; + if (params) { + if ( + Math.abs(params.lx - NumCast(this.layoutDoc.x)) > 1e-5 || + Math.abs(params.ty - NumCast(this.layoutDoc.y)) > 1e-5 || + Math.abs(params.rx - params.lx - NumCast(this.layoutDoc._width)) > 1e-5 || + Math.abs(params.by - params.ty - NumCast(this.layoutDoc._height)) > 1e-5 + ) { + this.layoutDoc.x = params?.lx; + this.layoutDoc.y = params?.ty; + this.layoutDoc._width = params.rx - params?.lx; + this.layoutDoc._height = params?.by - params?.ty; + } + } + }, + { fireImmediately: true } + ); + } + componentWillUnmount(): void { + this.disposer?.(); + } + @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined; + render() { + if (this.renderProps) { const highlight = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; - const bez = new Bezier(pts.map(p => ({ x: p[0], y: p[1] }))); + const bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] }))); const text = bez.get(0.5); const linkDesc = StrCast(this.rootDoc.link_description) || 'description'; const strokeWidth = NumCast(this.rootDoc.stroke_width, 4); const dash = StrCast(this.rootDoc.stroke_dash); const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined; + const { pts, lx, ty, rx, by } = this.renderProps; return ( <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}> <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 66e0ed21f..a94279e88 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -861,7 +861,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps key="sidebar" title="Toggle Sidebar" style={{ - display: !this.props.isContentActive() ? 'none' : undefined, top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} @@ -950,42 +949,48 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get SidebarShown() { return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } + renderAnnotations = (childFilters: () => string[]) => ( + <CollectionFreeFormView + {...this.props} + setContentView={this.setInnerContent} + NativeWidth={returnZero} + NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} + renderDepth={this.props.renderDepth + 1} + isAnnotationOverlay={true} + fieldKey={this.annotationKey} + setPreviewCursor={this.setPreviewCursor} + PanelWidth={this.panelWidth} + PanelHeight={this.panelHeight} + ScreenToLocalTransform={this.scrollXf} + NativeDimScaling={returnOne} + focus={this.focus} + childFilters={childFilters} + select={emptyFunction} + isAnyChildContentActive={returnFalse} + bringToFront={emptyFunction} + styleProvider={this.childStyleProvider} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={this.addDocumentWrapper} + childPointerEvents={this.childPointerEvents} + pointerEvents={this.annotationPointerEvents} + /> + ); + @computed get renderOpaqueAnnotations() { + return this.renderAnnotations(this.opaqueFilter); + } + @computed get renderTransparentAnnotations() { + return this.renderAnnotations(this.transparentFilter); + } childPointerEvents = () => (this.props.isContentActive() ? 'all' : undefined); @computed get webpage() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (childFilters: () => string[]) => ( - <CollectionFreeFormView - {...this.props} - setContentView={this.setInnerContent} - NativeWidth={returnZero} - NativeHeight={returnZero} - originTopLeft={false} - isAnnotationOverlayScrollable={true} - renderDepth={this.props.renderDepth + 1} - isAnnotationOverlay={true} - fieldKey={this.annotationKey} - setPreviewCursor={this.setPreviewCursor} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight} - ScreenToLocalTransform={this.scrollXf} - NativeDimScaling={returnOne} - focus={this.focus} - childFilters={childFilters} - select={emptyFunction} - isAnyChildContentActive={returnFalse} - bringToFront={emptyFunction} - styleProvider={this.childStyleProvider} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocumentWrapper} - childPointerEvents={this.childPointerEvents} - pointerEvents={this.annotationPointerEvents} - /> - ); return ( <div className="webBox-outerContent" @@ -1000,8 +1005,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onPointerDown={this.onMarqueeDown}> <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> {this.content} - {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} - {renderAnnotations(this.opaqueFilter)} + <div style={{ display: SnappingManager.GetCanEmbed() ? 'none' : undefined, mixBlendMode: 'multiply' }}>{this.renderTransparentAnnotations}</div> + {this.renderOpaqueAnnotations} {this.annotationLayer} </div> </div> @@ -1050,7 +1055,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(SnappingManager.GetCanEmbed() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc)) return 'none'; @@ -1131,8 +1136,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps removeDocument={this.removeDocument} /> </div> - {this.sidebarHandle} - {!this.props.isContentActive() ? null : this.searchUI} + {!this.props.isContentActive() || SnappingManager.GetIsDragging() ? null : this.sidebarHandle} + {!this.props.isContentActive() || SnappingManager.GetIsDragging() ? null : this.searchUI} </div> ); } diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 2e494aa45..712a5e92f 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -512,7 +512,7 @@ export class PDFViewer extends React.Component<IViewerProps> { panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(SnappingManager.GetCanEmbed() && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none'; @@ -557,7 +557,7 @@ export class PDFViewer extends React.Component<IViewerProps> { ); @computed get overlayTransparentAnnotations() { const transparentChildren = DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.transparentFilter(), []); - return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined); + return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', SnappingManager.GetCanEmbed() && this.props.isContentActive() ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); |