diff options
| author | bobzel <zzzman@gmail.com> | 2024-03-26 22:34:10 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-03-26 22:34:10 -0400 |
| commit | b420caf2c7ecd386cae2cc550904522474b541aa (patch) | |
| tree | bb28f771a1a14086e2d184f794fde013969a48ff /src/client/views/nodes | |
| parent | e0b846bf66b5d830b452ce0faa73eeb5f8a6cdcd (diff) | |
added empty image tool and click on empty image to select from filesystem. fixed following links in lightbox and showing links to stackedTimelines. fixed embedding docs into text. fixed not resizing text boxes that also show up in pivot view. prevent context menu from going off top of screen. fixed freeform clustering colors and click to type. fixed links to stackedTimeline marks, and titles for marks. made title editing from doc deco and header use same syntax as keyValue. fixed marquee selection on webBoxes. turn off transitions in freeformdocview after timeout. enabled iconifying templates to propagate to "offspring". fixes images in templates. don't show headr on schema views.
Diffstat (limited to 'src/client/views/nodes')
| -rw-r--r-- | src/client/views/nodes/CollectionFreeFormDocumentView.tsx | 24 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 7 | ||||
| -rw-r--r-- | src/client/views/nodes/DocumentView.tsx | 35 | ||||
| -rw-r--r-- | src/client/views/nodes/FieldView.tsx | 1 | ||||
| -rw-r--r-- | src/client/views/nodes/ImageBox.tsx | 55 | ||||
| -rw-r--r-- | src/client/views/nodes/KeyValueBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/LabelBox.tsx | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/LinkBox.tsx | 47 | ||||
| -rw-r--r-- | src/client/views/nodes/VideoBox.tsx | 5 | ||||
| -rw-r--r-- | src/client/views/nodes/WebBox.tsx | 65 | ||||
| -rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 17 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 2 |
12 files changed, 189 insertions, 73 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 2800ea200..0d0a7c623 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -1,4 +1,4 @@ -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { OmitKeys, numberRange } from '../../../Utils'; @@ -17,6 +17,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldViewProps } from './FieldView'; +import { TransitionTimer } from '../../../fields/DocSymbols'; /// Ugh, typescript has no run-time way of iterating through the keys of an interface. so we need /// manaully keep this list of keys in synch wih the fields of the freeFormProps interface @@ -91,6 +92,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF @observable AutoDim = this.props.autoDim; @observable Transition = this.props.transition; + componentDidMount(): void { + if (this.props.transition && !this.Document[TransitionTimer]) { + const num = Number(this.props.transition.match(/([0-9.]+)s/)?.[1]) * 1000 || Number(this.props.transition.match(/([0-9.]+)ms/)?.[1]); + this.Document[TransitionTimer] = setTimeout( + action(() => (this.Document[TransitionTimer] = this.Transition = undefined)), + num + ); + } + } + componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) { super.componentDidUpdate(prevProps); this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower]))); @@ -98,14 +109,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes - DataTransition = () => this._props.transition; // prettier-ignore + DataTransition = () => this.Transition || StrCast(this.Document.dataTransition); // prettier-ignore RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (doc === this.layoutDoc) { - switch (property) { + switch (property.split(':')[0]) { case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.BackgroundColor; case StyleProp.Color: return this.Color; @@ -224,7 +235,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF // undefined - this is not activated by a group isGroupActive = () => { if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; - const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); + const backColor = this.BackgroundColor; + const isGroup = this.dataDoc.isGroup && (!backColor || backColor === 'transparent'); return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined; }; render() { @@ -237,7 +249,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this.PanelWidth(), height: this.PanelHeight(), transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`, - transition: this.Transition || StrCast(this.Document.dataTransition), + transition: this.DataTransition(), zIndex: this.ZIndex, display: this.Width ? undefined : 'none', }}> @@ -251,6 +263,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} /> )} </div> diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 5a326ecb0..e729e2fa2 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -7,7 +7,7 @@ import { OmitKeys, Without, emptyPath } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; -import { Cast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; @@ -128,7 +128,9 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte if (this._props.LayoutTemplateString) return this._props.LayoutTemplateString; if (!this.layoutDoc) return '<p>awaiting layout</p>'; if (this._props.layoutFieldKey === 'layout_keyValue') return StrCast(this._props.Document.layout_keyValue, KeyValueBox.LayoutString()); - const layout = Cast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); + const tempLayout = DocCast(this.layoutDoc[this.layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')]); + const layoutDoc = tempLayout ?? this.layoutDoc; + const layout = Cast(layoutDoc[layoutDoc === this._props.Document && this._props.layoutFieldKey ? this._props.layoutFieldKey : StrCast(layoutDoc.layout_fieldKey, 'layout')], 'string'); if (layout === undefined) return this._props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); if (typeof layout === 'string') return layout; return '<p>Loading layout</p>'; @@ -154,6 +156,7 @@ export class DocumentContentsView extends ObservableReactComponent<DocumentConte 'LayoutTemplate', 'layoutFieldKey', 'dontCenter', + 'DataTransition', 'contextMenuItems', //'onClick', // don't need to omit this since it will be set 'onDoubleClickScript', diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1044d6609..3154ee434 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 { Howl } from 'howler'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal'; @@ -104,12 +104,11 @@ export interface DocumentViewProps extends FieldViewSharedProps { childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. dragWhenActive?: boolean; dontHideOnDrag?: boolean; - suppressSetHeight?: boolean; onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected DataTransition?: () => string | undefined; NativeWidth?: () => number; NativeHeight?: () => number; - contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; @@ -560,7 +559,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document StrListCast(this.Document.contextMenuLabels).forEach((label, i) => cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.Document, scriptContext: this._props.scriptContext }), icon: 'sticky-note' }) ); - this._props.contextMenuItems?.().forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.Document, scriptContext: this._props.scriptContext }), icon: item.icon as IconProp })); + this._props + .contextMenuItems?.() + .forEach( + item => + item.label && + cm.addItem({ description: item.label, event: () => (item.method ? item.method() : item.script?.script.run({ this: this.Document, documentView: this, scriptContext: this._props.scriptContext })), icon: item.icon as IconProp }) + ); if (!this.Document.isFolder) { const templateDoc = Cast(this.Document[StrCast(this.Document.layout_fieldKey)], Doc, null); @@ -597,7 +602,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (!this.Document.annotationOn) { onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); @@ -810,8 +815,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; /** * 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] + * setting layout_showTitle using the format: field1[:hover] **/ @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; @@ -851,21 +855,26 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document contents={ showTitle .split(';') - .map(field => Field.toString(targetDoc[field.trim()] as Field)) + .map(field => Field.toJavascriptString(this.Document[field] as Field)) .join(' \\ ') || '-unset-' } display="block" oneLine={true} fontSize={(this.titleHeight / 15) * 10} - GetValue={() => (showTitle.split(';').length !== 1 ? '#' + showTitle : Field.toKeyValueString(this.Document, showTitle.split(';')[0]))} + GetValue={() => + showTitle + .split(';') + .map(field => Field.toKeyValueString(this.Document, field)) + .join('\\') + } SetValue={undoBatch((input: string) => { - if (input?.startsWith('#')) { + if (input?.startsWith('$')) { if (this.layoutDoc.layout_showTitle) { - this.layoutDoc._layout_showTitle = input?.substring(1); + this.layoutDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; } else if (!this._props.layout_showTitle) { - Doc.UserDoc().layout_showTitle = input?.substring(1) ?? 'author_date'; + Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'title'; } - } else if (showTitle && !showTitle.includes('Date') && showTitle !== 'author') { + } else if (showTitle && !showTitle.includes(';') && !showTitle.includes('Date') && showTitle !== 'author') { KeyValueBox.SetField(targetDoc, showTitle, input); } return true; diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 5b47dd91d..771856788 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -92,6 +92,7 @@ export interface FieldViewSharedProps { waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; + suppressSetHeight?: boolean; } /** diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 251235b93..86e8ed60a 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -9,7 +9,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ObjectField } from '../../../fields/ObjectField'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils'; @@ -29,6 +29,12 @@ import { OpenWhere } from './DocumentView'; import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView'; import './ImageBox.scss'; import { PinProps, PresBox } from './trails'; +import { Colors } from 'browndash-components'; +import { listSpec } from '../../../fields/Schema'; +import { List } from '../../../fields/List'; +import { url } from 'inspector'; +import { OverlayView } from '../OverlayView'; +import { Networking } from '../../Network'; @observer export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -134,7 +140,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop); }, true); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; @@ -259,7 +265,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -297,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this._props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || this.dataDoc[this.fieldKey + '_alternates'] ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -311,13 +317,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get paths() { const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc - const alts = DocListCast(this.dataDoc[this.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images - const altpaths = alts - .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url) - .filter(url => url) - .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents + const alts = this.dataDoc[this.fieldKey + '_alternates'] as any as List<Doc>; // retrieve alternate documents that may be rendered as alternate images + const defaultUrl = new URL(Utils.prepend('/assets/unknown-file-icon-hi.png')); + const altpaths = + alts + ?.map(doc => (doc instanceof Doc ? ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl : defaultUrl)) + .filter(url => url) + .map(url => this.choosePath(url)) ?? []; // acc ess the primary layout data of the alternate documents const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths; - return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')]; + return paths.length ? paths : [defaultUrl.href]; } @observable _error = ''; @@ -326,7 +334,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl @computed get content() { TraceMobx(); - const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)); + const backColor = DashColor(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) ?? Colors.WHITE); const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1; const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0]; const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement(); @@ -370,6 +378,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { + if (!this.dataDoc[this.fieldKey]) return this.chooseImage(); if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, @@ -468,4 +477,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl </div> ); } + + public chooseImage = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.multiple = true; + input.accept = 'image/*'; + input.onchange = async _e => { + const file = input.files?.[0]; + if (file) { + const disposer = OverlayView.ShowSpinner(); + const [{ result }] = await Networking.UploadFilesToServer({ file }); + if (result instanceof Error) { + alert('Error uploading files - possibly due to unsupported file types'); + } else { + this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); + !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); + } + disposer(); + } else { + console.log('No file selected'); + } + }; + input.click(); + }; } diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 5394c7dbb..78e4435ce 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -113,7 +113,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> { if (key) target[key] = script.originalScript; return false; } - field === undefined && (field = res.result); + field === undefined && (field = res.result instanceof Array ? new List<any>(res.result) : res.result); } } if (!key) return false; diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index be20b5934..74e78c671 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -41,7 +41,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } @computed get Title() { - return Field.toString(this.dataDoc[this.fieldKey] as Field); + return Field.toString(this.dataDoc[this.fieldKey] as Field) || StrCast(this.Document.title); } protected createDropTarget = (ele: HTMLDivElement) => { diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 6e4d0e92a..0809e2ad6 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -2,12 +2,13 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import Xarrow from 'react-xarrows'; +import { FieldResult } from '../../../fields/Doc'; import { DocCss, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { TraceMobx } from '../../../fields/util'; 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'; @@ -22,8 +23,8 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } - disposer: IReactionDisposer | undefined; - @observable _forceAnimate = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor + _disposer: IReactionDisposer | undefined; + @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor constructor(props: FieldViewProps) { @@ -39,11 +40,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; componentWillUnmount() { - this.disposer?.(); + this._disposer?.(); } componentDidMount() { this._props.setContentViewBox?.(this); - this.disposer = reaction( + this._disposer = reaction( () => ({ drag: SnappingManager.IsDragging }), ({ drag }) => { !LightboxView.Contains(this.DocumentView?.()) && @@ -64,12 +65,13 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { } }) ); - }, - { fireImmediately: true } + } ); } render() { + TraceMobx(); + if (this._hide) return null; const a = this.anchor1; const b = this.anchor2; @@ -92,18 +94,34 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { 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; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) + var foundParent = false; + const getAnchor = (field: FieldResult): Element[] => { + const doc = DocCast(field); + const ele = document.getElementById(doc[Id]); + if (ele?.getBoundingClientRect().width) return [ele]; + const eles = Array.from(document.getElementsByClassName(doc[Id])).filter(ele => ele?.getBoundingClientRect().width); + const annoOn = DocCast(doc.annotationOn); + if (eles.length || !annoOn) return eles; + const pareles = getAnchor(annoOn); + foundParent = pareles.length ? true : false; + return pareles; + }; // 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(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement(); - const targetBhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement(); + const targetAhyperlinks = getAnchor(this.dataDoc.link_anchor_1); + const targetBhyperlinks = getAnchor(this.dataDoc.link_anchor_2); - const aid = targetAhyperlink?.id || a.Document[Id]; - const bid = targetBhyperlink?.id || b.Document[Id]; - if (!document.getElementById(aid) || !document.getElementById(bid)) { + const container = this.DocumentView?.().containerViewPath?.().lastElement().ContentDiv; + const aid = targetAhyperlinks?.find(alink => container?.contains(alink))?.id ?? targetAhyperlinks?.lastElement()?.id; + const bid = targetBhyperlinks?.find(blink => container?.contains(blink))?.id ?? targetBhyperlinks?.lastElement()?.id; + if (!aid || !bid) { setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); return null; } + if (foundParent) { + setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 1))); + } 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); @@ -192,6 +210,11 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { </> ); } + + setTimeout( + action(() => (this._forceAnimate = this._forceAnimate + 1)), + 2 + ); 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/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index b2ae7201c..0af0751bf 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -335,7 +335,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl Doc.SetNativeHeight(imageSnapshot[DocData], Doc.NativeHeight(this.layoutDoc)); this._props.addDocument?.(imageSnapshot); const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); - link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); + // link && (DocCast(link.link_anchor_2)[DocData].timecodeToHide = NumCast(DocCast(link.link_anchor_2).timecodeToShow) + 3); // do we need to set an end time? should default to +0.1 setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, dropActionType.move, true)); }; @@ -373,7 +373,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() impl getView = (doc: Doc, options: FocusViewOptions) => { if (this._stackedTimeline?.makeDocUnfiltered(doc)) { if (this.heightPercent === 100) { - this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; + // do we want to always open up the timeline when followin a link? kind of clunky visually + //this.layoutDoc._layout_timelineHeightPercent = VideoBox.heightPercent; options.didMove = true; } return this._stackedTimeline.getView(doc, options); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index c9340edc0..f4d5eef05 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { htmlToText } from 'html-to-text'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as WebRequest from 'web-request'; @@ -341,7 +341,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem savedAnnotationsCreator: () => ObservableMap<number, HTMLDivElement[]> = () => this._textAnnotationCreator?.() || this._savedAnnotations; @action + iframeMove = (e: PointerEvent) => { + const theclick = this.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); + this._marqueeref.current?.onMove(theclick); + }; + @action iframeUp = (e: PointerEvent) => { + this._iframe?.contentDocument?.removeEventListener('pointermove', this.iframeMove); + this.marqueeing = undefined; this._getAnchor = AnchorMenu.Instance?.GetAnchor; // need to save AnchorMenu's getAnchor since a subsequent selection on another doc will overwrite this value this._textAnnotationCreator = undefined; this.DocumentView?.()?.cleanupPointerEvents(); // pointerup events aren't generated on containing document view, so we have to invoke it here. @@ -358,6 +368,29 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } + } else { + const theclick = this.props + .ScreenToLocalTransform() + .inverse() + .transformPoint(e.clientX, e.clientY - NumCast(this.layoutDoc.layout_scrollTop)); + if (!this._marqueeref.current?.isEmpty) this._marqueeref.current?.onEnd(theclick[0], theclick[1]); + else { + if (!(e.target as any)?.tagName?.includes('INPUT')) this.finishMarquee(theclick[0], theclick[1]); + this._getAnchor = AnchorMenu.Instance?.GetAnchor; + this.marqueeing = undefined; + } + + ContextMenu.Instance.closeMenu(); + ContextMenu.Instance.setIgnoreEvents(false); + if (e?.button === 2 || e?.altKey) { + e?.preventDefault(); + e?.stopPropagation(); + setTimeout(() => { + // if menu comes up right away, the down event can still be active causing a menu item to be selected + this.specificContextMenu(undefined as any); + this.DocumentView?.().onContextMenu(undefined, theclick[0], theclick[1]); + }); + } } }; @action @@ -400,6 +433,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem }; @action iframeDown = (e: PointerEvent) => { + this._textAnnotationCreator = undefined; + const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); + if (sel?.empty) + sel.empty(); // Chrome + else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox + this._props.select(false); const theclick = this.props .ScreenToLocalTransform() @@ -409,6 +448,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem const word = getWordAtPoint(e.target, e.clientX, e.clientY); if (!word && !(e.target as any)?.className?.includes('rangeslider') && !(e.target as any)?.onclick && !(e.target as any)?.parentNode?.onclick) { this.marqueeing = theclick; + this._marqueeref.current?.onInitiateSelection(this.marqueeing); + this._iframe?.contentDocument?.addEventListener('pointermove', this.iframeMove); e.preventDefault(); } }; @@ -739,28 +780,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem this.marqueeing = undefined; } }; - @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => { + @action finishMarquee = (x?: number, y?: number) => { this._getAnchor = AnchorMenu.Instance?.GetAnchor; this.marqueeing = undefined; - this._textAnnotationCreator = undefined; - const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty) - sel.empty(); // Chrome - else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox this._setPreviewCursor?.(x ?? 0, y ?? 0, false, !this._marqueeref.current?.isEmpty, this.Document); - if (x !== undefined && y !== undefined) { - ContextMenu.Instance.closeMenu(); - ContextMenu.Instance.setIgnoreEvents(false); - if (e?.button === 2 || e?.altKey) { - e?.preventDefault(); - e?.stopPropagation(); - setTimeout(() => { - // if menu comes up right away, the down event can still be active causing a menu item to be selected - this.specificContextMenu(undefined as any); - this.DocumentView?.().onContextMenu(undefined, x, y); - }); - } - } }; @observable lighttext = false; @@ -992,6 +1015,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined); @computed get webpage() { + TraceMobx(); 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); @@ -1071,6 +1095,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem : 'none'; annotationPointerEvents = () => (this._props.isContentActive() && (SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); render() { + TraceMobx(); 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); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 793595694..eb7293054 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -450,16 +450,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this.Document).filter( + link => + ((!Doc.isTemplateForField(this.Document) && + (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + (!Doc.isTemplateForField(DocCast(link.link_anchor_2)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && + link.link_relationship === LinkManager.AutoKeywords + ); // prettier-ignore if (this._editorView?.state.doc.textContent) { - const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; + const t = this._editorView.state.selection.to; var tr = this._editorView.state.tr as any; const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; tr = tr.removeMark(0, tr.doc.content.size, autoAnch); Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); @@ -624,7 +631,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB docId: draggedDoc[Id], float: 'unset', }); - if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { + if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { added = dragData.removeDocument?.(draggedDoc) ? true : false; } else { added = true; @@ -1814,7 +1821,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); const scrollHeight = this.ProseRef && proseHeight; - if (this._props.setHeight && scrollHeight && !this._props.dontRegisterView) { + if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index e34144fae..ae6da8fb0 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -703,7 +703,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (pinProps.pinData.temporal) { pinDoc.config_clipStart = targetDoc._layout_currentTimecode; const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1); - pinDoc.config_clipEnd = NumCast(targetDoc.clipEnd, duration); + pinDoc.config_clipEnd = NumCast(pinDoc.config_clipStart) + NumCast(targetDoc.clipEnd, duration); } } if (pinProps?.pinViewport) { |
