From 55bac585fa0b8d6c3f513ccecb22456d1d361040 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 3 Aug 2022 13:33:38 -0400
Subject: fixes for dragging notes so that they highlight properly and go to
 the right place when embedded in freeform views.

---
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b8ee89ef2..add83f4e0 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1001,7 +1001,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
             ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
                 const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
-                autoHeight && newHeight && this.props.setHeight?.(newHeight);
+                if (autoHeight && newHeight && newHeight !== this.rootDoc.height) {
+                    this.props.setHeight?.(newHeight);
+                }
             },
             { fireImmediately: true }
         );
@@ -1695,7 +1697,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             if (this.props.setHeight && scrollHeight && this.props.renderDepth && !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.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight);
-                if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) {
+                if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
                     setScrollHeight();
                 } else {
                     setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template.  So set a timeout and hope its arrived...
-- 
cgit v1.2.3-70-g09d2


From 26670c8b9eb6e2fd981c3a0997bff5556b60504b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Aug 2022 09:34:51 -0400
Subject: made props.showTitle = '' override doc.showTitle.  fixed
 de-autoLinking when text box is empty

---
 src/client/views/StyleProvider.tsx                        | 1 +
 src/client/views/nodes/LinkDocPreview.tsx                 | 3 ++-
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 6 +++---
 3 files changed, 6 insertions(+), 4 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index a83163eb0..3bd4f5152 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -124,6 +124,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return (
                 (doc &&
                     !doc.presentationTargetDoc &&
+                    props?.showTitle?.() !== '' &&
                     StrCast(
                         doc._showTitle,
                         props?.showTitle?.() ||
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index a12564839..85b1f8d9e 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -5,7 +5,7 @@ import { observer } from 'mobx-react';
 import wiki from 'wikijs';
 import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -236,6 +236,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                             isDocumentActive={returnFalse}
                             isContentActive={returnFalse}
                             addDocument={returnFalse}
+                            showTitle={returnEmptyString}
                             removeDocument={returnFalse}
                             addDocTab={returnFalse}
                             pinToPres={returnFalse}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index add83f4e0..a018f51c2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -382,9 +382,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     autoLink = () => {
+        const newAutoLinks = new Set<Doc>();
+        const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
         if (this._editorView?.state.doc.textContent) {
-            const newAutoLinks = new Set<Doc>();
-            const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
             const f = this._editorView.state.selection.from;
             const t = this._editorView.state.selection.to;
             var tr = this._editorView.state.tr as any;
@@ -393,8 +393,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
             tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
             this._editorView?.dispatch(tr);
-            oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
         }
+        oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
     };
 
     updateTitle = () => {
-- 
cgit v1.2.3-70-g09d2


From 2a1d6793d33d9c72295d23965aefd23f2004bbcb Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Aug 2022 11:48:27 -0400
Subject: cleaning up link ui.   linkPreviews (not from linkMenu) have an edit
 button.  link lines work when in lightboxview.

---
 .../CollectionFreeFormLinkView.tsx                 |   5 +-
 .../CollectionFreeFormLinksView.tsx                |   5 +-
 src/client/views/linking/LinkEditor.scss           |  65 +++++-----
 src/client/views/linking/LinkEditor.tsx            | 141 ++++++++++++---------
 src/client/views/linking/LinkMenuItem.tsx          |  89 +------------
 src/client/views/nodes/DocumentLinksButton.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  16 ++-
 src/client/views/nodes/LinkAnchorBox.tsx           |   2 +-
 src/client/views/nodes/LinkDocPreview.tsx          |  18 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 10 files changed, 149 insertions(+), 198 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index d979ef961..bf9de6760 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -195,7 +195,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
         const aleft = this.visibleX(adiv);
         const bleft = this.visibleX(bdiv);
-        const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
+        const aclipped = aleft !== a.left || atop !== a.top;
+        const bclipped = bleft !== b.left || btop !== b.top;
+        if (aclipped && bclipped) return undefined;
+        const clipped = aclipped || bclipped;
         const pt1 = [aleft + a.width / 2, atop + a.height / 2];
         const pt2 = [bleft + b.width / 2, btop + b.width / 2];
         const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]];
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 8720c9097..b8344dc0c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -5,11 +5,14 @@ import { DocumentManager } from '../../../util/DocumentManager';
 import './CollectionFreeFormLinksView.scss';
 import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView';
 import React = require('react');
+import { LightboxView } from '../../LightboxView';
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> {
     @computed get uniqueConnections() {
-        return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)).map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />);
+        return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews))
+            .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath)))
+            .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />);
     }
 
     render() {
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index 1d6496d3c..b0ee4e46d 100644
--- a/src/client/views/linking/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
@@ -1,10 +1,11 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
 
 .linkEditor {
     width: 100%;
     height: auto;
     font-size: 13px; // TODO
     user-select: none;
+    max-width: 280px;
 }
 
 .linkEditor-button-back {
@@ -20,19 +21,20 @@
 }
 
 .linkEditor-info {
-    //border-bottom: 0.5px solid $light-gray;
-    //padding-bottom: 1px;
-    padding: 12px;
+    padding-top: 12px;
     padding-left: 5px;
+    padding-bottom: 3px;
     //margin-bottom: 6px;
     display: flex;
     justify-content: space-between;
     color: black;
 
     .linkEditor-linkedTo {
-        width: calc(100% - 26px);
-        padding-left: 5px;
-        padding-right: 5px;
+        width: calc(100% - 46px);
+        overflow: hidden;
+        position: relative;
+        text-overflow: ellipsis;
+        white-space: pre;
 
         .linkEditor-downArrow {
             &:hover {
@@ -77,11 +79,14 @@
         width: 20px;
     }
 }
+.linkEditor-deleteBtn {
+    padding-left: 3px;
+}
 
 .linkEditor-description {
     padding-left: 26px;
-    padding-right: 6.5px;
     padding-bottom: 3.5px;
+    display: flex;
 
     .linkEditor-description-label {
         text-decoration-color: black;
@@ -96,7 +101,6 @@
             //border: 1px solid grey;
             //border-radius: 4px;
             padding-left: 2px;
-            padding-right: 2px;
             //margin-right: 4px;
             color: black;
             text-decoration-color: grey;
@@ -104,17 +108,13 @@
 
         .linkEditor-description-add-button {
             display: inline;
-            /* float: right; */
             border-radius: 7px;
             font-size: 9px;
             background: black;
-            /* padding: 3px; */
-            padding-top: 4px;
-            padding-left: 7px;
-            padding-bottom: 4px;
-            padding-right: 8px;
             height: 80%;
             color: white;
+            padding: 3px;
+            margin-left: 3px;
 
             &:hover {
                 cursor: pointer;
@@ -146,6 +146,7 @@
     padding-left: 26px;
     padding-right: 6.5px;
     padding-bottom: 15px;
+    display: flex;
 
     &:hover {
         cursor: pointer;
@@ -153,12 +154,11 @@
 
     .linkEditor-followingDropdown-label {
         color: black;
+        padding-right: 3px;
     }
 
     .linkEditor-followingDropdown-dropdown {
-
         .linkEditor-followingDropdown-header {
-
             border: 1px solid grey;
             border-radius: 4px;
             //background-color: rgb(236, 236, 236);
@@ -195,27 +195,25 @@
                     background-color: rgb(187, 220, 231);
                 }
             }
-
         }
     }
-
-
 }
 
-
-
-
-
-
 .linkEditor-button,
 .linkEditor-addbutton {
-    width: 18px;
-    height: 18px;
-    padding: 0;
-    // font-size: 12px;
-    border-radius: 10px;
-
-
+    width: 15%;
+    border-radius: 7px;
+    font-size: 9px;
+    background: black;
+    padding: 3px;
+    height: 80%;
+    color: white;
+    text-align: center;
+    margin: auto;
+    margin-left: 3px;
+    > svg {
+        margin: auto;
+    }
     &:disabled {
         background-color: gray;
     }
@@ -270,7 +268,6 @@
     }
 }
 
-
 .linkEditor-dropdown {
     width: 100%;
     position: relative;
@@ -334,4 +331,4 @@
     .linkEditor-button {
         margin-left: 3px;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index ba301962b..d23d38854 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -9,11 +9,12 @@ import { undoBatch } from '../../util/UndoManager';
 import './LinkEditor.scss';
 import { LinkRelationshipSearch } from './LinkRelationshipSearch';
 import React = require('react');
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
 
 interface LinkEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    showLinks: () => void;
+    showLinks?: () => void;
     hideback?: boolean;
 }
 @observer
@@ -22,13 +23,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
     @observable zoomFollow = StrCast(this.props.sourceDoc.followLinkZoom);
     @observable openDropdown: boolean = false;
-    @observable showInfo: boolean = false;
-    @computed get infoIcon() {
-        if (this.showInfo) {
-            return 'chevron-up';
-        }
-        return 'chevron-down';
-    }
     @observable private buttonColor: string = '';
     @observable private relationshipButtonColor: string = '';
     @observable private relationshipSearchVisibility: string = 'none';
@@ -36,12 +30,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
 
     //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
 
-    @undoBatch
-    deleteLink = (): void => {
-        LinkManager.Instance.deleteLink(this.props.linkDoc);
-        this.props.showLinks();
-    };
-
     @undoBatch
     setRelationshipValue = action((value: string) => {
         if (LinkManager.currentLink) {
@@ -173,7 +161,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
         return (
             <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Link Relationship:</div>
+                <div className="linkEditor-description-label">Relationship:</div>
                 <div className="linkEditor-description-input">
                     <div className="linkEditor-description-editing">
                         <input
@@ -214,7 +202,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     get editDescription() {
         return (
             <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Link Description:</div>
+                <div className="linkEditor-description-label">Description:</div>
                 <div className="linkEditor-description-input">
                     <div className="linkEditor-description-editing">
                         <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input>
@@ -284,68 +272,99 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         );
     }
 
-    @action
-    changeInfo = () => {
-        this.showInfo = !this.showInfo;
+    autoMove = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
+    };
+
+    showAnchor = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
+    };
+
+    showLink = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
+    };
+
+    deleteLink = (e: React.PointerEvent): void => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
     };
 
     render() {
         const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
         return !destination ? null : (
-            <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()}>
+            <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
                 <div className="linkEditor-info">
-                    <Tooltip
-                        title={
-                            <>
-                                <div className="dash-tooltip">Return to link menu</div>
-                            </>
-                        }
-                        placement="top">
-                        <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onClick={this.props.showLinks}>
-                            <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
-                        </button>
-                    </Tooltip>
+                    {!this.props.showLinks ? null : (
+                        <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top">
+                            <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}>
+                                <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
+                            </button>
+                        </Tooltip>
+                    )}
                     <p className="linkEditor-linkedTo">
                         Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b>
                     </p>
-                    <Tooltip
-                        title={
-                            <>
-                                <div className="dash-tooltip">Show more link information</div>
-                            </>
-                        }
-                        placement="top">
-                        <div className="linkEditor-downArrow">
-                            <FontAwesomeIcon className="button" icon={this.infoIcon} size="lg" onPointerDown={this.changeInfo} />
+                    <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
+                        <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+                            <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
                         </div>
                     </Tooltip>
                 </div>
-                {this.showInfo ? (
-                    <div className="linkEditor-moreInfo">
-                        <div>
-                            {this.props.linkDoc.author ? (
-                                <div>
-                                    {' '}
-                                    <b>Author:</b> {StrCast(this.props.linkDoc.author)}
-                                </div>
-                            ) : null}
-                        </div>
-                        <div>
-                            {this.props.linkDoc.creationDate ? (
-                                <div>
-                                    {' '}
-                                    <b>Creation Date:</b>
-                                    {DateCast(this.props.linkDoc.creationDate).toString()}
-                                </div>
-                            ) : null}
-                        </div>
-                    </div>
-                ) : null}
+                <div className="linkEditor-moreInfo">
+                    {this.props.linkDoc.author ? (
+                        <>
+                            {' '}
+                            <b>Author:</b> {StrCast(this.props.linkDoc.author)}
+                        </>
+                    ) : null}
+                    {this.props.linkDoc.creationDate ? (
+                        <>
+                            {' '}
+                            <b>Creation Date:</b>
+                            {DateCast(this.props.linkDoc.creationDate).toString()}
+                        </>
+                    ) : null}
+                </div>
 
                 {this.editDescription}
                 {this.editRelationship}
                 {this.editZoomFollow}
+                <div className="linkEditor-description">
+                    Show Anchor:
+                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}>
+                        <div
+                            className="linkEditor-button"
+                            style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue                                       */ }}
+                            onPointerDown={this.showAnchor}
+                            onClick={e => e.stopPropagation()}>
+                            <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
+                        </div>
+                    </Tooltip>
+                </div>
+                <div className="linkEditor-description">
+                    Show Link Line:
+                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}>
+                        <div
+                            className="linkEditor-button"
+                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
+                            onPointerDown={this.showLink}
+                            onClick={e => e.stopPropagation()}>
+                            <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
+                        </div>
+                    </Tooltip>
+                </div>
+                <div className="linkEditor-description">
+                    Freeze Anchor:
+                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}>
+                        <div
+                            className="linkEditor-button"
+                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
+                            onPointerDown={this.autoMove}
+                            onClick={e => e.stopPropagation()}>
+                            <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
+                        </div>
+                    </Tooltip>
+                </div>
                 {this.followingDropdown}
             </div>
         );
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index ed856a4ab..60428fc98 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -125,33 +125,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         );
     };
 
