From e3fde25014d523c5f43a138093718899fe17d108 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 7 Feb 2024 16:18:16 -0500 Subject: made various render methods in DocumentView computed getters for efficiency and to avoid artifacts (LInkanchorBox dragging) when something else invalidates causing components to regenerate. fixed linklines to animate when doing a zoom transition and to be able to target texts hyperlinks. fixed link lines to share properties with ink and updated the properties panel / menus to allow editing of either. addding toggling link lines on and off from linkitemmenu --- src/client/views/nodes/DocumentView.tsx | 35 ++-- src/client/views/nodes/LinkBox.tsx | 197 +++++++++++++--------- src/client/views/nodes/formattedText/marks_rts.ts | 5 +- 3 files changed, 143 insertions(+), 94 deletions(-) (limited to 'src/client/views/nodes') diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 5efa028d1..042ae6e55 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -48,6 +48,7 @@ import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import { PresEffect, PresEffectDirection } from './trails'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm'; interface Window { MediaRecorder: MediaRecorder; } @@ -726,7 +727,7 @@ export class DocumentViewInternal extends DocComponent () => (link.link_displayLine = false); - allLinkEndpoints = () => { + @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; return this.filteredLinks.map(link => ( @@ -750,9 +751,9 @@ export class DocumentViewInternal extends DocComponent )); - }; + } - viewBoxContents = () => { + @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); @@ -778,10 +779,10 @@ export class DocumentViewInternal extends DocComponent - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()} + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} ); - }; + } captionStyleProvider = (doc: Opt, props: Opt, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { @@ -814,7 +815,7 @@ export class DocumentViewInternal extends DocComponent { + @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); @@ -888,9 +889,9 @@ export class DocumentViewInternal extends DocComponent ); - }; + } - captionView = () => { + @computed get captionView() { return !this.layout_showCaption ? null : (
); - }; + } renderDoc = (style: object) => { TraceMobx(); @@ -933,15 +934,15 @@ export class DocumentViewInternal extends DocComponent {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( - this.viewBoxContents() + this.viewBoxContents ) : (
- {this.titleView()} - {this.viewBoxContents()} - {this.captionView()} + {this.titleView} + {this.viewBoxContents} + {this.captionView}
)} {this.widgetDecorations ?? null} @@ -1191,8 +1192,8 @@ export class DocumentView extends DocComponent() { if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined }; } // transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox - const transition = this.docViewPath().find((parent: DocumentView) => parent._props.DataTransition?.() || StrCast(parent.Document.dataTransition)); - return { left, top, right, bottom, transition: transition?._props.DataTransition?.() || StrCast(transition?.Document.dataTransition) }; + const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.()); + return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() }; } @computed get nativeWidth() { @@ -1337,6 +1338,7 @@ export class DocumentView extends DocComponent() { } } }; + DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition); ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; @@ -1402,6 +1404,7 @@ export class DocumentView extends DocComponent() { () { @@ -43,19 +46,20 @@ export class LinkBox extends ViewBoxBaseComponent() { this.disposer = reaction( () => ({ drag: SnappingManager.IsDragging, a: this.anchor1, b: this.anchor2 }), ({ drag, a, b }) => { - setTimeout( - // need to wait for drag manager to set 'hidden' flag on dragged elements - action(() => { - let a1 = a && document.getElementById(a.Guid); - let a2 = b && document.getElementById(b.Guid); - if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; - else { - for (; a1 && !a1.hidden; a1 = a1.parentElement); - for (; a2 && !a2.hidden; a2 = a2.parentElement); - this._hide = a1 || a2 ? true : false; - } - }) - ); + !LightboxView.Contains(this.DocumentView?.()) && + setTimeout( + // need to wait for drag manager to set 'hidden' flag on dragged elements + action(() => { + let a1 = a && document.getElementById(a.Guid); + let a2 = b && document.getElementById(b.Guid); + if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; + else { + for (; a1 && !a1.hidden; a1 = a1.parentElement); + for (; a2 && !a2.hidden; a2 = a2.parentElement); + this._hide = a1 || a2 ? true : false; + } + }) + ); }, { fireImmediately: true } ); @@ -63,89 +67,130 @@ export class LinkBox extends ViewBoxBaseComponent() { select = (ctrlKey: boolean, shiftKey: boolean) => (LinkManager.Instance.currentLink = this.Document); - descriptionDown = (e: React.PointerEvent) => { - setupMoveUpEvents( - this, - e, - returnFalse, - returnFalse, - action(() => { - LinkManager.Instance.currentLink = this.Document; - LinkDescriptionPopup.Instance.popupX = e.clientX; - LinkDescriptionPopup.Instance.popupY = e.clientY; - LinkDescriptionPopup.Instance.display = true; - - const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.Instance.popupX + 200 > rect.width) { - LinkDescriptionPopup.Instance.popupX -= 190; - } - if (LinkDescriptionPopup.Instance.popupY + 100 > rect.height) { - LinkDescriptionPopup.Instance.popupY -= 40; - } - }), - false - ); - }; render() { - trace(); + if (this._hide) return null; const a = this.anchor1; const b = this.anchor2; this._forceAnimate; - if (a && b && !this._hide) { + if (a && b && !LightboxView.Contains(this.DocumentView?.())) { + // text selection bounds are not directly observable, so we have to + // force an update when anything that could affect them changes (text edits causing reflow, scrolling) + a.Document[a.LayoutFieldKey]; + b.Document[b.LayoutFieldKey]; + a.Document.layout_scrollTop; + b.Document.layout_scrollTop; + const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) const bxf = b.screenToViewTransform(); const scale = a.CollectionFreeFormView === this.DocumentView?.().CollectionFreeFormView ? axf.Scale : bxf.Scale; const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition - const bt = b.getBounds?.transition; + const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) + + // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext ), + // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // otherwise, we just use the computed nearest point on the document boundary to the target Document + const targetAhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement(); + const targetBhyperlink = Array.from(window.document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement(); + + const aid = targetAhyperlink?.id || a.Document[Id]; + const bid = targetBhyperlink?.id || b.Document[Id]; + if (!document.getElementById(aid) || !document.getElementById(bid)) { + setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); + return null; + } + if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); - const color = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); - const { strokeWidth, stroke_startMarker, stroke_endMarker } = this.Document; - const dash = StrCast(this.Document.stroke_dash); - const stroke = highlightColor ?? 'lightblue'; + const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); + const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document; + const strokeWidth = NumCast(stroke_width, 4); const linkDesc = StrCast(this.dataDoc.link_description) || ' '; const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( - - {labelText} - - } - passProps={{ onPointerDown: this.descriptionDown }} - /> + <> + {!highlightColor ? null : ( + + )} + + linkDesc} + SetValue={action(val => { + this.Document[DocData].link_description = val; + return true; + })} + /> + + {/* (this.Document[DocData].link_description = val))} + fillWidth + /> */} + + } + passProps={{}} + /> + ); } - return null; return (
(p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; + return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -104,7 +105,7 @@ export const marks: { [index: string]: MarkSpec } = { node.attrs.title, ], ] - : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; + : ['a', { id: '' + Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; }, }, -- cgit v1.2.3-70-g09d2