diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 21 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.scss | 8 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 175 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 117 | ||||
| -rw-r--r-- | src/client/views/nodes/RecordingBox/RecordingBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 9 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/generativeFill/GenerativeFill.tsx | 32 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 194 |
9 files changed, 396 insertions, 164 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 4a438f826..f9afe4d53 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -6,13 +6,12 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { numberRange } from '../../../Utils'; +import { numberRange, OmitKeys } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; -import { InkingStroke } from '../InkingStroke'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; @@ -183,6 +182,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF } }; + dragEnding = () => this.props.CollectionFreeFormView?.dragEnding(); + dragStarting = () => this.props.CollectionFreeFormView?.dragStarting(false, true); + nudge = (x: number, y: number) => { this.props.Document.x = NumCast(this.props.Document.x) + x; this.props.Document.y = NumCast(this.props.Document.y) + y; @@ -191,6 +193,17 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); returnThis = () => this; + + /// this indicates whether the doc view is activated because of its relationshop to a group + // 'group' - this is a group that is activated because it's on an active canvas, but is not part of some other group + // 'child' - this is a group child that is activated because its containing group is activated + // 'inactive' - this is a group child but it is not active + // undefined - this is not activated by a group + isGroupActive = () => { + if (this.props.CollectionFreeFormView.isAnyChildContentActive()) return undefined; + const isGroup = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); + return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined; + }; render() { TraceMobx(); const divProps: DocumentViewProps = { @@ -200,8 +213,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF ScreenToLocalTransform: this.screenToLocalTransform, PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, + isGroupActive: this.isGroupActive, }; - const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._stroke_isInkMask; return ( <div className="collectionFreeFormDocumentView-container" @@ -213,7 +226,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)), zIndex: this.ZInd, display: this.sizeProvider?.width ? undefined : 'none', - pointerEvents: isInk ? 'none' : undefined, + pointerEvents: 'none', }}> {this.props.renderCutoffProvider(this.props.Document) ? ( <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} /> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index b25540dd3..931594568 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -24,6 +24,8 @@ width: 100%; height: 100%; border-radius: inherit; + // bcz: can't clip the title this way because groups need to be able to render outside of overflow region to support drag/drop-extending the group borders + // overflow: hidden; // need this so that title will be clipped when borderRadius is set // transition: outline 0.3s linear; // background: $white; //overflow: hidden; @@ -118,7 +120,7 @@ display: flex; justify-content: center; align-items: center; - + .sharingIndicator { height: 30px; width: 30px; @@ -165,6 +167,7 @@ height: 100%; border-radius: inherit; white-space: normal; + overflow: hidden; // so that titles will clip when borderRadius is set .documentView-styleContentWrapper { width: 100%; @@ -185,6 +188,7 @@ text-overflow: ellipsis; white-space: pre; position: absolute; + display: flex; // this allows title field dropdown to be inline with editable title } .documentView-titleWrapper-hover { @@ -214,7 +218,7 @@ .documentView-node:hover { > .documentView-styleWrapper { > .documentView-titleWrapper-hover { - display: inline-block; + display: flex; } // > .documentView-contentsView { // opacity: 0.5; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index da665a502..3d6b53ccc 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1,4 +1,5 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { Dropdown, DropdownType, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -17,7 +18,7 @@ import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { DocOptions, Docs, DocUtils, FInfo } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DictationManager } from '../../util/DictationManager'; @@ -38,7 +39,6 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; -import { InkingStroke } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { UndoStack } from '../UndoStack'; @@ -48,11 +48,11 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { KeyValueBox } from './KeyValueBox'; const { Howl } = require('howler'); interface Window { @@ -120,7 +120,6 @@ export interface DocComponentView { addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. - shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views select?: (ctrlKey: boolean, shiftKey: boolean) => void; focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>; menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. @@ -142,6 +141,7 @@ export interface DocComponentView { annotationKey?: string; getTitle?: () => string; getCenter?: (xf: Transform) => { X: number; Y: number }; + screenBounds?: () => { left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }; ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; @@ -154,8 +154,8 @@ export interface DocumentViewSharedProps { renderDepth: number; Document: Doc; DataDoc?: Doc; - contentBounds?: () => undefined | { x: number; y: number; r: number; b: number }; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document + isGroupActive?: () => string | undefined; // is this document part of a group that is active suppressSetHeight?: boolean; setContentView?: (view: DocComponentView) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; @@ -219,7 +219,7 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideLinkAnchors?: boolean; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events - contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + contentPointerEvents?: 'none' | 'all' | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents radialMenu?: String[]; LayoutTemplateString?: string; dontCenter?: 'x' | 'y' | 'xy'; @@ -425,7 +425,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse; onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { + if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; + if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); @@ -496,7 +497,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // prettier-ignore clickFunc ?? (() => (sendToBack ? this.props.DocumentView().props.bringToFront(this.rootDoc, true) : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? - this.props.select(e.ctrlKey || e.metaKey || e.shiftKey))); + this.props.select(e.ctrlKey||e.shiftKey, e.metaKey))); const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); @@ -513,6 +514,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerDown = (e: React.PointerEvent): void => { + if (this.props.isGroupActive?.() === 'child' && !this.props.isDocumentActive?.()) return; this._longPressSelector = setTimeout(() => { if (DocumentView.LongPress) { if (this.rootDoc.undoIgnoreFields) { @@ -536,7 +538,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !this.props.onBrowseClick?.() && !this.Document.ignoreClick && e.button === 0 && - this.pointerEvents !== 'none' && !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); @@ -622,7 +623,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.props.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); - alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); + alert( + (e.target as any)?.closest?.('*.lm_content') + ? "You can't perform this move most likely because you didn't drag the document's title bar to enable embedding in a different document." + : 'Linking to document tabs not yet supported. Drop link on document content.' + ); return true; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; @@ -635,6 +640,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) { const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); + if (de.complete.linkDocument) { + de.complete.linkDocument.layout_isSvg = true; + this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.addDocument(de.complete.linkDocument); + } } e.stopPropagation(); return true; @@ -889,17 +898,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps /// 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; } contentPointerEvents = () => this._contentPointerEvents; @computed get contents() { TraceMobx(); - const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; + const isInk = this.layoutDoc._layout_isSvg && !this.props.LayoutTemplateString; + const noBackground = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent'); return ( <div className="documentView-contentsView" style={{ - pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all', + pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? 'all', height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> <DocumentContentsView @@ -1063,6 +1074,41 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }; captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption'); + @observable _changingTitleField = false; + @observable _dropDownInnerWidth = 0; + fieldsDropdown = (inputOptions: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { + const filteredOptions = new Set(inputOptions); + const scaling = this.titleHeight / 30; /* height of Dropdown */ + Object.entries(DocOptions) + .filter(opts => opts[1].filterable) + .forEach((pair: [string, FInfo]) => filteredOptions.add(pair[0])); + filteredOptions.add(StrCast(this.layoutDoc.layout_showTitle)); + const options = Array.from(filteredOptions) + .filter(f => f) + .map(facet => ({ val: facet, text: facet })); + return ( + <div style={{ width: dropdownWidth }}> + <div + ref={action((r: any) => r && (this._dropDownInnerWidth = Number(getComputedStyle(r).width.replace('px', ''))))} + onPointerDown={action(e => (this._changingTitleField = true))} + style={{ width: 'max-content', transformOrigin: 'left', transform: `scale(${scaling})` }}> + <Dropdown + activeChanged={action(isOpen => !isOpen && (this._changingTitleField = false))} + selectedVal={placeholder} + setSelectedVal={onChange} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + type={Type.TERT} + closeOnSelect={true} + dropdownType={DropdownType.SELECT} + items={options} + width={100} + fillWidth + /> + </div> + </div> + ); + }; @computed get innards() { TraceMobx(); const showTitle = this.layout_showTitle?.split(':')[0]; @@ -1090,8 +1136,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; - const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor)); + const background = StrCast( + this.layoutDoc.layout_headingColor, + StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(this.layoutDoc.layout_headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor))) + ); + const dropdownWidth = this._titleRef.current?._editing || this._changingTitleField ? Math.max(10, (this._dropDownInnerWidth * this.titleHeight) / 30) : 0; const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); + // displays a 'title' at the top of a document. The title contents default to the 'title' field, but can be changed to one or more fields by + // setting layout_showTitle using the format: field1[;field2[...][:hover]] + // from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] const titleView = !showTitle ? null : ( <div className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} @@ -1099,39 +1152,58 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, - width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button + width: 100 - sidebarWidthPercent + '%', color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> - <EditableView - ref={this._titleRef} - contents={showTitle - .split(';') - .map(field => field.trim()) - .map(field => targetDoc[field]?.toString()) - .join('\\')} - display={'block'} - fontSize={10} - GetValue={() => { - return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; - }} - SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { - if (this.rootDoc.layout_showTitle) { - this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else if (!this.props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; + {!dropdownWidth + ? null + : this.fieldsDropdown( + [], + dropdownWidth, + StrCast(this.layoutDoc.layout_showTitle).split(':')[0], + action((field: string | number) => { + if (this.rootDoc.layout_showTitle) { + this.rootDoc._layout_showTitle = field; + } else if (!this.props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = field; + } + this._changingTitleField = false; + }), + action(() => (this._changingTitleField = false)) + )} + <div + style={{ + width: `calc(100% - ${dropdownWidth}px)`, + minWidth: '100px', + color: this._titleRef.current?._editing || this._changingTitleField ? 'black' : undefined, + background: this._titleRef.current?._editing || this._changingTitleField ? 'yellow' : undefined, + }}> + <EditableView + ref={this._titleRef} + contents={showTitle + .split(';') + .map(field => targetDoc[field.trim()]?.toString()) + .join(' \\ ')} + display="block" + oneLine={true} + fontSize={(this.titleHeight / 15) * 10} + GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.rootDoc, showTitle.split(';')[0]))} + SetValue={undoBatch((input: string) => { + if (input?.startsWith('#')) { + if (this.rootDoc.layout_showTitle) { + this.rootDoc._layout_showTitle = input?.substring(1); + } else if (!this.props.layout_showTitle) { + Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; + } + } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { + KeyValueBox.SetField(targetDoc, showTitle, input); } - } else { - var value = input.replace(new RegExp(showTitle + '='), '') as string | number; - if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (showTitle.includes('Date') || showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); - } - return true; - })} - /> + return true; + })} + /> + </div> </div> ); return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( @@ -1237,10 +1309,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)} style={{ borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, + pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, // visible painted means that the underlying doc contents are irregular and will process their own pointer events (otherwise, the contents are expected to fill the entire doc view box so we can handle pointer events here) }}> <> - {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} + {this._componentView instanceof KeyValueBox ? renderDoc : DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} {borderPath?.jsx} </> </div> @@ -1414,16 +1486,20 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.props.dontCenter?.includes('y') ? 0 : this.Yshift; } - public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); + public toggleNativeDimensions = () => this.docView && this.rootDoc.type !== DocumentType.INK && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight()); public getBounds = () => { if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { return undefined; } + if (this.docView._componentView?.screenBounds) { + return this.docView._componentView.screenBounds(); + } const xf = this.docView.props .ScreenToLocalTransform() .scale(this.trueNativeWidth() ? this.nativeScaling : 1) .inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; + if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; @@ -1482,7 +1558,16 @@ export class DocumentView extends React.Component<DocumentViewProps> { scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale; docViewPathFunc = () => this.docViewPath; isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); - select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection); + select = (extendSelection: boolean, focusSelection?: boolean) => { + SelectionManager.SelectView(this, extendSelection); + if (focusSelection) { + DocumentManager.Instance.showDocument(this.rootDoc, { + willZoomCentered: true, + zoomScale: 0.9, + zoomTime: 500, + }); + } + }; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; PanelWidth = () => this.panelWidth; diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index efb949a47..38ff21209 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,11 +1,19 @@ import React = require('react'); +import { Bezier } from 'bezier-js'; +import { computed, action } from 'mobx'; import { observer } from 'mobx-react'; -import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils'; +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 { 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>() { @@ -17,8 +25,113 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { 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; + return DocumentManager.Instance.getDocumentView(anchor_1, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + } + @computed get anchor2() { + const anchor2 = DocCast(this.rootDoc.link_anchor_2); + const anchor_2 = anchor2?.layout_unrendered ? DocCast(anchor2.annotationOn) : anchor2; + return DocumentManager.Instance.getDocumentView(anchor_2, this.props.docViewPath()[this.props.docViewPath().length - 2]); // this.props.docViewPath().lastElement()); + } + screenBounds = () => { + if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { + const a_invXf = this.anchor1.props.ScreenToLocalTransform().inverse(); + const b_invXf = this.anchor2.props.ScreenToLocalTransform().inverse(); + const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(this.anchor1.rootDoc[Width](), this.anchor1.rootDoc[Height]()) }; + const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(this.anchor2.rootDoc[Width](), this.anchor2.rootDoc[Height]()) }; + + const pts = [] as number[][]; + pts.push([(a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2]); + pts.push(Utils.getNearestPointInPerimeter(a_scrBds.tl[0], a_scrBds.tl[1], a_scrBds.br[0] - a_scrBds.tl[0], a_scrBds.br[1] - a_scrBds.tl[1], (b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2)); + pts.push(Utils.getNearestPointInPerimeter(b_scrBds.tl[0], b_scrBds.tl[1], b_scrBds.br[0] - b_scrBds.tl[0], b_scrBds.br[1] - b_scrBds.tl[1], (a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2)); + pts.push([(b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2]); + const agg = aggregateBounds( + pts.map(pt => ({ x: pt[0], y: pt[1] })), + 0, + 0 + ); + return { left: agg.x, top: agg.y, right: agg.r, bottom: agg.b, center: undefined }; + } + return { left: 0, top: 0, right: 0, bottom: 0, center: undefined }; + }; render() { - if (this.dataDoc.treeView_Open === undefined) setTimeout(() => (this.dataDoc.treeView_Open = true)); + 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]) }; + + 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])]; + setTimeout( + action(() => { + this.layoutDoc.x = lx; + this.layoutDoc.y = ty; + this.layoutDoc._width = rx - lx; + this.layoutDoc._height = by - ty; + }) + ); + + 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 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; + 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' }}> + <defs> + <filter x="0" y="0" width="1" height="1" id={`${this.rootDoc[Id] + 'background'}`}> + <feFlood floodColor={`${StrCast(this.rootDoc._backgroundColor, 'lightblue')}`} result="bg" /> + <feMerge> + <feMergeNode in="bg" /> + <feMergeNode in="SourceGraphic" /> + </feMerge> + </filter> + </defs> + <path + className="collectionfreeformlinkview-linkLine" + style={{ + pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // + stroke: highlightColor ?? 'lightblue', + strokeDasharray, + strokeWidth, + transition: 'inherit', + }} + d={`M ${pts[1][0] - lx} ${pts[1][1] - ty} C ${pts[1][0] + pts[1][0] - pts[0][0] - lx} ${pts[1][1] + pts[1][1] - pts[0][1] - ty}, + ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`} + /> + <text + filter={`url(#${this.rootDoc[Id] + 'background'})`} + style={{ pointerEvents: this.props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }} + x={text.x - lx} + y={text.y - ty}> + <tspan> </tspan> + <tspan dy="2">{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}</tspan> + <tspan dy="2"> </tspan> + </text> + </svg> + </div> + ); + } return ( <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}> <ComparisonBox diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 481e43feb..116069cbd 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -123,7 +123,7 @@ export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() { Doc.AddToMyOverlay(value); DocumentManager.Instance.AddViewRenderedCb(value, docView => { Doc.UserDoc().currentRecording = docView.rootDoc; - SelectionManager.SelectSchemaViewDoc(value); + docView.select(false); RecordingBox.resumeWorkspaceReplaying(value); }); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index 348bdd79e..818c0cbe7 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -5,6 +5,14 @@ height: 100%; min-height: 100%; } +.formattedTextBox-inner.centered, +.formattedTextBox-inner-rounded.centered { + align-items: center; + display: flex; + .ProseMirror { + min-height: unset; + } +} .ProseMirror:focus { outline: none !important; @@ -29,7 +37,6 @@ audiotag:hover { background: inherit; padding: 0; border-width: 0px; - border-radius: inherit; border-color: $medium-gray; box-sizing: border-box; background-color: inherit; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 46cd6db8f..6535ac6de 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -2150,7 +2150,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onScroll={this.onScroll} onDrop={this.ondrop}> <div - className={`formattedTextBox-inner${rounded}`} + className={`formattedTextBox-inner${rounded} ${this.layoutDoc.layout_centered ? 'centered' : ''}`} ref={this.createDropTarget} style={{ padding: StrCast(this.layoutDoc._textBoxPadding), diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 1ec6d6e3f..3093287e9 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -20,6 +20,8 @@ import { CursorData, ImageDimensions, Point } from './generativeFillUtils/genera import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; import { PointerHandler } from './generativeFillUtils/PointerHandler'; import React = require('react'); +import { DocumentManager } from '../../../util/DocumentManager'; +import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; enum BrushStyle { ADD, @@ -303,19 +305,21 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD originalImg.current = currImg.current; originalDoc.current = parentDoc.current; const { urls } = res as APISuccess; - const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); - const imgRes = await Promise.all( - imgUrls.map(async url => { - const saveRes = await onSave(url); - return [url, saveRes as Doc]; - }) - ); - setEdits(imgRes); - const image = new Image(); - image.src = imgUrls[0]; - ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); - currImg.current = image; - parentDoc.current = imgRes[0][1] as Doc; + if (res.status !== 'error') { + const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height))); + const imgRes = await Promise.all( + imgUrls.map(async url => { + const saveRes = await onSave(url); + return [url, saveRes as Doc]; + }) + ); + setEdits(imgRes); + const image = new Image(); + image.src = imgUrls[0]; + ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height); + currImg.current = image; + parentDoc.current = imgRes[0][1] as Doc; + } } catch (err) { console.log(err); } @@ -418,7 +422,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD ImageBox.setImageEditorOpen(false); ImageBox.setImageEditorSource(''); if (newCollectionRef.current) { - newCollectionRef.current.fitContentOnce = true; + DocumentManager.Instance.AddViewRenderedCb(newCollectionRef.current, dv => (dv.ComponentView as CollectionFreeFormView)?.fitContentOnce()); } setEdits([]); }; diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index a88dd6035..87ffb0112 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; @@ -619,13 +619,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.timecodeToShow !== undefined)) { + if (bestTarget._layout_currentTimecode !== activeItem.timecodeToShow) { + bestTarget._layout_currentTimecode = activeItem.timecodeToShow; + changed = true; + } + } if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) { if (bestTarget.fillColor !== activeItem.config_fillColor) { - Doc.GetProto(bestTarget).fillColor = activeItem.config_fillColor; + Doc.GetProto(bestTarget).fillColor = StrCast(activeItem.config_fillColor, StrCast(bestTarget.fillColor)); changed = true; } if (bestTarget.color !== activeItem.config_color) { - Doc.GetProto(bestTarget).color = activeItem.config_color; + Doc.GetProto(bestTarget).color = StrCast(activeItem.config_color, StrCast(bestTarget.color)); changed = true; } if (bestTarget.width !== activeItem.width) { @@ -933,7 +939,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.childDocs.forEach((doc, index) => { const curDoc = Cast(doc, Doc, null); const tagDoc = PresBox.targetRenderedDoc(curDoc); - const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc); + const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, curDoc); let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined; if (curDoc.presentation_hide) { if (index !== this.itemIndex) { @@ -967,7 +973,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { opacity = 0; } } - opacity !== undefined && (tagDoc.opacity = opacity); + opacity !== undefined && (tagDoc.opacity = opacity === 1 ? undefined : opacity); }); }; @@ -1405,71 +1411,55 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1); // Adds the index in the pres path graphically - @computed get order() { + orderedPathLabels = (collection: Doc) => { const order: JSX.Element[] = []; - const docs: Doc[] = []; - const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement(); + const docs = new Set<Doc>(); + const presCollection = collection; const dv = DocumentManager.Instance.getDocumentView(presCollection); - this.childDocs - .filter(doc => Cast(doc.presentation_targetDoc, Doc, null)) - .forEach((doc, index) => { - const tagDoc = Cast(doc.presentation_targetDoc, Doc, null); - const srcContext = Cast(tagDoc.embedContainer, Doc, null); + this.childDocs.forEach((doc, index) => { + const tagDoc = PresBox.targetRenderedDoc(doc); + const srcContext = Cast(tagDoc.embedContainer, Doc, null); + const labelCreator = (top: number, left: number, edge: number, fontSize: number) => ( + <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top, left, width: edge, height: edge, fontSize }} onClick={() => this.selectElement(doc)}> + <div className="pathOrder-frame">{index + 1}</div> + </div> + ); + if (presCollection === srcContext) { + const gap = 2; const width = NumCast(tagDoc._width) / 10; const height = Math.max(NumCast(tagDoc._height) / 10, 15); const edge = Math.max(width, height); const fontSize = edge * 0.8; - const gap = 2; - if (presCollection === srcContext) { - // Case A: Document is contained within the collection - if (docs.includes(tagDoc)) { - const prevOccurances: number = this.getAllIndexes(docs, tagDoc).length; - docs.push(tagDoc); - order.push( - <div - className="pathOrder" - key={tagDoc.id + 'pres' + index} - style={{ top: NumCast(tagDoc.y) + (prevOccurances * (edge + gap) - edge / 2), left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }} - onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - ); - } else { - docs.push(tagDoc); - order.push( - <div - className="pathOrder" - key={tagDoc.id + 'pres' + index} - style={{ top: NumCast(tagDoc.y) - edge / 2, left: NumCast(tagDoc.x) - edge / 2, width: edge, height: edge, fontSize: fontSize }} - onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - ); - } - } else if (doc.config_pinView && presCollection === tagDoc && dv) { - // Case B: Document is presPinView and is presCollection - const scale: number = 1 / NumCast(doc.config_viewScale); - const height: number = dv.props.PanelHeight() * scale; - const width: number = dv.props.PanelWidth() * scale; - const indWidth = width / 10; - const indHeight = Math.max(height / 10, 15); - const indEdge = Math.max(indWidth, indHeight); - const indFontSize = indEdge * 0.8; - const xLoc: number = NumCast(doc.config_panX) - width / 2; - const yLoc: number = NumCast(doc.config_panY) - height / 2; - docs.push(tagDoc); - order.push( - <> - <div className="pathOrder" key={tagDoc.id + 'pres' + index} style={{ top: yLoc - indEdge / 2, left: xLoc - indEdge / 2, width: indEdge, height: indEdge, fontSize: indFontSize }} onClick={() => this.selectElement(doc)}> - <div className="pathOrder-frame">{index + 1}</div> - </div> - <div className="pathOrder-presPinView" style={{ top: yLoc, left: xLoc, width: width, height: height, borderWidth: indEdge / 10 }}></div> - </> - ); + // Case A: Document is contained within the collection + if (docs.has(tagDoc)) { + const prevOccurences = this.getAllIndexes(Array.from(docs), tagDoc).length; + order.push(labelCreator(NumCast(tagDoc.y) + (prevOccurences * (edge + gap) - edge / 2), NumCast(tagDoc.x) - edge / 2, edge, fontSize)); + } else { + order.push(labelCreator(NumCast(tagDoc.y) - edge / 2, NumCast(tagDoc.x) - edge / 2, edge, fontSize)); } - }); + } else if (doc.config_pinView && presCollection === tagDoc && dv) { + // Case B: Document is presPinView and is presCollection + const scale = 1 / NumCast(doc.config_viewScale); + const viewBounds = NumListCast(doc.config_viewBounds, [0, 0, dv.props.PanelWidth(), dv.props.PanelHeight()]); + const height = (viewBounds[3] - viewBounds[1]) * scale; + const width = (viewBounds[2] - viewBounds[0]) * scale; + const indWidth = width / 10; + const indHeight = Math.max(height / 10, 15); + const indEdge = Math.max(indWidth, indHeight); + const indFontSize = indEdge * 0.8; + const left = NumCast(doc.config_panX) - width / 2; + const top = NumCast(doc.config_panY) - height / 2; + order.push( + <> + {labelCreator(top - indEdge / 2, left - indEdge / 2, indEdge, indFontSize)} + <div className="pathOrder-presPinView" style={{ top, left, width, height, borderWidth: indEdge / 10 }}></div> + </> + ); + } + docs.add(tagDoc); + }); return order; - } + }; /** * Method called for viewing paths which adds a single line with @@ -1479,41 +1469,57 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * (Design needed for when documents in presentation trail are in another * collection) */ - @computed get paths() { + pathLines = (collection: Doc) => { let pathPoints = ''; - this.childDocs.forEach((doc, index) => { - const tagDoc = PresBox.targetRenderedDoc(doc); - if (tagDoc) { - const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; - const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } else if (doc.config_pinView) { - const n1x = NumCast(doc.config_panX); - const n1y = NumCast(doc.config_panY); - if ((index = 0)) pathPoints = n1x + ',' + n1y; - else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; - } - }); + this.childDocs + .filter(doc => PresBox.targetRenderedDoc(doc)?.embedContainer === collection) + .forEach((doc, index) => { + const tagDoc = PresBox.targetRenderedDoc(doc); + if (tagDoc) { + const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2; + const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2; + if ((index = 0)) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; + } else if (doc.config_pinView) { + const n1x = NumCast(doc.config_panX); + const n1y = NumCast(doc.config_panY); + if ((index = 0)) pathPoints = n1x + ',' + n1y; + else pathPoints = pathPoints + ' ' + n1x + ',' + n1y; + } + }); return ( - <polyline - points={pathPoints} - style={{ - opacity: 1, - stroke: '#69a6db', - strokeWidth: 5, - strokeDasharray: '10 5', - boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', - }} - fill="none" - markerStart="url(#markerArrow)" - markerMid="url(#markerSquare)" - markerEnd="url(#markerSquareFilled)" - /> + <> + <div className="presPathLabels">{PresBox.Instance?.orderedPathLabels(collection)}</div> + <svg key="svg" className="presPaths"> + <defs> + <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + <marker id="markerSquareFilled" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible"> + <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="#69a6db" /> + </marker> + <marker id="markerArrow" markerWidth="3" markerHeight="3" refX="2" refY="4" orient="auto" overflow="visible"> + <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" /> + </marker> + </defs> + <polyline + points={pathPoints} + style={{ + opacity: 1, + stroke: '#69a6db', + strokeWidth: 5, + strokeDasharray: '10 5', + boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)', + }} + fill="none" + markerStart="url(#markerArrow)" + markerMid="url(#markerSquare)" + markerEnd="url(#markerSquareFilled)" + /> + </svg> + </> ); - } - getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection. or better yet, draw everything in an overlay layer instad of within a collection - + }; // Converts seconds to ms and updates presentation_transition public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => { let timeInMS = Number(number) * 1000; |