-    deleteLink = (e: React.PointerEvent): void => {
-        setupMoveUpEvents(
-            this,
-            e,
-            returnFalse,
-            emptyFunction,
-            undoBatch(
-                action(() => {
-                    this.props.linkDoc.linksToAnnotation && Hypothesis.deleteLink(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
-                    LinkManager.Instance.deleteLink(this.props.linkDoc);
-                })
-            )
-        );
-    };
-
-    autoMove = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
-    };
-
-    showLink = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
-    };
-
-    showAnchor = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
-    };
-
     render() {
         const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp;
 
@@ -183,7 +156,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                                 linkSrc: this.props.sourceDoc,
                                 linkDoc: this.props.linkDoc,
                                 showHeader: false,
-                                location: [e.clientX, e.clientY + 20],
+                                location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
                             })
                         }
                         onPointerDown={this.onLinkButtonDown}>
@@ -206,69 +179,11 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                         </div>
 
                         <div className="linkMenu-item-buttons" ref={this._buttonRef}>
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>
-                                    </>
-                                }>
-                                <div className="button" ref={this._editRef} style={{ background: this.props.linkDoc.hidden ? '' : '#4476f7' /* $medium-blue */ }} onPointerDown={this.showAnchor} onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
-                                </div>
-                            </Tooltip>
-
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>
-                                    </>
-                                }>
-                                <div
-                                    className="button"
-                                    ref={this._editRef}
-                                    style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
-                                    onPointerDown={this.showLink}
-                                    onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
-                                </div>
-                            </Tooltip>
-
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>
-                                    </>
-                                }>
-                                <div
-                                    className="button"
-                                    ref={this._editRef}
-                                    style={{ background: this.props.linkDoc.hidden ? 'gray' : !this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
-                                    onPointerDown={this.autoMove}
-                                    onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
-                                </div>
-                            </Tooltip>
-
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">Edit Link</div>
-                                    </>
-                                }>
+                            <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
                                 <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
                                     <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
                                 </div>
                             </Tooltip>
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">Delete Link</div>
-                                    </>
-                                }>
-                                <div className="button" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
-                                </div>
-                            </Tooltip>
                         </div>
                     </div>
                 </div>
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 5939d1680..a37de7f69 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -307,7 +307,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             <div
                 className="documentLinksButton-wrapper"
                 style={{
-                    transform: this.props.InMenu ? undefined : `scale(${this.props.scaling})`,
+                    transform: `scale(${this.props.scaling?.() || 1})`,
                 }}>
                 {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? (
                     <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1ee1aec5a..2524d66dd 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1070,7 +1070,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     <DocumentLinksButton
                         View={this.props.DocumentView()}
                         scaling={this.linkButtonInverseScaling}
-                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -30, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -30]}
+                        Offset={[this.topMost ? 0 : !this.props.isSelected() ? -15 : -36, undefined, undefined, this.topMost ? 10 : !this.props.isSelected() ? -15 : -28]}
                     />
                 )}
                 {audioView}
@@ -1298,6 +1298,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     isHovering = () => this._isHovering;
     @observable _isHovering = false;
     @observable _: string = '';
