diff options
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 35 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 197 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/marks_rts.ts | 5 |
3 files changed, 143 insertions, 94 deletions
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<FieldViewProps & Document }; removeLinkByHiding = (link: Doc) => () => (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<FieldViewProps & Document /> </div> )); - }; + } - 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<FieldViewProps & Document setTitleFocus={this.setTitleFocus} hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()} + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); - }; + } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, 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<FieldViewProps & Document * 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] **/ - titleView = () => { + @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<FieldViewProps & Document </div> </div> ); - }; + } - captionView = () => { + @computed get captionView() { return !this.layout_showCaption ? null : ( <div className="documentView-captionWrapper" @@ -913,7 +914,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document /> </div> ); - }; + } renderDoc = (style: object) => { TraceMobx(); @@ -933,15 +934,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? this._props.DataTransition?.() || StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + transition: !this._animateScalingTo ? this._props.DataTransition?.() : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( - this.viewBoxContents() + this.viewBoxContents ) : ( <div className="documentView-styleWrapper"> - {this.titleView()} - {this.viewBoxContents()} - {this.captionView()} + {this.titleView} + {this.viewBoxContents} + {this.captionView} </div> )} {this.widgetDecorations ?? null} @@ -1191,8 +1192,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { 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<DocumentViewProps>() { } } }; + 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<DocumentViewProps>() { <DocumentViewInternal {...this._props} fieldKey={this.LayoutFieldKey} + DataTransition={this.DataTransition} DocumentView={this.selfView} docViewPath={this.docViewPath} PanelWidth={this.PanelWidth} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 85d22abfd..0788e5adc 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,18 +1,21 @@ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; +import { DocData } from '../../../fields/DocSymbols'; +import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, lightOrDark, returnFalse, setupMoveUpEvents } from '../../../Utils'; +import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { EditableView } from '../EditableView'; +import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { FieldView, FieldViewProps } from './FieldView'; import './LinkBox.scss'; -import { LinkDescriptionPopup } from './LinkDescriptionPopup'; @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -43,19 +46,20 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { 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<FieldViewProps>() { 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 <a>), + // 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 ( - <Xarrow - divContainerStyle={{ transform: `scale(${scale})` }} - start={a.Guid} - end={b.Guid} // - strokeWidth={NumCast(strokeWidth, 4)} - dashness={dash ? true : false} - showHead={stroke_startMarker ? true : false} - showTail={stroke_endMarker ? true : false} - tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} - headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} - color={stroke} - labels={ - <div - onPointerDown={this.descriptionDown} // - style={{ - borderRadius: '20%', // - padding: '3', - pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, - background: stroke, - color: color || lightOrDark(stroke), - fontSize, - fontFamily /*, fontStyle: 'italic'*/, - }}> - {labelText} - </div> - } - passProps={{ onPointerDown: this.descriptionDown }} - /> + <> + {!highlightColor ? null : ( + <Xarrow + divContainerStyle={{ transform: `scale(${scale})` }} + start={aid} + end={bid} // + strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)} + showHead={stroke_startMarker ? true : false} + showTail={stroke_endMarker ? true : false} + headSize={NumCast(stroke_markerScale, 3)} + tailSize={NumCast(stroke_markerScale, 3)} + tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + color={highlightColor} + /> + )} + <Xarrow + divContainerStyle={{ transform: `scale(${scale})` }} + start={aid} + end={bid} // + strokeWidth={strokeWidth} + dashness={Number(stroke_dash) ? true : false} + showHead={stroke_startMarker ? true : false} + showTail={stroke_endMarker ? true : false} + headSize={NumCast(stroke_markerScale, 3)} + tailSize={NumCast(stroke_markerScale, 3)} + tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + color={color} + labels={ + <div + style={{ + borderRadius: '8px', + pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, + fontSize, + fontFamily /*, fontStyle: 'italic'*/, + color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()), + paddingLeft: 4, + paddingRight: 4, + paddingTop: 3, + paddingBottom: 3, + background: DashColor(highlightColor || color) + .fade(0.5) + .toString(), + }}> + <EditableView + key="editableView" + oneLine + contents={labelText} + height={fontSize + 4} + fontSize={fontSize} + GetValue={() => linkDesc} + SetValue={action(val => { + this.Document[DocData].link_description = val; + return true; + })} + /> + + {/* <EditableText + placeholder={labelText} + background={color} + color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())} + type={Type.PRIM} + val={StrCast(this.Document[DocData].link_description)} + setVal={action(val => (this.Document[DocData].link_description = val))} + fillWidth + /> */} + </div> + } + passProps={{}} + /> + </> ); } - return null; return ( <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox - {...this._props} // + {...this.props} // fieldKey="link_anchor" setHeight={emptyFunction} dontRegisterView={true} diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index a342285b0..a141ef041 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model'; import { Doc } from '../../../../fields/Doc'; +import { Utils } from '../../../../Utils'; const emDOM: DOMOutputSpec = ['em', 0]; const strongDOM: DOMOutputSpec = ['strong', 0]; @@ -44,7 +45,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (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]; }, }, |
