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.

---
 src/client/views/linking/LinkMenuItem.tsx | 89 +------------------------------
 1 file changed, 2 insertions(+), 87 deletions(-)

(limited to 'src/client/views/linking/LinkMenuItem.tsx')

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>
-- 
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/linking/LinkMenuItem.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