+    _hoverTimeout: any = undefined;
     @computed get renderDoc() {
         TraceMobx();
         const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
@@ -1308,8 +1309,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 <div
                     className={`documentView-node${this.topMost ? '-topmost' : ''}`}
                     id={this.props.Document[Id]}
-                    onPointerEnter={action(() => (this._isHovering = true))}
-                    onPointerLeave={action(() => (this._isHovering = false))}
+                    onPointerEnter={action(() => {
+                        clearTimeout(this._hoverTimeout);
+                        this._isHovering = true;
+                    })}
+                    onPointerLeave={action(() => {
+                        clearTimeout(this._hoverTimeout);
+                        this._hoverTimeout = setTimeout(
+                            action(() => (this._isHovering = false)),
+                            500
+                        );
+                    })}
                     style={{
                         background: isButton || thumb ? undefined : this.backgroundColor,
                         opacity: this.opacity,
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 85a8622ec..5102eae51 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -136,7 +136,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return (
             <div
                 className={`linkAnchorBox-cont${small ? '-small' : ''}`}
-                onPointerLeave={LinkDocPreview.Clear}
+                //onPointerLeave={} //LinkDocPreview.Clear}
                 onPointerEnter={e =>
                     LinkDocPreview.SetLinkInfo({
                         docProps: this.props,
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 85b1f8d9e..93ca22d5d 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -17,6 +17,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { DocumentView, DocumentViewSharedProps } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
+import { LinkEditor } from '../linking/LinkEditor';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -118,13 +119,15 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         }
         return undefined;
     }
-    deleteLink = (e: React.PointerEvent) => {
+    @observable _showEditor = false;
+    editLink = (e: React.PointerEvent): void => {
+        LinkManager.currentLink = this.props.linkDoc;
         setupMoveUpEvents(
             this,
             e,
             returnFalse,
             emptyFunction,
-            undoBatch(() => this._linkDoc && LinkManager.Instance.deleteLink(this._linkDoc))
+            action(() => (this._showEditor = !this._showEditor))
         );
     };
     nextHref = (e: React.PointerEvent) => {
@@ -183,9 +186,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                         </div>
                     </Tooltip>
 
-                    <Tooltip title={<div className="dash-tooltip">Delete Link</div>} placement="top">
-                        <div className="linkDocPreview-button" onPointerDown={this.deleteLink}>
-                            <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="trash" color="white" size="sm" />
+                    <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
+                        <div className="linkDocPreview-button" onPointerDown={this.editLink}>
+                            <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
                         </div>
                     </Tooltip>
                 </div>
@@ -271,8 +274,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 className="linkDocPreview"
                 ref={this._linkDocRef}
                 onPointerDown={this.followLinkPointerDown}
-                style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
-                {this.docPreview}
+                style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+                {this._showEditor ? null : this.docPreview}
+                {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />}
             </div>
         );
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a018f51c2..40b0d285c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -24,7 +24,7 @@ import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction,
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DictationManager } from '../../../util/DictationManager';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
@@ -1751,7 +1751,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
     @computed get sidebarCollection() {
         const renderComponent = (tag: string) => {
-            const ComponentTag = tag === 'freeform' ? CollectionFreeFormView : tag === 'tree' ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
+            const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
-- 
cgit v1.2.3-70-g09d2


From 4b2f131d91e71f4514642b5846713e9c3b68210b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Aug 2022 12:54:06 -0400
Subject: fixed autolinking to not match partial word substrings.  restored
 textbox dictation.

---
 src/client/documents/Documents.ts                  |   5 +-
 src/client/util/DictationManager.ts                | 439 +++++++++++----------
 src/client/views/DocumentDecorations.tsx           |   1 -
 src/client/views/linking/LinkMenuItem.tsx          |   4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  96 +++--
 5 files changed, 282 insertions(+), 263 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9d00664ad..c7ea04839 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1287,10 +1287,11 @@ export namespace DocUtils {
 
     export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = [];
 
-    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) {
+    export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
         broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
         return DocUtils.ActiveRecordings.map(audio => {
-            const link = DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
+            const sourceDoc = getSourceDoc();
+            const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
             link && (link.followLinkLocation = 'add:right');
             return link;
         });
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index a6dcda4bc..13f036838 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -1,37 +1,35 @@
-import * as interpreter from "words-to-numbers";
+import * as interpreter from 'words-to-numbers';
 // @ts-ignore  bcz: how are you supposed to include these definitions since dom-speech-recognition isn't a module?
-import type { } from "@types/dom-speech-recognition";
-import { Doc, Opt } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { RichTextField } from "../../fields/RichTextField";
-import { listSpec } from "../../fields/Schema";
-import { Cast, CastCtor } from "../../fields/Types";
-import { AudioField, ImageField } from "../../fields/URLField";
-import { Utils } from "../../Utils";
-import { Docs } from "../documents/Documents";
-import { DocumentType } from "../documents/DocumentTypes";
-import { DictationOverlay } from "../views/DictationOverlay";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { SelectionManager } from "./SelectionManager";
-import { UndoManager } from "./UndoManager";
-
+import type {} from '@types/dom-speech-recognition';
+import { Doc, Opt } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { RichTextField } from '../../fields/RichTextField';
+import { listSpec } from '../../fields/Schema';
+import { Cast, CastCtor } from '../../fields/Types';
+import { AudioField, ImageField } from '../../fields/URLField';
+import { Utils } from '../../Utils';
+import { Docs } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DictationOverlay } from '../views/DictationOverlay';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { SelectionManager } from './SelectionManager';
+import { UndoManager } from './UndoManager';
 
 /**
  * This namespace provides a singleton instance of a manager that
  * handles the listening and text-conversion of user speech.
- * 
+ *
  * The basic manager functionality can be attained by the DictationManager.Controls namespace, which provide
  * a simple recording operation that returns the interpreted text as a string.
- * 
+ *
  * Additionally, however, the DictationManager also exposes the ability to execute voice commands within Dash.
  * It stores a default library of registered commands that can be triggered by listen()'ing for a phrase and then
  * passing the results into the execute() function.
- * 
+ *
  * In addition to compile-time default commands, you can invoke DictationManager.Commands.Register(Independent|Dependent)
  * to add new commands as classes or components are constructed.
  */
 export namespace DictationManager {
-
     /**
      * Some type maneuvering to access Webkit's built-in
      * speech recognizer.
@@ -42,27 +40,26 @@ export namespace DictationManager {
         }
     }
     const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow;
-    export const placeholder = "Listening...";
+    export const placeholder = 'Listening...';
 
     export namespace Controls {
-
-        export const Infringed = "unable to process: dictation manager still involved in previous session";
+        export const Infringed = 'unable to process: dictation manager still involved in previous session';
         const browser = (() => {
             const identifier = navigator.userAgent.toLowerCase();
-            if (identifier.indexOf("safari") >= 0) {
-                return "Safari";
+            if (identifier.indexOf('safari') >= 0) {
+                return 'Safari';
             }
-            if (identifier.indexOf("chrome") >= 0) {
-                return "Chrome";
+            if (identifier.indexOf('chrome') >= 0) {
+                return 'Chrome';
             }
-            if (identifier.indexOf("firefox") >= 0) {
-                return "Firefox";
+            if (identifier.indexOf('firefox') >= 0) {
+                return 'Firefox';
             }
-            return "Unidentified Browser";
+            return 'Unidentified Browser';
         })();
         const unsupported = `listening is not supported in ${browser}`;
-        const intraSession = ". ";
-        const interSession = " ... ";
+        const intraSession = '. ';
+        const interSession = ' ... ';
 
         export let isListening = false;
         let isManuallyStopped = false;
@@ -74,7 +71,7 @@ export namespace DictationManager {
 
         export type InterimResultHandler = (results: string) => any;
         export type ContinuityArgs = { indefinite: boolean } | false;
-        export type DelimiterArgs = { inter: string, intra: string };
+        export type DelimiterArgs = { inter: string; intra: string };
         export type ListeningUIStatus = { interim: boolean } | false;
 
         export interface ListeningOptions {
@@ -105,21 +102,21 @@ export namespace DictationManager {
             try {
                 results = await (pendingListen = listenImpl(options));
                 pendingListen = undefined;
-                // if (results) {
-                //     Utils.CopyText(results);
-                //     if (overlay) {
-                //         DictationOverlay.Instance.isListening = false;
-                //         const execute = options?.tryExecute;
-                //         DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
-                //         DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
-                //     }
-                //     options?.tryExecute && await DictationManager.Commands.execute(results);
-                // }
+                if (results) {
+                    Utils.CopyText(results);
+                    if (overlay) {
+                        DictationOverlay.Instance.isListening = false;
+                        const execute = options?.tryExecute;
+                        DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
+                        DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+                    }
+                    options?.tryExecute && (await DictationManager.Commands.execute(results));
+                }
             } catch (e: any) {
                 console.log(e);
                 if (overlay) {
                     DictationOverlay.Instance.isListening = false;
-                    DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+                    DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${'error' in e ? e.error : 'unknown error'}`;
                     DictationOverlay.Instance.dictationSuccess = false;
                 }
             } finally {
@@ -131,7 +128,7 @@ export namespace DictationManager {
 
         const listenImpl = (options?: Partial<ListeningOptions>) => {
             if (!recognizer) {
-                console.log("DictationManager:" + unsupported);
+                console.log('DictationManager:' + unsupported);
                 return unsupported;
             }
             if (isListening) {
@@ -146,16 +143,18 @@ export namespace DictationManager {
             const intra = options?.delimiters?.intra;
             const inter = options?.delimiters?.inter;
 
-            recognizer.onstart = () => console.log("initiating speech recognition session...");
+            recognizer.onstart = () => console.log('initiating speech recognition session...');
             recognizer.interimResults = handler !== undefined;
             recognizer.continuous = continuous === undefined ? false : continuous !== false;
-            recognizer.lang = language === undefined ? "en-US" : language;
+            recognizer.lang = language === undefined ? 'en-US' : language;
 
             recognizer.start();
 
             return new Promise<string>((resolve, reject) => {
-                recognizer.onerror = (e: any) => { // e is SpeechRecognitionError but where is that defined? 
-                    if (!(indefinite && e.error === "no-speech")) {
+                recognizer.onerror = (e: any) => {
+                    console.log('SPEECH error:', encodeURIComponent);
+                    // e is SpeechRecognitionError but where is that defined?
+                    if (!(indefinite && e.error === 'no-speech')) {
                         recognizer.stop();
                         resolve(e);
                         //reject(e);
@@ -163,9 +162,10 @@ export namespace DictationManager {
                 };
 
                 recognizer.onresult = (e: SpeechRecognitionEvent) => {
+                    console.log('RESULT: ', e);
                     current = synthesize(e, intra);
                     let matchedTerminator: string | undefined;
-                    if (options?.terminators && (matchedTerminator = options.terminators.find(end => current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false))) {
+                    if (options?.terminators && (matchedTerminator = options.terminators.find(end => (current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false)))) {
                         current = matchedTerminator;
                         recognizer.abort();
                         return complete();
@@ -175,6 +175,7 @@ export namespace DictationManager {
                 };
 
                 recognizer.onend = (e: Event) => {
+                    console.log('END: ', e);
                     if (!indefinite || isManuallyStopped) {
                         return complete();
                     }
@@ -187,11 +188,12 @@ export namespace DictationManager {
                 };
 
                 const complete = () => {
+                    console.log('COMPLETE:');
                     if (indefinite) {
                         current && sessionResults.push(current);
                         sessionResults.length && resolve(sessionResults.join(inter || interSession));
                     } else {
-                        resolve(current || "");
+                        resolve(current || '');
                     }
                     current = undefined;
                     sessionResults = [];
@@ -201,7 +203,6 @@ export namespace DictationManager {
                     recognizer.onerror = null;
                     recognizer.onend = null;
                 };
-
             });
         };
 
@@ -222,171 +223,173 @@ export namespace DictationManager {
             }
             return transcripts.join(delimiter || intraSession);
         };
-
     }
 
-    // export namespace Commands {
-
-    //     export const dictationFadeDuration = 2000;
-
-    //     export type IndependentAction = (target: DocumentView) => any | Promise<any>;
-    //     export type IndependentEntry = { action: IndependentAction, restrictTo?: DocumentType[] };
-
-    //     export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>;
-    //     export type DependentEntry = { expression: RegExp, action: DependentAction, restrictTo?: DocumentType[] };
-
-    //     export const RegisterIndependent = (key: string, value: IndependentEntry) => Independent.set(key, value);
-    //     export const RegisterDependent = (entry: DependentEntry) => Dependent.push(entry);
-
-    //     export const execute = async (phrase: string) => {
-    //         return UndoManager.RunInBatch(async () => {
-    //             const targets = SelectionManager.Views();
-    //             if (!targets || !targets.length) {
-    //                 return;
-    //             }
-
-    //             phrase = phrase.toLowerCase();
-    //             const entry = Independent.get(phrase);
-
-    //             if (entry) {
-    //                 let success = false;
-    //                 const restrictTo = entry.restrictTo;
-    //                 for (const target of targets) {
-    //                     if (!restrictTo || validate(target, restrictTo)) {
-    //                         await entry.action(target);
-    //                         success = true;
-    //                     }
-    //                 }
-    //                 return success;
-    //             }
-
-    //             for (const entry of Dependent) {
-    //                 const regex = entry.expression;
-    //                 const matches = regex.exec(phrase);
-    //                 regex.lastIndex = 0;
-    //                 if (matches !== null) {
-    //                     let success = false;
-    //                     const restrictTo = entry.restrictTo;
-    //                     for (const target of targets) {
-    //                         if (!restrictTo || validate(target, restrictTo)) {
-    //                             await entry.action(target, matches);
-    //                             success = true;
-    //                         }
-    //                     }
-    //                     return success;
-    //                 }
-    //             }
-
-    //             return false;
-    //         }, "Execute Command");
-    //     };
-
-    //     const ConstructorMap = new Map<DocumentType, CastCtor>([
-    //         [DocumentType.COL, listSpec(Doc)],
-    //         [DocumentType.AUDIO, AudioField],
-    //         [DocumentType.IMG, ImageField],
-    //         [DocumentType.IMPORT, listSpec(Doc)],
-    //         [DocumentType.RTF, "string"]
-    //     ]);
-
-    //     const tryCast = (view: DocumentView, type: DocumentType) => {
-    //         const ctor = ConstructorMap.get(type);
-    //         if (!ctor) {
-    //             return false;
-    //         }
-    //         return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined;
-    //     };
-
-    //     const validate = (target: DocumentView, types: DocumentType[]) => {
-    //         for (const type of types) {
-    //             if (tryCast(target, type)) {
-    //                 return true;
-    //             }
-    //         }
-    //         return false;
-    //     };
-
-    //     const interpretNumber = (number: string) => {
-    //         const initial = parseInt(number);
-    //         if (!isNaN(initial)) {
-    //             return initial;
-    //         }
-    //         const converted = interpreter.wordsToNumbers(number, { fuzzy: true });
-    //         if (converted === null) {
-    //             return NaN;
-    //         }
-    //         return typeof converted === "string" ? parseInt(converted) : converted;
-    //     };
-
-    //     const Independent = new Map<string, IndependentEntry>([
-
-    //         ["clear", {
-    //             action: (target: DocumentView) => Doc.GetProto(target.props.Document).data = new List(),
-    //             restrictTo: [DocumentType.COL]
-    //         }],
-
-    //         ["open fields", {
-    //             action: (target: DocumentView) => {
-    //                 const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
-    //                 target.props.addDocTab(kvp, "add:right");
-    //             }
-    //         }],
-
-    //         ["new outline", {
-    //             action: (target: DocumentView) => {
-    //                 const newBox = Docs.Create.TextDocument("", { _width: 400, _height: 200, title: "My Outline", _autoHeight: true });
-    //                 const proto = newBox.proto!;
-    //                 const prompt = "Press alt + r to start dictating here...";
-    //                 const head = 3;
-    //                 const anchor = head + prompt.length;
-    //                 const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
-    //                 proto.data = new RichTextField(proseMirrorState);
-    //                 proto.backgroundColor = "#eeffff";
-    //                 target.props.addDocTab(newBox, "add:right");
-    //             }
-    //         }]
-
-    //     ]);
-
-    //     const Dependent = new Array<DependentEntry>(
-
-    //         {
-    //             expression: /create (\w+) documents of type (image|nested collection)/g,
-    //             action: (target: DocumentView, matches: RegExpExecArray) => {
-    //                 const count = interpretNumber(matches[1]);
-    //                 const what = matches[2];
-    //                 const dataDoc = Doc.GetProto(target.props.Document);
-    //                 const fieldKey = "data";
-    //                 if (isNaN(count)) {
-    //                     return;
-    //                 }
-    //                 for (let i = 0; i < count; i++) {
-    //                     let created: Doc | undefined;
-    //                     switch (what) {
-    //                         case "image":
-    //                             created = Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg");
-    //                             break;
-    //                         case "nested collection":
-    //                             created = Docs.Create.FreeformDocument([], {});
-    //                             break;
-    //                     }
-    //                     created && Doc.AddDocToList(dataDoc, fieldKey, created);
-    //                 }
-    //             },
-    //             restrictTo: [DocumentType.COL]
-    //         },
-
-    //         {
-    //             expression: /view as (freeform|stacking|masonry|schema|tree)/g,
-    //             action: (target: DocumentView, matches: RegExpExecArray) => {
-    //                 const mode = matches[1];
-    //                 mode && (target.props.Document._viewType = mode);
-    //             },
-    //             restrictTo: [DocumentType.COL]
-    //         }
-
-    //     );
-
-    // }
-
-}
\ No newline at end of file
+    export namespace Commands {
+        export const dictationFadeDuration = 2000;
+
+        export type IndependentAction = (target: DocumentView) => any | Promise<any>;
+        export type IndependentEntry = { action: IndependentAction; restrictTo?: DocumentType[] };
+
+        export type DependentAction = (target: DocumentView, matches: RegExpExecArray) => any | Promise<any>;
+        export type DependentEntry = { expression: RegExp; action: DependentAction; restrictTo?: DocumentType[] };
+
+        export const RegisterIndependent = (key: string, value: IndependentEntry) => Independent.set(key, value);
+        export const RegisterDependent = (entry: DependentEntry) => Dependent.push(entry);
+
+        export const execute = async (phrase: string) => {
+            return UndoManager.RunInBatch(async () => {
+                console.log('PHRASE: ' + phrase);
+                const targets = SelectionManager.Views();
+                if (!targets || !targets.length) {
+                    return;
+                }
+
+                phrase = phrase.toLowerCase();
+                const entry = Independent.get(phrase);
+
+                if (entry) {
+                    let success = false;
+                    const restrictTo = entry.restrictTo;
+                    for (const target of targets) {
+                        if (!restrictTo || validate(target, restrictTo)) {
+                            await entry.action(target);
+                            success = true;
+                        }
+                    }
+                    return success;
+                }
+
+                for (const entry of Dependent) {
+                    const regex = entry.expression;
+                    const matches = regex.exec(phrase);
+                    regex.lastIndex = 0;
+                    if (matches !== null) {
+                        let success = false;
+                        const restrictTo = entry.restrictTo;
+                        for (const target of targets) {
+                            if (!restrictTo || validate(target, restrictTo)) {
+                                await entry.action(target, matches);
+                                success = true;
+                            }
+                        }
+                        return success;
+                    }
+                }
+
+                return false;
+            }, 'Execute Command');
+        };
+
+        const ConstructorMap = new Map<DocumentType, CastCtor>([
+            [DocumentType.COL, listSpec(Doc)],
+            [DocumentType.AUDIO, AudioField],
+            [DocumentType.IMG, ImageField],
+            [DocumentType.IMPORT, listSpec(Doc)],
+            [DocumentType.RTF, 'string'],
+        ]);
+
+        const tryCast = (view: DocumentView, type: DocumentType) => {
+            const ctor = ConstructorMap.get(type);
+            if (!ctor) {
+                return false;
+            }
+            return Cast(Doc.GetProto(view.props.Document).data, ctor) !== undefined;
+        };
+
+        const validate = (target: DocumentView, types: DocumentType[]) => {
+            for (const type of types) {
+                if (tryCast(target, type)) {
+                    return true;
+                }
+            }
+            return false;
+        };
+
+        const interpretNumber = (number: string) => {
+            const initial = parseInt(number);
+            if (!isNaN(initial)) {
+                return initial;
+            }
+            const converted = interpreter.wordsToNumbers(number, { fuzzy: true });
+            if (converted === null) {
+                return NaN;
+            }
+            return typeof converted === 'string' ? parseInt(converted) : converted;
+        };
+
+        const Independent = new Map<string, IndependentEntry>([
+            [
+                'clear',
+                {
+                    action: (target: DocumentView) => (Doc.GetProto(target.props.Document).data = new List()),
+                    restrictTo: [DocumentType.COL],
+                },
+            ],
+
+            [
+                'open fields',
+                {
+                    action: (target: DocumentView) => {
+                        const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
+                        target.props.addDocTab(kvp, 'add:right');
+                    },
+                },
+            ],
+
+            [
+                'new outline',
+                {
+                    action: (target: DocumentView) => {
+                        const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true });
+                        const proto = newBox.proto!;
+                        const prompt = 'Press alt + r to start dictating here...';
+                        const head = 3;
+                        const anchor = head + prompt.length;
+                        const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
+                        proto.data = new RichTextField(proseMirrorState);
+                        proto.backgroundColor = '#eeffff';
+                        target.props.addDocTab(newBox, 'add:right');
+                    },
+                },
+            ],
+        ]);
+
+        const Dependent = new Array<DependentEntry>(
+            {
+                expression: /create (\w+) documents of type (image|nested collection)/g,
+                action: (target: DocumentView, matches: RegExpExecArray) => {
+                    const count = interpretNumber(matches[1]);
+                    const what = matches[2];
+                    const dataDoc = Doc.GetProto(target.props.Document);
+                    const fieldKey = 'data';
+                    if (isNaN(count)) {
+                        return;
+                    }
+                    for (let i = 0; i < count; i++) {
+                        let created: Doc | undefined;
+                        switch (what) {
+                            case 'image':
+                                created = Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg');
+                                break;
+                            case 'nested collection':
+                                created = Docs.Create.FreeformDocument([], {});
+                                break;
+                        }
+                        created && Doc.AddDocToList(dataDoc, fieldKey, created);
+                    }
+                },
+                restrictTo: [DocumentType.COL],
+            },
+
+            {
+                expression: /view as (freeform|stacking|masonry|schema|tree)/g,
+                action: (target: DocumentView, matches: RegExpExecArray) => {
+                    const mode = matches[1];
+                    mode && (target.props.Document._viewType = mode);
+                },
+                restrictTo: [DocumentType.COL],
+            }
+        );
+    }
+}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 0db9eab69..7628beef2 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -491,7 +491,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     let actualdW = Math.max(width + dW * scale, 20);
                     let actualdH = Math.max(height + dH * scale, 20);
                     const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen);
-                    console.log(fixedAspect);
                     if (fixedAspect) {
                         if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
                             if (dragRight && modifyNativeDim) {
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 60428fc98..3f9db2612 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -6,14 +6,12 @@ import { observer } from 'mobx-react';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { Cast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
-import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 40b0d285c..849deb04e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -426,28 +426,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const editorView = this._editorView;
         if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) {
             const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
-            const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm);
             var alink: Doc | undefined;
-            flattened1.forEach((flat, i) => {
-                const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm);
-                this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+            this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => {
                 const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
-                const sel = flattened[i];
-                tr = tr.addMark(sel.from, sel.to, splitter);
-                tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
-                    if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
-                        alink =
-                            alink ??
-                            (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
-                                DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
-                        newAutoLinks.add(alink);
-                        const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
-                        allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
-                        const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' });
-                        tr = tr.addMark(pos, pos + node.nodeSize, link);
-                    }
-                });
-                tr = tr.removeMark(sel.from, sel.to, splitter);
+                if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) {
+                    tr = tr.addMark(sel.from, sel.to, splitter);
+                    tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
+                        if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
+                            alink =
+                                alink ??
+                                (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
+                                    DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+                            newAutoLinks.add(alink);
+                            const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
+                            allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
+                            const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term', location: 'add:right' });
+                            tr = tr.addMark(pos, pos + node.nodeSize, link);
+                        }
+                    });
+                    tr = tr.removeMark(sel.from, sel.to, splitter);
+                }
             });
         }
         return tr;
@@ -841,6 +839,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     };
     recordDictation = () => {
+        console.log('RECORD DICTATIN:');
         DictationManager.Controls.listen({
             interimHandler: this.setDictationContent,
             continuous: { indefinite: false },
@@ -853,26 +852,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort);
 
     setDictationContent = (value: string) => {
+        console.log('DICTATION CONETNT: ' + value);
         if (this._editorView && this._recordingStart) {
+            console.log('STEP 1');
             if (this._break) {
-                const textanchor = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
-                this.addDocument(textanchor);
-                const link = DocUtils.MakeLinkToActiveAudio(() => textanchor, false).lastElement();
-                link && (Doc.GetProto(link).isDictation = true);
-                if (!link) return;
-                const audioanchor = Cast(link.anchor2, Doc, null);
-                if (!audioanchor) return;
-                audioanchor.backgroundColor = 'tan';
-                const audiotag = this._editorView.state.schema.nodes.audiotag.create({
-                    timeCode: NumCast(audioanchor._timecodeToShow),
-                    audioId: audioanchor[Id],
-                    textId: textanchor[Id],
-                });
-                Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode;
-                const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
-                const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
-                this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+                console.log('BREAK');
+                const textanchorFunc = () => {
+                    const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
+                    return this.addDocument(tanch) ? tanch : undefined;
+                };
+                const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
+                if (link) {
+                    Doc.GetProto(link).isDictation = true;
+                    const audioanchor = Cast(link.anchor2, Doc, null);
+                    const textanchor = Cast(link.anchor1, Doc, null);
+                    if (audioanchor) {
+                        audioanchor.backgroundColor = 'tan';
+                        const audiotag = this._editorView.state.schema.nodes.audiotag.create({
+                            timeCode: NumCast(audioanchor._timecodeToShow),
+                            audioId: audioanchor[Id],
+                            textId: textanchor[Id],
+                        });
+                        Doc.GetProto(textanchor).title = 'dictation:' + audiotag.attrs.timeCode;
+                        const tr = this._editorView.state.tr.insert(this._editorView.state.doc.content.size, audiotag);
+                        const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size));
+                        this._editorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size)));
+                    }
+                }
             }
+            console.log('FINALIziNG');
             const from = this._editorView.state.selection.from;
             this._break = false;
             const tr = this._editorView.state.tr.insertText(value);
@@ -1468,7 +1476,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             const editor = this._editorView!;
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
-            let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
+            let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
             FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
         }
@@ -1725,7 +1733,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     @computed get audioHandle() {
         return (
-            <div className="formattedTextBox-dictation" onClick={action(e => (this._recording = !this._recording))}>
+            <div
+                className="formattedTextBox-dictation"
+                onPointerDown={e =>
+                    setupMoveUpEvents(
+                        this,
+                        e,
+                        returnFalse,
+                        emptyFunction,
+                        action(e => (this._recording = !this._recording))
+                    )
+                }>
                 <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? 'red' : 'blue', transitionDelay: '0.6s', opacity: this._recording ? 1 : 0.25 }} icon={'microphone'} size="sm" />
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From f9cded6755ee236d88e21b3b2d4c6da357d059b2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 8 Aug 2022 15:47:35 -0400
Subject: moved 5 sec anno recording button to documentButtonsBar.  made
 recording button appear only when there is one & click to play recording.

---
 src/client/util/CurrentUserUtils.ts                |  3 +-
 src/client/util/DictationManager.ts                |  4 --
 src/client/views/DocumentButtonBar.tsx             | 24 ++++++-
 src/client/views/PropertiesButtons.tsx             |  9 ---
 src/client/views/nodes/DocumentView.scss           |  6 +-
 src/client/views/nodes/DocumentView.tsx            | 82 +++++++++++++---------
 src/client/views/nodes/button/FontIconBox.tsx      |  9 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx | 12 +---
 8 files changed, 90 insertions(+), 59 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7856c913b..d19874720 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -107,7 +107,7 @@ export class CurrentUserUtils {
         const reqdClickList = reqdTempOpts.map(opts => {
             const allOpts = {...reqdClickOpts, ...opts.opts};
             const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
-            return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script,allOpts));
+            return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts);
         });
 
         const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true};
@@ -633,6 +633,7 @@ export class CurrentUserUtils {
             { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
             { title: "Right",  toolTip: "Right align",  btnType: ButtonType.ToggleButton, icon: "align-right",  scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
             { title: "NoLink", toolTip: "Auto Link",    btnType: ButtonType.ToggleButton, icon: "link",         scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}},
+            { title: "Dictate",toolTip: "Dictate",      btnType: ButtonType.ToggleButton, icon: "microphone",   scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
         ];
     }
 
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 13f036838..0a61f3478 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -152,7 +152,6 @@ export namespace DictationManager {
 
             return new Promise<string>((resolve, reject) => {
                 recognizer.onerror = (e: any) => {
-                    console.log('SPEECH error:', encodeURIComponent);
                     // e is SpeechRecognitionError but where is that defined?
                     if (!(indefinite && e.error === 'no-speech')) {
                         recognizer.stop();
@@ -162,7 +161,6 @@ export namespace DictationManager {
                 };
 
                 recognizer.onresult = (e: SpeechRecognitionEvent) => {
-                    console.log('RESULT: ', e);
                     current = synthesize(e, intra);
                     let matchedTerminator: string | undefined;
                     if (options?.terminators && (matchedTerminator = options.terminators.find(end => (current ? current.trim().toLowerCase().endsWith(end.toLowerCase()) : false)))) {
@@ -175,7 +173,6 @@ export namespace DictationManager {
                 };
 
                 recognizer.onend = (e: Event) => {
-                    console.log('END: ', e);
                     if (!indefinite || isManuallyStopped) {
                         return complete();
                     }
@@ -188,7 +185,6 @@ export namespace DictationManager {
                 };
 
                 const complete = () => {
-                    console.log('COMPLETE:');
                     if (indefinite) {
                         current && sessionResults.push(current);
                         sessionResults.length && resolve(sessionResults.join(inter || interSession));
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 1d4056759..265df3abc 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
 import { Cast, NumCast } from '../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 import { Docs } from '../documents/Documents';
@@ -334,6 +334,27 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             </Tooltip>
         );
     }
+
+    @observable _isRecording = false;
+    @computed
+    get recordButton() {
+        const targetDoc = this.view0?.props.Document;
+        return !targetDoc ? null : (
+            <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
+                <div
+                    className="documentButtonBar-icon"
+                    style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
+                    onClick={undoBatch(
+                        action(e => {
+                            this._isRecording = true;
+                            this.props.views().map(view => view?.docView?.recordAudioAnnotation(action(() => (this._isRecording = false))));
+                        })
+                    )}>
+                    <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
+                </div>
+            </Tooltip>
+        );
+    }
     @observable _aliasDown = false;
     onTemplateButton = action((e: React.PointerEvent): void => {
         this._tooltipOpen = false;
@@ -411,6 +432,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
                     </div>
                 ) : null}
+                <div className="documentButtonBar-button">{this.recordButton}</div>
                 {
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
                     /*<div className="documentButtonBar-button">
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 39e7b89c1..80c2c7705 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -68,14 +68,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => 'thumbtack'
         );
     }
-    @computed get dictationButton() {
-        return this.propertyToggleBtn(
-            'Dictate',
-            '_showAudio',
-            on => `${on ? 'Hide' : 'Show'} dictation/recording controls`,
-            on => 'microphone'
-        );
-    }
     @computed get maskButton() {
         return this.propertyToggleBtn(
             'Mask',
@@ -365,7 +357,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.titleButton)}
                 {toggle(this.captionButton)}
                 {toggle(this.lockButton)}
-                {toggle(this.dictationButton, { display: isNovice ? 'none' : '' })}
                 {toggle(this.onClickButton)}
                 {toggle(this.fitWidthButton)}
                 {toggle(this.freezeThumb)}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index c02692bfb..6ea697a2f 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -65,11 +65,13 @@
         width: 25px;
         height: 25;
         position: absolute;
-        top: 10px;
-        left: 10px;
+        top: 0;
+        left: 50%;
         border-radius: 25px;
         background: white;
         opacity: 0.3;
+        pointer-events: all;
+        cursor: default;
 
         svg {
             width: 90% !important;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2524d66dd..4b2bd07ef 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -10,7 +10,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -52,6 +52,8 @@ import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PresBox } from './trails/PresBox';
 import React = require('react');
+import { DictationManager } from '../../util/DictationManager';
+import { Tooltip } from '@material-ui/core';
 const { Howl } = require('howler');
 
 interface Window {
@@ -1005,16 +1007,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.props.DocumentView().screenToLocalTransform().Scale;
     @computed get contents() {
         TraceMobx();
-        const audioView = !this.layoutDoc._showAudio ? null : (
-            <div className="documentView-audioBackground" onPointerDown={this.recordAudioAnnotation} onPointerEnter={this.onPointerEnter}>
-                <FontAwesomeIcon
-                    className="documentView-audioFont"
-                    style={{ color: [DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }}
-                    icon={!DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']).length ? 'microphone' : 'file-audio'}
-                    size="sm"
-                />
-            </div>
-        );
+        const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
+        const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
+        const audioView =
+            (!this.props.isSelected() && !this._isHovering) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this._mediaState) ? null : (
+                <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
+                    <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
+                        <FontAwesomeIcon className="documentView-audioFont" style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
+                    </div>
+                </Tooltip>
+            );
 
         return (
             <div
@@ -1148,26 +1150,29 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     }
 
     @action
-    onPointerEnter = () => {
+    playAnnotation = () => {
         const self = this;
-        const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations']);
-        if (audioAnnos && audioAnnos.length && this._mediaState === 0) {
-            const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
-            anno.data instanceof AudioField &&
-                new Howl({
-                    src: [anno.data.url.href],
-                    format: ['mp3'],
-                    autoplay: true,
-                    loop: false,
-                    volume: 0.5,
-                    onend: function () {
-                        runInAction(() => (self._mediaState = 0));
-                    },
-                });
+        const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+        const anno = audioAnnos.lastElement();
+        if (anno instanceof AudioField && this._mediaState === 0) {
+            new Howl({
+                src: [anno.url.href],
+                format: ['mp3'],
+                autoplay: true,
+                loop: false,
+                volume: 0.5,
+                onend: function () {
+                    runInAction(() => {
+                        console.log('PLAYED');
+                        self._mediaState = 0;
+                    });
+                },
+            });
             this._mediaState = 1;
         }
     };
-    recordAudioAnnotation = () => {
+
+    recordAudioAnnotation = (onEnd?: () => void) => {
         let gumStream: any;
         let recorder: any;
         const self = this;
@@ -1176,18 +1181,30 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 audio: true,
             })
             .then(function (stream) {
+                let audioTextAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
+                if (audioTextAnnos) audioTextAnnos.push('');
+                else audioTextAnnos = self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'] = new List<string>(['']);
+                DictationManager.Controls.listen({
+                    interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
+                    continuous: { indefinite: false },
+                }).then(results => {
+                    if (results && [DictationManager.Controls.Infringed].includes(results)) {
+                        DictationManager.Controls.stop();
+                    }
+                    onEnd?.();
+                });
+
                 gumStream = stream;
                 recorder = new MediaRecorder(stream);
                 recorder.ondataavailable = async (e: any) => {
                     const [{ result }] = await Networking.UploadFilesToServer(e.data);
                     if (!(result instanceof Error)) {
-                        const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: 'audio test', _width: 200, _height: 32 });
-                        audioDoc.treeViewExpandedView = 'layout';
-                        const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(Doc));
+                        const audioField = new AudioField(result.accessPaths.agnostic.client);
+                        const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
                         if (audioAnnos === undefined) {
-                            self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioDoc]);
+                            self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioField]);
                         } else {
-                            audioAnnos.push(audioDoc);
+                            audioAnnos.push(audioField);
                         }
                     }
                 };
@@ -1195,6 +1212,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 recorder.start();
                 setTimeout(() => {
                     recorder.stop();
+                    DictationManager.Controls.stop(false);
                     runInAction(() => (self._mediaState = 0));
                     gumStream.getAudioTracks()[0].stop();
                 }, 5000);
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 78ef85ec2..d3b95e25a 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ColorState, SketchPicker } from 'react-color';
@@ -710,6 +710,13 @@ ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
     }
     if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
 });
+ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) {
+    const textView = RichTextMenu.Instance?.TextView;
+    if (checkResult) {
+        return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent';
+    }
+    if (textView) runInAction(() => (textView._recording = !textView._recording));
+});
 
 ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
     const editorView = RichTextMenu.Instance?.TextView?.EditorView;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 849deb04e..929cca1ea 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -771,7 +771,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const uicontrols: ContextMenuProps[] = [];
         !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
-        uicontrols.push({ description: `${this.layoutDoc._showAudio ? 'Hide' : 'Show'} Dictation Icon`, event: () => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
         !Doc.noviceMode &&
             uicontrols.push({
@@ -839,7 +838,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     };
     recordDictation = () => {
-        console.log('RECORD DICTATIN:');
         DictationManager.Controls.listen({
             interimHandler: this.setDictationContent,
             continuous: { indefinite: false },
@@ -852,11 +850,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort);
 
     setDictationContent = (value: string) => {
-        console.log('DICTATION CONETNT: ' + value);
         if (this._editorView && this._recordingStart) {
-            console.log('STEP 1');
             if (this._break) {
-                console.log('BREAK');
                 const textanchorFunc = () => {
                     const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
                     return this.addDocument(tanch) ? tanch : undefined;
@@ -880,7 +875,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     }
                 }
             }
-            console.log('FINALIziNG');
             const from = this._editorView.state.selection.from;
             this._break = false;
             const tr = this._editorView.state.tr.insertText(value);
@@ -1732,7 +1726,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             .scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1));
 
     @computed get audioHandle() {
-        return (
+        return !this._recording ? null : (
             <div
                 className="formattedTextBox-dictation"
                 onPointerDown={e =>
@@ -1744,7 +1738,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         action(e => (this._recording = !this._recording))
                     )
                 }>
-                <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: this._recording ? 'red' : 'blue', transitionDelay: '0.6s', opacity: this._recording ? 1 : 0.25 }} icon={'microphone'} size="sm" />
+                <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" />
             </div>
         );
     }
@@ -1898,7 +1892,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     </div>
                     {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
                     {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
-                    {!this.layoutDoc._showAudio ? null : this.audioHandle}
+                    {this.audioHandle}
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From bca0026764fe554e1066b2f432e749371450d239 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 9 Aug 2022 11:35:00 -0400
Subject: added an audio annotation button to the anchor menu bar.

---
 src/client/views/DocumentButtonBar.tsx             |  12 +-
 src/client/views/MarqueeAnnotator.tsx              |   1 +
 src/client/views/SidebarAnnos.tsx                  |   1 +
 src/client/views/nodes/DocumentView.tsx            |  35 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  11 ++
 src/client/views/pdf/AnchorMenu.tsx                |  14 ++
 src/fields/documentSchemas.ts                      | 182 ++++++++++-----------
 7 files changed, 145 insertions(+), 111 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 265df3abc..81e417fca 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -21,7 +21,7 @@ import './DocumentButtonBar.scss';
 import { Colors } from './global/globalEnums';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
 import { TemplateMenu } from './TemplateMenu';
@@ -347,7 +347,15 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                     onClick={undoBatch(
                         action(e => {
                             this._isRecording = true;
-                            this.props.views().map(view => view?.docView?.recordAudioAnnotation(action(() => (this._isRecording = false))));
+                            this.props.views().map(
+                                view =>
+                                    view &&
+                                    DocumentViewInternal.recordAudioAnnotation(
+                                        view.dataDoc,
+                                        view.LayoutFieldKey,
+                                        action(() => (this._isRecording = false))
+                                    )
+                            );
                         })
                     )}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index b01ee5f42..f90ad8bb5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -65,6 +65,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
 
         AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true);
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true));
+        AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
         AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 1c14c7cd5..90d9c3c43 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -74,6 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
         this.addDocument(target);
         this._stackRef.current?.focusDocument(target);
+        return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
         if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4b2bd07ef..f87581875 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -206,7 +206,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
     _animateScaleTime = 300; // milliseconds;
     @observable _animateScalingTo = 0;
-    @observable _mediaState = 0;
     @observable _pendingDoubleClick = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _downX: number = 0;
@@ -879,7 +878,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
             if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
-                !Doc.noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? 'Show' : 'Hide'} Audio Button`, event: action(() => (this.layoutDoc._showAudio = !this.layoutDoc._showAudio)), icon: 'microphone' });
                 const existingOnClick = cm.findByDescription('OnClick...');
                 const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
 
@@ -1010,10 +1008,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
         const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
         const audioView =
-            (!this.props.isSelected() && !this._isHovering) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this._mediaState) ? null : (
+            (!this.props.isSelected() && !this._isHovering && this.dataDoc.audioAnnoState !== 2) || this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!audioAnnosCount && !this.dataDoc.audioAnnoState) ? null : (
                 <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
                     <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
-                        <FontAwesomeIcon className="documentView-audioFont" style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][this._mediaState] }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
+                        <FontAwesomeIcon
+                            className="documentView-audioFont"
+                            style={{ color: [audioAnnosCount ? 'blue' : 'gray', 'green', 'red'][NumCast(this.dataDoc.audioAnnoState)] }}
+                            icon={!audioAnnosCount ? 'microphone' : 'file-audio'}
+                            size="sm"
+                        />
                     </div>
                 </Tooltip>
             );
@@ -1154,7 +1157,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const self = this;
         const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
         const anno = audioAnnos.lastElement();
-        if (anno instanceof AudioField && this._mediaState === 0) {
+        if (anno instanceof AudioField && this.dataDoc.audioAnnoState === 0) {
             new Howl({
                 src: [anno.url.href],
                 format: ['mp3'],
@@ -1163,27 +1166,25 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 volume: 0.5,
                 onend: function () {
                     runInAction(() => {
-                        console.log('PLAYED');
-                        self._mediaState = 0;
+                        self.dataDoc.audioAnnoState = 0;
                     });
                 },
             });
-            this._mediaState = 1;
+            this.dataDoc.audioAnnoState = 1;
         }
     };
 
-    recordAudioAnnotation = (onEnd?: () => void) => {
+    static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) {
         let gumStream: any;
         let recorder: any;
-        const self = this;
         navigator.mediaDevices
             .getUserMedia({
                 audio: true,
             })
             .then(function (stream) {
-                let audioTextAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
+                let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null);
                 if (audioTextAnnos) audioTextAnnos.push('');
-                else audioTextAnnos = self.dataDoc[self.LayoutFieldKey + '-audioAnnotations-text'] = new List<string>(['']);
+                else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']);
                 DictationManager.Controls.listen({
                     interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
                     continuous: { indefinite: false },
@@ -1200,24 +1201,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     const [{ result }] = await Networking.UploadFilesToServer(e.data);
                     if (!(result instanceof Error)) {
                         const audioField = new AudioField(result.accessPaths.agnostic.client);
-                        const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+                        const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null);
                         if (audioAnnos === undefined) {
-                            self.dataDoc[self.LayoutFieldKey + '-audioAnnotations'] = new List([audioField]);
+                            dataDoc[field + '-audioAnnotations'] = new List([audioField]);
                         } else {
                             audioAnnos.push(audioField);
                         }
                     }
                 };
-                runInAction(() => (self._mediaState = 2));
+                runInAction(() => (dataDoc.audioAnnoState = 2));
                 recorder.start();
                 setTimeout(() => {
                     recorder.stop();
                     DictationManager.Controls.stop(false);
-                    runInAction(() => (self._mediaState = 0));
+                    runInAction(() => (dataDoc.audioAnnoState = 0));
                     gumStream.getAudioTracks()[0].stop();
                 }, 5000);
             });
-    };
+    }
 
     captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
     @computed get innards() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 929cca1ea..223441b3b 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -63,6 +63,7 @@ import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
 import { text } from 'body-parser';
 import { CollectionTreeView } from '../../collections/CollectionTreeView';
+import { DocumentViewInternal } from '../DocumentView';
 const translateGoogleApi = require('translate-google-api');
 
 export interface FormattedTextBoxProps {
@@ -249,6 +250,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.layoutDoc.showSidebar && this.toggleSidebar();
             this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
         };
+        AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
+            !this.layoutDoc.showSidebar && this.toggleSidebar();
+            const anchor = this.getAnchor();
+            const target = this._sidebarRef.current?.anchorMenuClick(anchor);
+            if (target) {
+                anchor.followLinkAudio = true;
+                DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
+                target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
+            }
+        };
         AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
             this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
             return undefined;
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 1a1120b6c..ee2ae10a7 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -49,6 +49,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
     public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+    public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
@@ -92,6 +93,10 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         );
     };
 
+    audioDown = (e: React.PointerEvent) => {
+        setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e));
+    };
+
     cropDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
@@ -196,6 +201,15 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                               <FontAwesomeIcon icon="comment-alt" size="lg" />
                           </button>
                       </Tooltip>,
+                      AnchorMenu.Instance.OnAudio === unimplementedFunction ? (
+                          <></>
+                      ) : (
+                          <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">{'Click to Record Annotation'}</div>}>
+                              <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
+                                  <FontAwesomeIcon icon="microphone" size="lg" />
+                              </button>
+                          </Tooltip>
+                      ),
                       //NOTE: link popup is currently in progress
                       <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
                           <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index be39e0709..24b5a359d 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -1,116 +1,114 @@
-import { makeInterface, createSchema, listSpec } from "./Schema";
-import { ScriptField } from "./ScriptField";
-import { Doc } from "./Doc";
-import { DateField } from "./DateField";
-import { SchemaHeaderField } from "./SchemaHeaderField";
+import { makeInterface, createSchema, listSpec } from './Schema';
+import { ScriptField } from './ScriptField';
+import { Doc } from './Doc';
+import { DateField } from './DateField';
+import { SchemaHeaderField } from './SchemaHeaderField';
 
 export const documentSchema = createSchema({
     // content properties
-    type: "string",             // enumerated type of document -- should be template-specific (ie, start with an '_')
-    title: "string",            // document title (can be on either data document or layout)
-    isTemplateForField: "string",// if specified, it indicates the document is a template that renders the specified field
-    creationDate: DateField,    // when the document was created
-    links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_')
+    title: 'string', // document title (can be on either data document or layout)
+    isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field
+    creationDate: DateField, // when the document was created
+    links: listSpec(Doc), // computed (readonly) list of links associated with this document
 
     // "Location" properties in a very general sense
-    _curPage: "number",         // current page of a page based document
-    _currentFrame: "number",    // current frame of a frame based collection (e.g., a progressive slide)
-    lastFrame: "number",        // last frame of a frame based collection (e.g., a progressive slide)
-    activeFrame: "number",      // the active frame of a frame based animated document 
-    _currentTimecode: "number", // current play back time of a temporal document (video / audio)
-    _timecodeToShow: "number",  // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
-    _timecodeToHIde: "number",  // the time that a document should be hidden
-    markers: listSpec(Doc),     // list of markers for audio / video
-    x: "number",                // x coordinate when in a freeform view 
-    y: "number",                // y coordinate when in a freeform view 
-    z: "number",                // z "coordinate" - non-zero specifies the overlay layer of a freeformview
-    zIndex: "number",           // zIndex of a document in a freeform view
-    _scrollTop: "number",       // scroll position of a scrollable document (pdf, text, web)
-    lat: "number",
-    lng: "number",
+    _curPage: 'number', // current page of a page based document
+    _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide)
+    lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide)
+    activeFrame: 'number', // the active frame of a frame based animated document
+    _currentTimecode: 'number', // current play back time of a temporal document (video / audio)
+    _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
+    _timecodeToHIde: 'number', // the time that a document should be hidden
+    markers: listSpec(Doc), // list of markers for audio / video
+    x: 'number', // x coordinate when in a freeform view
+    y: 'number', // y coordinate when in a freeform view
+    z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview
+    zIndex: 'number', // zIndex of a document in a freeform view
+    _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
+    lat: 'number',
+    lng: 'number',
 
     // appearance properties on the layout
-    "_backgroundGrid-spacing": "number", // the size of the grid for collection views
-    _autoHeight: "boolean",     // whether the height of the document should be computed automatically based on its contents
-    _nativeWidth: "number",     // native width of document which determines how much document contents are scaled when the document's width is set
-    _nativeHeight: "number",    // "
-    _width: "number",           // width of document in its container's coordinate system
-    _height: "number",          // "
-    _xPadding: "number",        // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
-    _yPadding: "number",        // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
-    _xMargin: "number",         // margin added on left/right of most documents to add separation from their container
-    _yMargin: "number",         // margin added on top/bottom of most documents to add separation from their container
-    _overflow: "string",        // sets overflow behvavior for CollectionFreeForm views
-    _showCaption: "string",     // whether editable caption text is overlayed at the bottom of the document 
-    _showTitle: "string",       // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";".  Use :hover suffix to indicate title should be shown on hover
-    _showAudio: "boolean",      // whether to show the audio record icon on documents
-    _pivotField: "string",      // specifies which field key should be used as the timeline/pivot axis
-    _columnsFill: "boolean",    // whether documents in a stacking view column should be sized to fill the column
-    _columnsSort: "string",     // how a document should be sorted "ascending", "descending", undefined (none)   
-    _columnsHideIfEmpty: "boolean",   // whether empty stacking view column headings should be hidden
+    '_backgroundGrid-spacing': 'number', // the size of the grid for collection views
+    _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents
+    _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set
+    _nativeHeight: 'number', // "
+    _width: 'number', // width of document in its container's coordinate system
+    _height: 'number', // "
+    _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
+    _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
+    _xMargin: 'number', // margin added on left/right of most documents to add separation from their container
+    _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container
+    _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views
+    _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document
+    _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";".  Use :hover suffix to indicate title should be shown on hover
+    _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis
+    _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column
+    _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none)
+    _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
     _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
     _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
-    _fontSize: "string",
-    _fontFamily: "string",
-    _sidebarWidthPercent: "string", // percent of text window width taken up by sidebar
+    _fontSize: 'string',
+    _fontFamily: 'string',
+    _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
 
     // appearance properties on the data document
-    backgroundColor: "string",  // background color of document
-    borderRounding: "string",   // border radius rounding of document
-    boxShadow: "string",        // the amount of shadow around the perimeter of a document
-    color: "string",            // foreground color of document
-    fitContentsToBox: "boolean",// whether freeform view contents should be zoomed/panned to fill the area of the document view box
-    fontSize: "string",
-    hidden: "boolean",          // whether a document should not be displayed
-    isInkMask: "boolean",       // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
-    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
-    layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
-    letterSpacing: "string",
-    opacity: "number",          // opacity of document
-    strokeWidth: "number",
-    strokeBezier: "number",
-    strokeStartMarker: "string",
-    strokeEndMarker: "string",
-    strokeDash: "string",
-    textTransform: "string",
-    treeViewOpen: "boolean",    //  flag denoting whether the documents sub-tree (contents) is visible or hidden
-    treeViewExpandedView: "string", // name of field whose contents are being displayed as the document's subtree
-    treeViewExpandedViewLock: "boolean", // whether the expanded view can be changed
-    treeViewOpenIsTransient: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
-    treeViewType: "string", // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
+    backgroundColor: 'string', // background color of document
+    borderRounding: 'string', // border radius rounding of document
+    boxShadow: 'string', // the amount of shadow around the perimeter of a document
+    color: 'string', // foreground color of document
+    fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
+    fontSize: 'string',
+    hidden: 'boolean', // whether a document should not be displayed
+    isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
+    layout: 'string', // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below
+    layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat
+    letterSpacing: 'string',
+    opacity: 'number', // opacity of document
+    strokeWidth: 'number',
+    strokeBezier: 'number',
+    strokeStartMarker: 'string',
+    strokeEndMarker: 'string',
+    strokeDash: 'string',
+    textTransform: 'string',
+    treeViewOpen: 'boolean', //  flag denoting whether the documents sub-tree (contents) is visible or hidden
+    treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
+    treeViewExpandedViewLock: 'boolean', // whether the expanded view can be changed
+    treeViewOpenIsTransient: 'boolean', // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+    treeViewType: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
 
     // interaction and linking properties
-    ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
-    onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    ignoreClick: 'boolean', // whether documents ignores input clicks (but does not ignore manipulation and other events)
+    onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
-    onPointerUp: ScriptField,   // script to run when document is clicked (can be overriden by an onClick prop)
-    onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    followLinkLocation: "string",// flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, ) 
-    hideLinkButton: "boolean",  // whether the blue link counter button should be hidden
-    hideAllLinks: "boolean",    // whether all individual blue anchor dots should be hidden
-    linkDisplay: "boolean",     // whether a link connection should be shown between link anchor endpoints.
-    isInPlaceContainer: "boolean",// whether the marked object will display addDocTab() calls that target "inPlace" destinations
-    isLinkButton: "boolean",    // whether document functions as a link follow button to follow the first link on the document when clicked   
-    layers: listSpec("string"), // which layers the document is part of
-    _lockedPosition: "boolean",  // whether the document can be moved (dragged)
-    _lockedTransform: "boolean",// whether a freeformview can pan/zoom
-    displayArrow: "boolean", // toggles directed arrows
+    onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+    onDragStart: ScriptField, // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
+    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
+    hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
+    hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
+    linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
+    isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
+    isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
+    layers: listSpec('string'), // which layers the document is part of
+    _lockedPosition: 'boolean', // whether the document can be moved (dragged)
+    _lockedTransform: 'boolean', // whether a freeformview can pan/zoom
+    displayArrow: 'boolean', // toggles directed arrows
 
     // drag drop properties
-    _stayInCollection: "boolean",// whether document can be dropped into a different collection
-    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
-    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
-    targetDropAction: "string", // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
-    childDropAction: "string",  // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
-    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+    _stayInCollection: 'boolean', // whether document can be dropped into a different collection
+    dragFactory: Doc, // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
+    dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move")
+    targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
+    childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+    removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped
 });
 
-
 export const collectionSchema = createSchema({
     childLayoutTemplate: Doc, // layout template to use to render children of a collecion
-    childLayoutString: "string", //layout string to use to render children of a collection
+    childLayoutString: 'string', //layout string to use to render children of a collection
     childClickedOpenTemplateView: Doc, // layout template to apply to a child when its clicked on in a collection and opened (requires onChildClick or other script to read this value and apply template)
-    childDontRegisterViews: "boolean", // whether views made of this document are registered so that they can be found when drawing links 
+    childDontRegisterViews: 'boolean', // whether views made of this document are registered so that they can be found when drawing links
     onChildClick: ScriptField, // script to run for each child when its clicked
     onChildDoubleClick: ScriptField, // script to run for each child when its clicked
     onCheckedClick: ScriptField, // script to run when a checkbox is clicked next to a child in a tree view
-- 
cgit v1.2.3-70-g09d2


From 0e8afc8e90efdf8037aa233a496e7d7bec75d908 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 9 Aug 2022 13:06:20 -0400
Subject: adjusted tootlip for showing pivot view of dashfieldview.  removed
 scrollbars for autoheight textboxes - metadata views weren't resizing
 properly.

---
 src/client/views/nodes/formattedText/DashFieldView.tsx    | 14 +++++++++-----
 src/client/views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 2 files changed, 10 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 8c8b74560..d2f7b5677 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -216,7 +216,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
     onPointerDownLabelSpan = (e: any) => {
         setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
             DashFieldViewMenu.createFieldView = this.createPivotForField;
-            DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16);
+            DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
         });
     };
 
@@ -253,17 +253,21 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
         DashFieldViewMenu.Instance.fadeOut(true);
     };
 
-    public show = (x: number, y: number) => {
+    @observable _fieldKey = '';
+
+    @action
+    public show = (x: number, y: number, fieldKey: string) => {
+        this._fieldKey = fieldKey;
         this.jumpTo(x, y, true);
         const hideMenu = () => {
             this.fadeOut(true);
-            document.removeEventListener('pointerdown', hideMenu);
+            document.removeEventListener('pointerdown', hideMenu, true);
         };
-        document.addEventListener('pointerdown', hideMenu);
+        document.addEventListener('pointerdown', hideMenu, true);
     };
     render() {
         const buttons = [
-            <Tooltip key="trash" title={<div className="dash-tooltip">{'Remove Link Anchor'}</div>}>
+            <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
                 <button className="antimodeMenu-button" onPointerDown={this.showFields}>
                     <FontAwesomeIcon icon="eye" size="lg" />
                 </button>
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 223441b3b..e027ab0bd 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1884,7 +1884,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         style={{
                             width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
                             pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
-                            overflow: this.layoutDoc._singleLine ? 'hidden' : undefined,
+                            overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined,
                         }}
                         onScroll={this.onScroll}
                         onDrop={this.ondrop}>
-- 
cgit v1.2.3-70-g09d2


From a63f017c213563728f45f2caa7415843f50f3559 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 9 Aug 2022 19:50:33 -0400
Subject: fixed click on menu button for text box with hyperlink to not bring
 up buttonBar.  fixed right click on hyperlink to just bring up button bar,
 but not native context menu.  fixe stacking view child pointer events so that
 sidebar documents in lightbox are editable.

---
 src/client/views/DocumentButtonBar.tsx                    |  7 +------
 src/client/views/collections/CollectionStackingView.tsx   |  5 +++--
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 10 +++++-----
 3 files changed, 9 insertions(+), 13 deletions(-)

(limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 81e417fca..1f8550ad6 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -274,12 +274,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
     get menuButton() {
         const targetDoc = this.view0?.props.Document;
         return !targetDoc ? null : (
-            <Tooltip
-                title={
-                    <>
-                        <div className="dash-tooltip">{`Open Context Menu`}</div>
-                    </>
-                }>
+            <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
                 <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
                 </div>
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ef68cadd7..d4efef47a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -300,9 +300,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             return this.addDocument?.(newDoc);
         }
     };
-    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+    isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
 
-    isChildContentActive = () => this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive));
+    isChildContentActive = () =>
+        this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
     // this is what renders the document that you see on the screen
     // called in Children: this actually adds a document to our children list
     getDisplayDoc(doc: Doc, width: () => number) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e027ab0bd..f61533619 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -703,9 +703,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
         const editor = this._editorView!;
         const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
-        let target = (e.target as any).parentElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
+        let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
         while (target && !target.dataset?.targethrefs) target = target.parentElement;
-        if (target) {
+        if (target && !(e.nativeEvent as any).dash) {
             const hrefs = (target.dataset?.targethrefs as string)
                 ?.trim()
                 .split(' ')
@@ -724,6 +724,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
                     })
                 );
+            e.preventDefault();
             e.stopPropagation();
             return;
         }
@@ -1474,10 +1475,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         document.removeEventListener('pointermove', this.onSelectMove);
     };
     onPointerUp = (e: React.PointerEvent): void => {
-        if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate) this.setupAnchorMenu();
+        if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
         if (!this._downEvent) return;
         this._downEvent = false;
-        if (this.props.isContentActive(true)) {
+        if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
             const editor = this._editorView!;
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
@@ -1795,7 +1796,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 />
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
-                    //@ts-ignore
                     <ComponentTag
                         {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                         NativeWidth={returnZero}
-- 
cgit v1.2.3-70-g09d2

