From 49cfa95fdb3653a1224d4e5e3380e111c8e2014f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 4 Mar 2024 12:55:05 -0500
Subject: changes to restore template menu and fix freeform templates to always
 show contents.   Fix for alt-dropping an image to replace another image.

---
 src/client/views/TemplateMenu.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5fc33207e..f7729dbc7 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -81,7 +81,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     componentDidMount() {
         !this._addedKeys && (this._addedKeys = new ObservableSet());
         [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))]
-            .filter(key => key.startsWith('layout_'))
+            .filter(key => key.startsWith('layout_') && key !== 'layout_fieldKey')
             .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
     }
 
-- 
cgit v1.2.3-70-g09d2


From 43770615603c9d0f0aa19a3a676822737157c161 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 6 Mar 2024 09:44:38 -0500
Subject: enabled copy and paste to work with template docs (and not copy
 template).  Fixed tempaltes of collections to allow new nodes to be
 added/removed.

---
 src/client/views/DocComponent.tsx       |  8 ++---
 src/client/views/TemplateMenu.tsx       |  9 ++++--
 src/client/views/nodes/ScriptingBox.tsx | 32 ++++++++++----------
 src/fields/Doc.ts                       | 52 ++++++++++++++++++++++-----------
 4 files changed, 60 insertions(+), 41 deletions(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 2a527eca1..a25a8f77a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -182,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
 
             // docs.forEach(doc => doc.annotationOn === this.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
-            const targetDataDoc = this.dataDoc;
+            const targetDataDoc = this.Document[DocData]; // this.dataDoc;  // we want to write to the template, not the actual data doc
             const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
             const toRemove = value.filter(v => docs.includes(v));
 
@@ -228,7 +228,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
             if (this._props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.Document))) {
                 return false;
             }
-            const targetDataDoc = this.dataDoc;
+            const targetDataDoc = this.Document[DocData]; // this.dataDoc;  // we want to write to the template, not the actual data doc
             const effectiveAcl = GetEffectiveAcl(targetDataDoc);
 
             if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
@@ -245,9 +245,9 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() {
                         inheritParentAcls(targetDataDoc, doc, true);
                     });
 
-                    const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them.  maybe we should copy them over, though...
+                    const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them, so we copy them below (should really be some kind of inheritance since the template contents could change)
                     if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
-                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
+                    else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>([...added, ...(annoDocs === undefined ? DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]) : [])]);
                     targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField();
                 }
             }
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f7729dbc7..5d9abb460 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
@@ -81,8 +81,11 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     componentDidMount() {
         !this._addedKeys && (this._addedKeys = new ObservableSet());
         [...Array.from(Object.keys(this.props.docViews[0].Document[DocData])), ...Array.from(Object.keys(this.props.docViews[0].Document))]
-            .filter(key => key.startsWith('layout_') && key !== 'layout_fieldKey')
-            .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
+            .filter(key => key.startsWith('layout_') && (
+                StrCast(this.props.docViews[0].Document[key]).startsWith("<") ||
+                DocCast(this.props.docViews[0].Document[key])?.isTemplateDoc
+            ))
+            .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', '')))); // prettier-ignore
     }
 
     return100 = () => 300;
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 89650889d..d9d0dbe3e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -130,7 +130,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 }
             })
         );
-        observer.observe(document.getElementsByClassName('scriptingBox')[0]);
+        observer.observe(document.getElementsByClassName('scriptingBox-outerDiv')[0]);
     }
 
     @action
@@ -811,22 +811,20 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     render() {
         TraceMobx();
         return (
-            <div className={`scriptingBox`} onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined}>
-                <div className="scriptingBox-outerDiv" onWheel={e => this._props.isSelected() && e.stopPropagation()}>
-                    {this._paramSuggestion ? (
-                        <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}>
-                            {' '}
-                            {this._scriptSuggestedParams}{' '}
-                        </div>
-                    ) : null}
-                    {!this._applied && !this._function ? this.renderScriptingInputs : null}
-                    {this._applied && !this._function ? this.renderParamsInputs() : null}
-                    {!this._applied && this._function ? this.renderFunctionInputs() : null}
-
-                    {!this._applied && !this._function ? this.renderScriptingTools() : null}
-                    {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null}
-                    {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null}
-                </div>
+            <div className="scriptingBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerUp={!this._function ? this.suggestionPos : undefined} onWheel={e => this._props.isSelected() && e.stopPropagation()}>
+                {this._paramSuggestion ? (
+                    <div className="boxed" ref={this._suggestionRef} style={{ left: this._suggestionBoxX + 20, top: this._suggestionBoxY - 15, display: 'inline' }}>
+                        {' '}
+                        {this._scriptSuggestedParams}{' '}
+                    </div>
+                ) : null}
+                {!this._applied && !this._function ? this.renderScriptingInputs : null}
+                {this._applied && !this._function ? this.renderParamsInputs() : null}
+                {!this._applied && this._function ? this.renderFunctionInputs() : null}
+
+                {!this._applied && !this._function ? this.renderScriptingTools() : null}
+                {this._applied && !this._function ? this.renderTools('Run', () => this.onRun()) : null}
+                {!this._applied && this._function ? this.renderTools('Create Function', () => this.onCreate()) : null}
             </div>
         );
     }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index b1bdd50a7..1df9d80a1 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -614,8 +614,19 @@ export namespace Doc {
     // this lists out all the tag ids that can be  in a RichTextField that might contain document ids.
     // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
     export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
-    export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean): Promise<Doc> {
-        if (Doc.IsBaseProto(doc)) return doc;
+    export async function makeClone(
+        doc: Doc,
+        cloneMap: Map<string, Doc>,
+        linkMap: Map<string, Doc>,
+        rtfs: { copy: Doc; key: string; field: RichTextField }[],
+        exclusions: string[],
+        pruneDocs: Doc[],
+        cloneLinks: boolean,
+        cloneTemplates: boolean
+    ): Promise<Doc> {
+        if (Doc.IsBaseProto(doc) || ((Doc.Get(doc, 'isTemplateDoc', true) || Doc.Get(doc, 'isTemplateForField', true)) && !cloneTemplates)) {
+            return doc;
+        }
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
         const copy = new Doc(undefined, true);
         cloneMap.set(doc[Id], copy);
@@ -630,7 +641,7 @@ export namespace Doc {
                     const list = await Cast(doc[key], listSpec(Doc));
                     const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
                     if (docs !== undefined && docs.length) {
-                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)));
+                        const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)));
                         assignKey(new List<Doc>(clones));
                     } else {
                         assignKey(ObjectField.MakeCopy(field));
@@ -645,7 +656,7 @@ export namespace Doc {
                                 );
                                 const results = docids && (await DocServer.GetRefFields(docids));
                                 const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
-                                docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
+                                docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
                                 rtfs.push({ copy, key, field });
                             }
                         }
@@ -657,8 +668,14 @@ export namespace Doc {
                 } else if (docAtKey instanceof Doc) {
                     if (pruneDocs.includes(docAtKey)) {
                         // prune doc and do nothing
-                    } else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) {
-                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
+                    } else if (
+                        !Doc.IsSystem(docAtKey) &&
+                        (key.includes('layout[') ||
+                            key.startsWith('layout') || //
+                            ['embedContainer', 'annotationOn', 'proto'].includes(key) ||
+                            (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === Doc.CurrentUserEmail))
+                    ) {
+                        assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
                     } else {
                         assignKey(docAtKey);
                     }
@@ -681,16 +698,17 @@ export namespace Doc {
                 ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) &&
                     (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id])))
             ) {
-                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
+                linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates));
             }
         });
-        Doc.SetInPlace(copy, 'title', '>:' + doc.title, true);
+        if (Doc.Get(copy, 'title', true)) copy.title = '>:' + doc.title;
+        //  Doc.SetInPlace(copy, 'title', '>:' + doc.title, true);
         copy.cloneOf = doc;
         cloneMap.set(doc[Id], copy);
 
         return copy;
     }
-    export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
+    export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, cloneTemplates: boolean, visited: Set<Doc>) {
         if (visited.has(clone)) return;
         visited.add(clone);
         Object.keys(clone)
@@ -699,22 +717,22 @@ export namespace Doc {
                 const docAtKey = DocCast(clone[key]);
                 if (docAtKey && !Doc.IsSystem(docAtKey)) {
                     if (!Array.from(cloneMap.values()).includes(docAtKey)) {
-                        clone[key] = cloneMap.get(docAtKey[Id]);
+                        clone[key] = !cloneTemplates && (Doc.Get(docAtKey, 'isTemplateDoc', true) || Doc.Get(docAtKey, 'isTemplateForField', true)) ? docAtKey : cloneMap.get(docAtKey[Id]);
                     } else {
-                        repairClone(docAtKey, cloneMap, visited);
+                        repairClone(docAtKey, cloneMap, cloneTemplates, visited);
                     }
                 }
             });
     }
-    export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+    export function MakeClones(docs: Doc[], cloneLinks: boolean, cloneTemplates: boolean) {
         const cloneMap = new Map<string, Doc>();
-        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
+        return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneTemplates, cloneMap));
     }
 
-    export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
+    export async function MakeClone(doc: Doc, cloneLinks = true, cloneTemplates = true, cloneMap: Map<string, Doc> = new Map()) {
         const linkMap = new Map<string, Doc>();
         const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
-        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks);
+        const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks, cloneTemplates);
         const repaired = new Set<Doc>();
         const linkedDocs = Array.from(linkMap.values());
         linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
@@ -732,7 +750,7 @@ export namespace Doc {
             copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
         });
         const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
-        clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired));
+        clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, cloneTemplates, repaired));
         return { clone: copy, map: cloneMap, linkMap };
     }
 
@@ -1395,7 +1413,7 @@ export namespace Doc {
             const list = Array.from(Object.values(fieldlist))
                 .map(d => DocCast(d))
                 .filter(d => d);
-            const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list;
+            const docs = clone ? (await Promise.all(Doc.MakeClones(list, false, false))).map(res => res.clone) : list;
             if (ptx !== undefined && pty !== undefined && newPoint !== undefined) {
                 const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0;
                 const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0;
-- 
cgit v1.2.3-70-g09d2


From bd9170eaa21a2fbccd0912f50d40cefaf0bbe47f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 6 Mar 2024 12:28:24 -0500
Subject: cleaned up some dropActionType strings.  fixed stackingview scrolling
 when always active.  fixed schemaView keys dropdown menu to be a popup. 
 cleaned up  display of key Finfo's

---
 src/client/documents/Documents.ts                  |  46 +++----
 src/client/views/DashboardView.tsx                 |   9 +-
 src/client/views/TemplateMenu.tsx                  |   2 +-
 .../views/collections/CollectionStackingView.tsx   |  15 ++-
 src/client/views/collections/CollectionSubView.tsx |   5 +-
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../collectionSchema/CollectionSchemaView.scss     | 149 +++++++++++----------
 .../collectionSchema/CollectionSchemaView.tsx      |  68 ++++++----
 src/client/views/nodes/LinkAnchorBox.tsx           |   4 +-
 .../views/nodes/RecordingBox/RecordingBox.tsx      |   4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 11 files changed, 170 insertions(+), 142 deletions(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 031560886..c9b5cb915 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -70,14 +70,14 @@ class EmptyBox {
 }
 
 export enum FInfoFieldType {
-    string,
-    boolean,
-    number,
-    Doc,
-    enumeration,
-    date,
-    list,
-    rtf,
+    string = 'string',
+    boolean = 'boolean',
+    number = 'number',
+    Doc = 'Doc',
+    enumeration = 'enum',
+    date = 'date',
+    list = 'list',
+    rtf = 'rich text',
 }
 export class FInfo {
     description: string = '';
@@ -198,20 +198,20 @@ type DATEt = DateInfo | number;
 type DTYPEt = DTypeInfo | string;
 export class DocumentOptions {
     // coordinate and dimensions depending on view
-    x?: NUMt = new NumInfo('x coordinate of document in a freeform view', false);
-    y?: NUMt = new NumInfo('y coordinate of document in a freeform view', false);
+    x?: NUMt = new NumInfo('horizontal coordinate in freeform view', false);
+    y?: NUMt = new NumInfo('vertical coordinate in freeform view', false);
     z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]);
-    overlayX?: NUMt = new NumInfo('x coordinate of document in a overlay view', false);
-    overlayY?: NUMt = new NumInfo('y coordinate of document in a overlay view', false);
-    text?: RTFt = new RtfInfo('rich text of a text doc', true);
+    overlayX?: NUMt = new NumInfo('horizontal coordinate in overlay view', false);
+    overlayY?: NUMt = new NumInfo('vertical coordinate in overlay view', false);
+    text?: RTFt = new RtfInfo('plain or rich text', true);
     _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false);
     _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
-    latitude?: NUMt = new NumInfo('latitude coordinate for map views', false);
-    longitude?: NUMt = new NumInfo('longitude coordinate for map views', false);
+    latitude?: NUMt = new NumInfo('latitude coordinate', false);
+    longitude?: NUMt = new NumInfo('longitude coordinate', false);
     routeCoordinates?: STRt = new StrInfo("stores a route's/direction's coordinates (stringified version)"); // for a route document, this stores the route's coordinates
-    markerType?: STRt = new StrInfo('Defines the marker type for a pushpin document');
-    markerColor?: STRt = new StrInfo('Defines the marker color for a pushpin document');
-    map?: STRt = new StrInfo('text location of map');
+    markerType?: STRt = new StrInfo('marker type for a pushpin document');
+    markerColor?: STRt = new StrInfo('marker color for a pushpin document');
+    map?: STRt = new StrInfo('map location name');
     map_type?: STRt = new StrInfo('type of map view', false);
     map_zoom?: NUMt = new NumInfo('zoom of a map view', false);
     map_pitch?: NUMt = new NumInfo('pitch of a map view', false);
@@ -221,11 +221,11 @@ export class DocumentOptions {
     date_range?: STRt = new StrInfo('date range for calendar', false);
 
     wikiData?: STRt = new StrInfo('WikiData ID related to map location');
-    description?: STRt = new StrInfo('A description of the document');
-    _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false);
-    _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false);
-    _width?: NUMt = new NumInfo('displayed width of a document');
-    _height?: NUMt = new NumInfo('displayed height of document');
+    description?: STRt = new StrInfo('description of document');
+    _timecodeToShow?: NUMt = new NumInfo('media timecode when document should appear (e.g., when an annotation shows up as a video plays)', false);
+    _timecodeToHide?: NUMt = new NumInfo('media timecode when document should disappear', false);
+    _width?: NUMt = new NumInfo("width of document in container's coordinates");
+    _height?: NUMt = new NumInfo("height of document in container's coordiantes");
     data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false);
     data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false);
     linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false);
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 472d419fc..439d250de 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -28,6 +28,7 @@ import { Colors } from './global/globalEnums';
 import { MainViewModal } from './MainViewModal';
 import { ButtonType } from './nodes/FontIconBox/FontIconBox';
 import { ObservableReactComponent } from './ObservableReactComponent';
+import { dropActionType } from '../util/DragManager';
 
 enum DashboardGroup {
     MyDashboards,
@@ -403,7 +404,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _layout_fitWidth: true,
             _gridGap: 5,
             _forceActive: true,
-            childDragAction: 'embed',
+            childDragAction: dropActionType.embed,
             treeView_TruncateTitleWidth: 150,
             ignoreClick: true,
             contextMenuIcons: new List<string>(['plus']),
@@ -411,7 +412,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _lockedPosition: true,
             layout_boxShadow: '0 0',
             childDontRegisterViews: true,
-            dropAction: 'same',
+            dropAction: dropActionType.same,
             isSystem: true,
             layout_explainer: 'All of the calendars that you have created will appear here.',
         };
@@ -450,7 +451,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _layout_fitWidth: true,
             _gridGap: 5,
             _forceActive: true,
-            childDragAction: 'embed',
+            childDragAction: dropActionType.embed,
             treeView_TruncateTitleWidth: 150,
             ignoreClick: true,
             layout_headerButton: myTrailsBtn,
@@ -459,7 +460,7 @@ export class DashboardView extends ObservableReactComponent<{}> {
             _lockedPosition: true,
             layout_boxShadow: '0 0',
             childDontRegisterViews: true,
-            dropAction: 'same',
+            dropAction: dropActionType.same,
             isSystem: true,
             layout_explainer: 'All of the trails that you have created will appear here.',
         };
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5d9abb460..00195f7d7 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -105,7 +105,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
         const templateMenu: Array<JSX.Element> = [];
         this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />));
-        templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
+        templateMenu.push(<OtherToggle key="default" name={firstDoc.layout instanceof Doc ? StrCast(firstDoc.layout.title) : 'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
         addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template)));
         this._addedKeys &&
             Array.from(this._addedKeys)
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index dc391631a..ea1caf58f 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -233,10 +233,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
         return this._props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
     };
-    createRef = (ele: HTMLDivElement | null) => {
-        this._masonryGridRef = ele;
-        this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
-    };
 
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     @computed get onChildDoubleClickHandler() {
@@ -679,6 +675,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))));
 
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+    _oldWheel: any;
     render() {
         TraceMobx();
         const editableViewProps = {
@@ -699,7 +697,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 <div className="collectionStackingMasonry-cont">
                     <div
                         className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
-                        ref={this.createRef}
+                        ref={ele => {
+                            this._masonryGridRef = ele;
+                            this.createDashEventsTarget(ele); //so the whole grid is the drop target?
+                            this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                            this._oldWheel = ele;
+                            // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                            ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                        }}
                         style={{
                             overflowY: this.isContentActive() ? 'auto' : 'hidden',
                             background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor),
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index fdbd1cc90..59a695a3d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -219,12 +219,13 @@ export function CollectionSubView<X>(moreProps?: X) {
                 const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]);
                 const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
                 if (someMoved) docDragData.droppedDocuments = docDragData.droppedDocuments.map((drop, i) => (targetDocments.includes(docDragData.draggedDocuments[i]) ? docDragData.draggedDocuments[i] : drop));
-                if ((!dropAction || dropAction === 'inSame' || dropAction === 'same' || dropAction === 'move' || someMoved) && docDragData.moveDocument) {
+                if ((!dropAction || dropAction === dropActionType.inPlace || dropAction === dropActionType.same || dropAction === dropActionType.move || someMoved) && docDragData.moveDocument) {
                     const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
                     const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
                     if (movedDocs.length) {
                         const canAdd =
-                            (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) && (dropAction !== 'inSame' || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document));
+                            (de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.Document)) &&
+                            (dropAction !== dropActionType.inPlace || docDragData.draggedDocuments.every(d => d.embedContainer === this.Document));
                         const moved = docDragData.moveDocument(movedDocs, this.Document, canAdd ? this.addDocument : returnFalse);
                         added = canAdd || moved ? moved : undefined;
                     } else if (addedDocs.length) {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 4f2d8b9c0..285a789e6 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -449,10 +449,10 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
 
         const addDoc = inside ? this.localAdd : parentAddDoc;
         const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd;
-        if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
-            const move = (!dropAction || canEmbed || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument;
+        if (canAdd && (dropAction !== dropActionType.inPlace || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) {
+            const move = (!dropAction || canEmbed || dropAction === dropActionType.proto || dropAction === dropActionType.move || dropAction === dropActionType.same || dropAction === dropActionType.inPlace) && moveDocument;
             this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = true);
-            const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false);
+            const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === dropActionType.proto ? addDoc(d) : false) : addDoc(d)) || added, false);
             this._props.parentTreeView instanceof TreeView && (this._props.parentTreeView.dropping = false);
             return res;
         }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index fed4e89cf..ac0bd2378 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -38,91 +38,97 @@
                 background: yellow;
             }
         }
+    }
 
-        .schema-column-menu,
-        .schema-filter-menu {
-            background: $light-gray;
-            position: absolute;
-            min-width: 200px;
-            max-width: 400px;
-            display: flex;
-            flex-direction: column;
-            align-items: flex-start;
-            z-index: 1;
-
-            .schema-key-search-input {
-                width: calc(100% - 20px);
-                margin: 10px;
-            }
+    .schema-preview-divider {
+        height: 100%;
+        background: black;
+        cursor: ew-resize;
+    }
+}
 
-            .schema-search-result {
-                cursor: pointer;
-                padding: 5px 10px;
-                width: 100%;
+.schema-column-menu,
+.schema-filter-menu {
+    background: $light-gray;
+    position: relative;
+    min-width: 200px;
+    max-width: 400px;
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    z-index: 1;
 
-                &:hover {
-                    background-color: $medium-gray;
-                }
+    .schema-key-search-input {
+        width: calc(100% - 20px);
+        margin: 10px;
+    }
 
-                .schema-search-result-type,
-                .schema-search-result-desc {
-                    color: $dark-gray;
-                    font-size: $body-text;
-                }
-            }
+    .schema-search-result {
+        cursor: pointer;
+        padding: 5px 10px;
+        width: 100%;
 
-            .schema-key-search,
-            .schema-new-key-options {
-                width: 100%;
-                display: flex;
-                flex-direction: column;
-                align-items: flex-start;
-            }
+        &:hover {
+            background-color: $medium-gray;
+        }
+        .schema-search-result-type {
+            font-family: 'Courier New', Courier, monospace;
+        }
 
-            .schema-new-key-options {
-                margin: 10px;
-                .schema-key-warning {
-                    color: red;
-                    font-weight: normal;
-                    align-self: center;
-                }
-            }
+        .schema-search-result-type,
+        .schema-search-result-desc {
+            color: $dark-gray;
+            font-size: $body-text;
+        }
+        .schema-search-result-desc {
+            font-style: italic;
+        }
+    }
 
-            .schema-key-list {
-                width: 100%;
-                max-height: 300px;
-                overflow-y: auto;
-            }
+    .schema-key-search,
+    .schema-new-key-options {
+        width: 100%;
+        display: flex;
+        flex-direction: column;
+        align-items: flex-start;
+    }
 
-            .schema-key-type-option {
-                margin: 2px 0px;
+    .schema-new-key-options {
+        margin: 10px;
+        .schema-key-warning {
+            color: red;
+            font-weight: normal;
+            align-self: center;
+        }
+    }
 
-                input {
-                    margin-right: 5px;
-                }
-            }
+    .schema-key-list {
+        width: 100%;
+        max-height: 300px;
+        overflow-y: auto;
+    }
 
-            .schema-key-default-val {
-                margin: 5px 0;
-            }
+    .schema-key-type-option {
+        margin: 2px 0px;
 
-            .schema-column-menu-button {
-                cursor: pointer;
-                padding: 2px 5px;
-                background: $medium-blue;
-                border-radius: 9999px;
-                color: $white;
-                width: fit-content;
-                margin: 5px;
-                align-self: center;
-            }
+        input {
+            margin-right: 5px;
         }
     }
 
-    .schema-preview-divider {
-        height: 100%;
-        background: black;
-        cursor: ew-resize;
+    .schema-key-default-val {
+        margin: 5px 0;
+    }
+
+    .schema-column-menu-button {
+        cursor: pointer;
+        padding: 2px 5px;
+        background: $medium-blue;
+        border-radius: 9999px;
+        color: $white;
+        width: fit-content;
+        margin: 5px;
+        align-self: center;
     }
 }
 
@@ -133,6 +139,7 @@
     .row-menu {
         display: flex;
         justify-content: center;
+        cursor: pointer;
     }
 }
 
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index eee3836d2..12f0ad5e9 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, makeObservable, observable, ObservableMap, observe } from 'mobx';
+import { action, computed, makeObservable, observable, ObservableMap, observe, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Field, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
@@ -25,6 +25,8 @@ import { CollectionSubView } from '../CollectionSubView';
 import './CollectionSchemaView.scss';
 import { SchemaColumnHeader } from './SchemaColumnHeader';
 import { SchemaRowBox } from './SchemaRowBox';
+import { Popup, PopupTrigger, Type } from 'browndash-components';
+import { SettingsManager } from '../../../util/SettingsManager';
 const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
 
 export enum ColumnType {
@@ -161,7 +163,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         (change as any).added.forEach((doc: Doc) => // for each document added 
                             Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself)
                                 Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them
-                                    !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo(key, key === 'author'))))));
+                                    !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author'))))));
                         break;
                     case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
                 }
@@ -681,6 +683,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                         } else {
                             this.setKey(this._menuValue, this._newFieldDefault);
                         }
+                        this._columnMenuIndex = undefined;
                     })}>
                     done
                 </div>
@@ -688,12 +691,12 @@ export class CollectionSchemaView extends CollectionSubView() {
         );
     }
 
-    onPassiveWheel = (e: WheelEvent) => {
+    onKeysPassiveWheel = (e: WheelEvent) => {
         // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
-        if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
+        if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
         e.stopPropagation();
     };
-    _oldWheel: any;
+    _oldKeysWheel: any;
     @computed get keysDropdown() {
         return (
             <div className="schema-key-search">
@@ -708,9 +711,9 @@ export class CollectionSchemaView extends CollectionSubView() {
                 <div
                     className="schema-key-list"
                     ref={r => {
-                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                        this._oldWheel = r;
-                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                        this._oldKeysWheel?.removeEventListener('wheel', this.onKeysPassiveWheel);
+                        this._oldKeysWheel = r;
+                        r?.addEventListener('wheel', this.onKeysPassiveWheel, { passive: false });
                     }}>
                     {this._menuKeys.map(key => (
                         <div
@@ -721,14 +724,14 @@ export class CollectionSchemaView extends CollectionSubView() {
                             }}>
                             <p>
                                 <span className="schema-search-result-key">
-                                    {key}
-                                    {this.fieldInfos.get(key)!.fieldType ? ', ' : ''}
+                                    <b>{key}</b>
+                                    {this.fieldInfos.get(key)!.fieldType ? ':' : ''}
                                 </span>
                                 <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
                                     {this.fieldInfos.get(key)!.fieldType}
                                 </span>
+                                <span className="schema-search-result-desc">&nbsp;&nbsp;{this.fieldInfos.get(key)!.description}</span>
                             </p>
-                            <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</p>
                         </div>
                     ))}
                 </div>
@@ -740,16 +743,17 @@ export class CollectionSchemaView extends CollectionSubView() {
         const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
         return (
             <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
-                <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+                <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+                {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
+            </div>
+        );
+    }
+    get renderKeysMenu() {
+        console.log('RNDERMENUT:' + this._columnMenuIndex);
+        return (
+            <div className="schema-column-menu" style={{ left: 0, minWidth: CollectionSchemaView._minColWidth }}>
+                <input className="schema-key-search-input" type="text" onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
                 {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
-                <div
-                    className="schema-column-menu-button"
-                    onPointerDown={action(e => {
-                        e.stopPropagation();
-                        this.closeColumnMenu();
-                    })}>
-                    cancel
-                </div>
             </div>
         );
     }
@@ -833,6 +837,8 @@ export class CollectionSchemaView extends CollectionSubView() {
     isContentActive = () => this._props.isSelected() || this._props.isContentActive();
     screenToLocal = () => this.ScreenToLocalBoxXf().translate(-this.tableWidth, 0);
     previewWidthFunc = () => this.previewWidth;
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+    _oldWheel: any;
     render() {
         return (
             <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
@@ -841,15 +847,23 @@ export class CollectionSchemaView extends CollectionSubView() {
                     className="schema-table"
                     style={{ width: `calc(100% - ${this.previewWidth}px)` }}
                     onWheel={e => this._props.isContentActive() && e.stopPropagation()}
-                    ref={r => {
-                        // prevent wheel events from passively propagating up through containers
-                        r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
+                    ref={ele => {
+                        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                        (this._oldWheel = ele)?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                     }}>
                     <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
                         <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
-                            <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
-                                <FontAwesomeIcon icon="plus" />
-                            </div>
+                            <Popup
+                                placement="right"
+                                background={SettingsManager.userBackgroundColor}
+                                color={SettingsManager.userColor}
+                                toggle={<FontAwesomeIcon onPointerDown={e => this.openColumnMenu(-1, true)} icon="plus" />}
+                                trigger={PopupTrigger.CLICK}
+                                type={Type.TERT}
+                                isOpen={this._columnMenuIndex !== -1 ? false : undefined}
+                                popup={this.renderKeysMenu}
+                            />
                         </div>
                         {this.columnKeys.map((key, index) => (
                             <SchemaColumnHeader
@@ -870,7 +884,7 @@ export class CollectionSchemaView extends CollectionSubView() {
                             />
                         ))}
                     </div>
-                    {this._columnMenuIndex !== undefined && this.renderColumnMenu}
+                    {this._columnMenuIndex !== undefined && this._columnMenuIndex !== -1 && this.renderColumnMenu}
                     {this._filterColumnIndex !== undefined && this.renderFilterMenu}
                     <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
                     {this.layoutDoc.chromeHidden ? null : (
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 00e1f04c5..0a4325d8c 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -5,7 +5,7 @@ import { Utils, emptyFunction, setupMoveUpEvents } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { DragManager } from '../../util/DragManager';
+import { DragManager, dropActionType } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
 import { SelectionManager } from '../../util/SelectionManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
@@ -54,7 +54,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
             if (separation > 100) {
                 const dragData = new DragManager.DocumentDragData([this.Document]);
-                dragData.dropAction = 'embed';
+                dragData.dropAction = dropActionType.embed;
                 dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
                 DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
                 return true;
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index f6d94ce05..e38a42b29 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -11,7 +11,7 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { DocumentType } from '../../../documents/DocumentTypes';
 import { Docs } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { Presentation } from '../../../util/TrackMovements';
 import { undoBatch } from '../../../util/UndoManager';
@@ -251,7 +251,7 @@ ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: b
 
 ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) {
     if (DocCast(value.doc)) {
-        DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY);
+        DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], dropActionType.embed), value.e.clientX, value.e.clientY);
         value.e.preventDefault();
         return true;
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2d5239f06..4a1a51558 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -30,7 +30,7 @@ import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DictationManager } from '../../../util/DictationManager';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { MakeTemplate } from '../../../util/DropConverter';
 import { LinkManager } from '../../../util/LinkManager';
 import { RTFMarkup } from '../../../util/RTFMarkup';
@@ -624,7 +624,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         docId: draggedDoc[Id],
                         float: 'unset',
                     });
-                    if (!['embed', 'copy'].includes((dropAction ?? '') as any)) {
+                    if (![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) {
                         added = dragData.removeDocument?.(draggedDoc) ? true : false;
                     } else {
                         added = true;
-- 
cgit v1.2.3-70-g09d2


From a9eb266296d1b71f1016c867f39e20299c011eea Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 9 Mar 2024 23:08:15 -0500
Subject: added a template button to top bar to set default layout.  fixed show
 title when value is a list.  fixed typeahead for createing notes with
 templates.

---
 src/client/util/CurrentUserUtils.ts                |  7 ++++---
 src/client/views/GlobalKeyHandler.ts               |  3 +++
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/TemplateMenu.tsx                  | 13 -------------
 src/client/views/global/globalScripts.ts           | 19 +++++++++++++++++++
 src/client/views/nodes/DocumentView.tsx            |  2 +-
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 21 +++++++++++++++------
 7 files changed, 43 insertions(+), 23 deletions(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 564a46514..15255641d 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -124,11 +124,11 @@ export class CurrentUserUtils {
             { title: "Postit", backgroundColor: "yellow",    icon: "sticky-note"},
             { title: "Idea",   backgroundColor: "pink",      icon: "lightbulb" },
             { title: "Topic",  backgroundColor: "lightblue", icon: "book-open" }];
-        const reqdNoteList = reqdTempOpts.map(opts => {
+        const reqdNoteList = [...reqdTempOpts.map(opts => {
             const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true};
             const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.title === opts.title): undefined;
             return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts));
-        });
+        }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))];
 
         const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
         return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
@@ -788,7 +788,8 @@ pie title Minerals in my tap water
                     CollectionViewType.Grid, CollectionViewType.NoteTaking]),
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                                                                    ignoreClick: true, width: 100, scripts: { script:  'setView(value, _readOnly_)'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         expertMode: false,                                                                                                                             width: 30,  scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
-            { title: "Header",  icon: "heading",       toolTip: "Doc Titlebar Color",   btnType: ButtonType.ColorButton,         expertMode: true,                                                                                                           ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'} },
+            { title: "Header",  icon: "heading",       toolTip: "Doc Titlebar Color",   btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'} },
+            { title: "Template",icon: "scroll",        toolTip: "Default Note Template",btnType: ButtonType.ToggleButton,        expertMode: false, toolType:DocumentType.RTF,                                                                                                              scripts: { onClick: '{ return setDefaultTemplate(_readOnly_); }'} },
             { title: "Fill",    icon: "fill-drip",     toolTip: "Fill/Background Color",btnType: ButtonType.ColorButton,         expertMode: false,                                                                                                          ignoreClick: true, width: 30,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
             { title: "Overlay", icon: "layer-group",   toolTip: "Overlay",              btnType: ButtonType.ToggleButton,        expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode, true)'},       scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(this.toolType, this.expertMode)'}, width: 30,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 733383002..e800798ca 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -24,6 +24,7 @@ import { MainView } from './MainView';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { OpenWhereMod } from './nodes/DocumentView';
 import { AnchorMenu } from './pdf/AnchorMenu';
+import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 
 const modifiers = ['control', 'meta', 'shift', 'alt'];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo;
@@ -61,6 +62,8 @@ export class KeyManager {
     });
 
     public handle = action((e: KeyboardEvent) => {
+        // accumulate buffer of characters to insert in a new text note.  once the note is created, it will stop keyboard events from reaching this function.
+        if (FormattedTextBox.SelectOnLoadChar) FormattedTextBox.SelectOnLoadChar = FormattedTextBox.SelectOnLoadChar + (e.key === 'Enter' ? '\n' : e.key);
         e.key === 'Control' && (CtrlKey = true);
         //if (!Doc.noviceMode && e.key.toLocaleLowerCase() === "shift") DocServer.UPDATE_SERVER_CACHE(true);
         const keyname = e.key && e.key.toLowerCase();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3be52597a..e369e2876 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -397,6 +397,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faPaintBrush,
                 fa.faTimes,
                 fa.faFlag,
+                fa.faScroll,
                 fa.faEye,
                 fa.faArrowsAlt,
                 fa.faQuoteLeft,
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 00195f7d7..eed197b0b 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -45,7 +45,6 @@ class OtherToggle extends React.Component<{ checked: boolean; name: string; togg
 
 export interface TemplateMenuProps {
     docViews: DocumentView[];
-    templates?: Map<string, boolean>;
 }
 
 @observer
@@ -61,17 +60,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         this.props.docViews.map(dv => dv.switchViews(false, 'layout'));
     };
 
-    @undoBatch
-    @action
-    toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: string): void => {
-        this.props.docViews.forEach(d => (Doc.Layout(d.layoutDoc)['_show' + template] = event.target.checked ? template.toLowerCase() : ''));
-    };
-
-    @action
-    toggleTemplateActivity = (): void => {
-        this._hidden = !this._hidden;
-    };
-
     // todo: add brushes to brushMap to save with a style name
     onCustomKeypress = (e: React.KeyboardEvent) => {
         if (e.key === 'Enter') {
@@ -104,7 +92,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
         const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
         const templateMenu: Array<JSX.Element> = [];
-        this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />));
         templateMenu.push(<OtherToggle key="default" name={firstDoc.layout instanceof Doc ? StrCast(firstDoc.layout.title) : 'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
         addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template)));
         this._addedKeys &&
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 33704e8fe..0579b07c7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -21,6 +21,9 @@ import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { WebBox } from '../nodes/WebBox';
 import { VideoBox } from '../nodes/VideoBox';
 import { DocData } from '../../../fields/DocSymbols';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { PrefetchProxy } from '../../../fields/Proxy';
+import { MakeTemplate } from '../../util/DropConverter';
 
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
@@ -71,6 +74,22 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
     }
 });
 
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) {
+    if (checkResult) {
+        return Doc.UserDoc().defaultTextLayout;
+    }
+    const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined;
+
+    if (view) {
+        const tempDoc = view.Document;
+        if (!view.layoutDoc.isTemplateDoc) {
+            MakeTemplate(tempDoc);
+        }
+        Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc);
+        tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc);
+    } else Doc.UserDoc().defaultTextLayout = undefined;
+});
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
     if (checkResult) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 431781d24..29266bd8e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -849,7 +849,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         contents={
                             showTitle
                                 .split(';')
-                                .map(field => targetDoc[field.trim()]?.toString())
+                                .map(field => Field.toString(targetDoc[field.trim()] as Field))
                                 .join(' \\ ') || '-unset-'
                         }
                         display="block"
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index 46043eefb..f02ad7300 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { computed, makeObservable, observable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
@@ -312,6 +312,8 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
         );
     }
 
+    @observable _hackToRecompute = 0; // bcz: ugh ... <Toggle>'s toggleStatus initializes but doesn't track its value after a click.  so a click that does nothing to the toggle state will toggle the button anyway.  this forces the Toggle to re-read the ToggleStatus value.
+
     @computed get toggleButton() {
         // Determine the type of toggle button
         const buttonText = StrCast(this.dataDoc.buttonText);
@@ -319,7 +321,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
 
         const script = ScriptCast(this.Document.onClick);
         const double = ScriptCast(this.Document.onDoubleClick);
-        const toggleStatus = script ? script.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result : false;
+        const toggleStatus = script?.script.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result ?? false;
         // Colors
         const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
         const backgroundColor = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor);
@@ -336,10 +338,17 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
                 icon={this.Icon(color)!}
                 label={this.label}
                 onPointerDown={e =>
-                    setupMoveUpEvents(this, e, returnTrue, emptyFunction, (e, doubleTap) => {
-                        (!doubleTap || !double) && script.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
-                        doubleTap && double && double.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
-                    })
+                    setupMoveUpEvents(
+                        this,
+                        e,
+                        returnTrue,
+                        emptyFunction,
+                        action((e, doubleTap) => {
+                            (!doubleTap || !double) && script?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+                            doubleTap && double?.script.run({ this: this.Document, self: this.Document, value: !toggleStatus, _readOnly_: false });
+                            this._hackToRecompute = this._hackToRecompute + 1;
+                        })
+                    )
                 }
             />
         );
-- 
cgit v1.2.3-70-g09d2


From a974aa4e6573c8becf93f78610406747fec14c1c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 19 Mar 2024 17:08:46 -0400
Subject: cleaned up user templates to not get changed on reload.  made setting
 a template add it to the template tools list and as a tools button.   fixed
 linking to parts of a template.   fixed disappearing templates caused by
 stacking view set a field with an empty key.   updated field assignment
 syntax in trees, dash field views, and key value box to all use :,:=,=,=:=
 syntax.  added text elide button.  added @(title) syntax for hyperlinking. 
 made using a template both inherit from the template to get default values
 and use the template to render.    fixed submenu placement of context menu. 
 updated RTF markdown doc.

---
 src/client/util/CurrentUserUtils.ts                | 19 +++--
 src/client/util/DropConverter.ts                   | 45 +++++-----
 src/client/util/LinkManager.ts                     | 13 +--
 src/client/util/RTFMarkup.tsx                      | 30 +++----
 src/client/views/ContextMenuItem.tsx               |  2 +-
 src/client/views/TemplateMenu.scss                 |  1 +
 src/client/views/TemplateMenu.tsx                  |  7 +-
 .../collections/CollectionMasonryViewFieldRow.tsx  | 10 +--
 .../CollectionStackingViewFieldColumn.tsx          |  6 +-
 src/client/views/collections/TreeView.tsx          | 14 +++-
 src/client/views/global/globalScripts.ts           | 33 +++-----
 src/client/views/nodes/ComparisonBox.tsx           | 71 +++++++++-------
 src/client/views/nodes/DocumentView.tsx            | 32 ++++++-
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/FontIconBox/FontIconBox.tsx | 22 ++---
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 src/client/views/nodes/KeyValuePair.tsx            |  2 +-
 .../views/nodes/formattedText/DashFieldView.tsx    | 57 +++++++++----
 .../views/nodes/formattedText/FormattedTextBox.tsx | 34 ++++----
 .../views/nodes/formattedText/RichTextMenu.tsx     | 13 +++
 .../views/nodes/formattedText/RichTextRules.ts     | 97 ++++++++++++----------
 src/client/views/nodes/formattedText/marks_rts.ts  |  5 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |  2 +
 src/fields/Doc.ts                                  | 42 +++++-----
 src/fields/util.ts                                 |  4 +
 25 files changed, 328 insertions(+), 236 deletions(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 84a33500d..bbee18707 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -73,7 +73,7 @@ export class CurrentUserUtils {
         };
         const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
         const reqdFuncs = { /* hidden: "IsNoviceMode()"  */ };
-        return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
+        return DocUtils.AssignScripts(userDocTemplates ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs);
     }
 
     /// Initializes templates for editing click funcs of a document 
@@ -133,11 +133,19 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
         return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
     }
+    static setupUserTemplates(doc: Doc, field="template_user") {
+        const tempUsers = DocCast(doc[field]);
+        const reqdUserList = DocListCast(tempUsers?.data);
+
+        const reqdOpts:DocumentOptions = { title: "User Layouts", _height: 75, isSystem: true };
+        return DocUtils.AssignOpts(tempUsers, reqdOpts, reqdUserList) ?? (doc[field] = Docs.Create.TreeDocument(reqdUserList, reqdOpts));
+    }
 
     /// Initializes collection of templates for notes and click functions
     static setupDocTemplates(doc: Doc, field="myTemplates") {
         const templates = [
             CurrentUserUtils.setupNoteTemplates(doc),
+            CurrentUserUtils.setupUserTemplates(doc),
             CurrentUserUtils.setupClickEditorTemplates(doc)
         ];
         CurrentUserUtils.setupChildClickEditors(doc)
@@ -375,9 +383,9 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a flashcard",       title: "Flashcard", icon: "id-card",  dragFactory: doc.emptyFlashcard as Doc,  clickFactory: DocCast(doc.emptyFlashcard)},
             { toolTip: "Tap or drag to create an equation",       title: "Math", icon: "calculator",    dragFactory: doc.emptyEquation as Doc,   clickFactory: DocCast(doc.emptyEquation)},
             { toolTip: "Tap or drag to create a mermaid node",    title: "Mermaids", icon: "rocket",    dragFactory: doc.emptyMermaids as Doc,   clickFactory: DocCast(doc.emptyMermaids)}, 
-            { toolTip: "Tap or drag to create a plotly node",     title: "Plotly", icon: "rocket",      dragFactory: doc.emptyPlotly as Doc,   clickFactory: DocCast(doc.emptyMermaids)}, 
+            { toolTip: "Tap or drag to create a plotly node",     title: "Plotly", icon: "rocket",      dragFactory: doc.emptyPlotly as Doc,     clickFactory: DocCast(doc.emptyMermaids)}, 
             { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, 
-            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "book",       dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
+            { toolTip: "Tap or drag to create a note board",      title: "Notes", icon: "book",         dragFactory: doc.emptyNoteboard as Doc,  clickFactory: DocCast(doc.emptyNoteboard)},
             { toolTip: "Tap or drag to create a collection",      title: "Col", icon: "folder",         dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
             { toolTip: "Tap or drag to create a webpage",         title: "Web", icon: "globe-asia",     dragFactory: doc.emptyWebpage as Doc,    clickFactory: DocCast(doc.emptyWebpage)},
             { toolTip: "Tap or drag to create a comparison box",  title: "Compare", icon: "columns",    dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
@@ -385,10 +393,10 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a map",             title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc,        clickFactory: DocCast(doc.emptyMap)},
             { toolTip: "Tap or drag to create a screen grabber",  title: "Grab", icon: "photo-video",   dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc,     clickFactory: DocCast(doc.emptyWebCam),     openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
-            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "circle",        dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
+            { toolTip: "Tap or drag to create a button",          title: "Button", icon: "circle",      dragFactory: doc.emptyButton as Doc,     clickFactory: DocCast(doc.emptyButton)},
             { toolTip: "Tap or drag to create a scripting box",   title: "Script", icon: "terminal",    dragFactory: doc.emptyScript as Doc,     clickFactory: DocCast(doc.emptyScript),                                             funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data viz node",   title: "DataViz", icon: "chart-bar",  dragFactory: doc.emptyDataViz as Doc,    clickFactory: DocCast(doc.emptyDataViz)},
-            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard",     dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+            { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true,            funcs: { hidden: "IsNoviceMode()"} },
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
@@ -738,6 +746,7 @@ pie title Minerals in my tap water
                 { title: "Center",   toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
                 { title: "Right",    toolTip: "Right align (Cmd-])",  btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
             ]},
+            { title: "Elide",    toolTip: "Elide selection",      btnType: ButtonType.ToggleButton, icon: "eye",         toolType:"elide",     ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
             { title: "Dictate",  toolTip: "Dictate",              btnType: ButtonType.ToggleButton, icon: "microphone",  toolType:"dictation", ignoreClick: true,  scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}},
             { title: "NoLink",   toolTip: "Auto Link",            btnType: ButtonType.ToggleButton, icon: "link",        toolType:"noAutoLink", expertMode:true,   scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
             // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 3df3e36c6..ed5749d06 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -28,6 +28,7 @@ export function MakeTemplate(doc: Doc) {
 }
 
 /**
+ *
  * Recursively converts 'doc' into a template that can be used to render other documents.
  *
  * For recurive Docs in the template, their target fieldKey is defined by their title,
@@ -75,32 +76,36 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 remProps.map(prop => (dbox[prop] = undefined));
             }
         } else if (!doc.onDragStart && !doc.isButtonBar) {
-            const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
-            if (layoutDoc.type !== DocumentType.FONTICON) {
-                !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
-            }
-            layoutDoc.isTemplateDoc = true;
-            dbox = Docs.Create.FontIconDocument({
-                _nativeWidth: 100,
-                _nativeHeight: 100,
-                _width: 100,
-                _height: 100,
-                backgroundColor: StrCast(doc.backgroundColor),
-                title: StrCast(layoutDoc.title),
-                btnType: ButtonType.ClickButton,
-                icon: 'bolt',
-                isSystem: false,
-            });
-            dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
-            dbox.dragFactory = layoutDoc;
-            dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
-            dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
+            dbox = makeUserTemplateButton(doc);
         } else if (doc.isButtonBar) {
             dbox.ignoreClick = true;
         }
         data.droppedDocuments[i] = dbox;
     });
 }
+export function makeUserTemplateButton(doc: Doc) {
+    const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+    if (layoutDoc.type !== DocumentType.FONTICON) {
+        !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
+    }
+    layoutDoc.isTemplateDoc = true;
+    const dbox = Docs.Create.FontIconDocument({
+        _nativeWidth: 100,
+        _nativeHeight: 100,
+        _width: 100,
+        _height: 100,
+        backgroundColor: StrCast(doc.backgroundColor),
+        title: StrCast(layoutDoc.title),
+        btnType: ButtonType.ClickButton,
+        icon: 'bolt',
+        isSystem: false,
+    });
+    dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
+    dbox.dragFactory = layoutDoc;
+    dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
+    dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
+    return dbox;
+}
 ScriptingGlobals.add(
     function convertToButtons(dragData: any) {
         convertDropDataToButtons(dragData as DragManager.DocumentDragData);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 0c8d18a7a..cf16c4d6d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -58,8 +58,8 @@ export class LinkManager {
                                 link &&
                                     action(lAnchProtoProtos => {
                                         Doc.AddDocToList(Doc.UserDoc(), 'links', link);
-                                        lAnchs[0] && lAnchs[0][DocData][DirectLinks].add(link);
-                                        lAnchs[1] && lAnchs[1][DocData][DirectLinks].add(link);
+                                        lAnchs[0]?.[DocData][DirectLinks].add(link);
+                                        lAnchs[1]?.[DocData][DirectLinks].add(link);
                                     })
                             )
                         )
@@ -170,10 +170,11 @@ export class LinkManager {
             console.log('WAITING FOR DOC/PROTO IN LINKMANAGER');
             return [];
         }
-        const dirLinks = Doc.GetProto(anchor)[DirectLinks];
-        const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']);
-        if (!annos) debugger;
-        return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
+
+        const dirLinks = Array.from(anchor[DocData][DirectLinks]).filter(l => Doc.GetProto(anchor) === anchor[DocData] || ['1', '2'].includes(LinkManager.anchorIndex(l, anchor) as any));
+        const anchorRoot = DocCast(anchor.rootDocument, anchor); // template Doc fields store annotations on the topmost root of a template (not on themselves since the template layout items are only for layout)
+        const annos = DocListCast(anchorRoot[Doc.LayoutFieldKey(anchor) + '_annotations']);
+        return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks));
     }, true);
 
     // returns map of group type to anchor's links in that group type
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index f96d8a5df..315daad42 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -3,18 +3,12 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { MainViewModal } from '../views/MainViewModal';
 import { SettingsManager } from './SettingsManager';
-import { Doc } from '../../fields/Doc';
-import { StrCast } from '../../fields/Types';
 
 @observer
 export class RTFMarkup extends React.Component<{}> {
     static Instance: RTFMarkup;
     @observable private isOpen = false; // whether the SharingManager modal is open or not
 
-    // private get linkVisible() {
-    //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
-    // }
-
     @action
     public open = () => (this.isOpen = true);
 
@@ -39,6 +33,10 @@ export class RTFMarkup extends React.Component<{}> {
                     <b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
                     {` display wikipedia page for entered text (terminate with carriage return)`}
                 </p>
+                <p>
+                    <b style={{ fontSize: 'larger' }}>{`(( any text ))`}</b>
+                    {` submit text to Chat GPT to have results appended afterward`}
+                </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
                     {`  add hashtag metadata to document. e.g, #idea`}
@@ -47,10 +45,6 @@ export class RTFMarkup extends React.Component<{}> {
                     <b style={{ fontSize: 'larger' }}>{`#, ## ... ###### `}</b>
                     {`  set heading style based on number of '#'s between 1 and 6`}
                 </p>
-                <p>
-                    <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
-                    {`  add hashtag metadata to document. e.g, #idea`}
-                </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`>> `}</b>
                     {`  add a sidebar text document inline`}
@@ -61,7 +55,7 @@ export class RTFMarkup extends React.Component<{}> {
                 </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`cmd-f `}</b>
-                    {`  collapse to an inline footnote)`}
+                    {`  collapse to an inline footnote`}
                 </p>
                 <p>
                     <b style={{ fontSize: 'larger' }}>{`cmd-e `}</b>
@@ -116,20 +110,20 @@ export class RTFMarkup extends React.Component<{}> {
                     {`  start a block of text that begins with a hanging indent`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[:doctitle]] `}</b>
+                    <b style={{ fontSize: 'larger' }}>{`@(doctitle) `}</b>
                     {`  hyperlink to document specified by it’s title`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname]] `}</b>
-                    {`  display value of fieldname`}
+                    <b style={{ fontSize: 'larger' }}>{`[@(doctitle.)fieldname] `}</b>
+                    {`  display value of fieldname of text document (unless (doctitle.) is used to indicate another document by it's title)`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname=value]] `}</b>
-                    {`  assign value to fieldname of document and display it`}
+                    <b style={{ fontSize: 'larger' }}>{`[@fieldname:value] `}</b>
+                    {`  assign value to fieldname to data document and display it (if '=' is used instead of ':' the value is set on the layout Doc.  if value is wrapped in (()) then it will be sent to ChatGPT and the response will replace the value)`}
                 </p>
                 <p>
-                    <b style={{ fontSize: 'larger' }}>{`[[fieldname:doctitle]] `}</b>
-                    {`  show value of fieldname from doc specified by it’s title`}
+                    <b style={{ fontSize: 'larger' }}>{`[@fieldname:=expression] `}</b>
+                    {`  assign a computed expression to fieldname to data document and display it (if '=:=' is used instead of ':=' the expression is set on the layout Doc.  if value is wrapped in (()) then it will be sent to ChatGPT and the prompt/response will replace the value)`}
                 </p>
             </div>
         );
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index b2076e1a5..d15ab749c 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -113,7 +113,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
                 <div
                     className="contextMenu-subMenu-cont"
                     style={{
-                        marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%',
+                        marginLeft: window.innerWidth - this._overPosX - 50 > 0 ? '90%' : '20%',
                         marginTop,
                         background: SettingsManager.userBackgroundColor,
                     }}>
diff --git a/src/client/views/TemplateMenu.scss b/src/client/views/TemplateMenu.scss
index 4d0f1bf00..36a9ce6d0 100644
--- a/src/client/views/TemplateMenu.scss
+++ b/src/client/views/TemplateMenu.scss
@@ -39,6 +39,7 @@
     display: inline-block;
     height: 100%;
     width: 100%;
+    max-height: 250px;
 
     .templateToggle,
     .chromeToggle {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index eed197b0b..e7269df98 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,7 +1,8 @@
-import { action, computed, observable, ObservableSet, runInAction } from 'mobx';
+import { computed, observable, ObservableSet, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
@@ -9,12 +10,10 @@ import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, retu
 import { Docs, DocUtils } from '../documents/Documents';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
-import { undoBatch } from '../util/UndoManager';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { DocumentView, returnEmptyDocViewList } from './nodes/DocumentView';
 import { DefaultStyleProvider } from './StyleProvider';
 import './TemplateMenu.scss';
-import { DocData } from '../../fields/DocSymbols';
 
 @observer
 class TemplateToggle extends React.Component<{ template: string; checked: boolean; toggle: (event: React.ChangeEvent<HTMLInputElement>, template: string) => void }> {
@@ -99,7 +98,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 .filter(key => !noteTypes.some(nt => nt.title === key))
                 .forEach(template => templateMenu.push(<OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         return (
-            <ul className="template-list" style={{ display: 'block' }}>
+            <ul className="template-list">
                 {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />}
                 {templateMenu}
                 <CollectionTreeView
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 763d2e3a6..7dcfd32bd 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -100,9 +100,8 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         if (de.complete.docDragData) {
             const key = this._props.pivotField;
             const castedValue = this.getValue(this.heading);
-            const onLayoutDoc = this.onLayoutDoc(key);
             if (this._props.parent.onInternalDrop(e, de)) {
-                de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc));
+                key && de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !this.onLayoutDoc(key)));
             }
             return true;
         }
@@ -128,7 +127,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
                     return false;
                 }
             }
-            this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
+            key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, castedValue, true));
             this._heading = castedValue.toString();
             return true;
         }
@@ -155,10 +154,9 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         this._createEmbeddingSelected = false;
         const key = this._props.pivotField;
         const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
-        const onLayoutDoc = this.onLayoutDoc(key);
         FormattedTextBox.SetSelectOnLoad(newDoc);
         FormattedTextBox.SelectOnLoadChar = value;
-        (onLayoutDoc ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading);
+        key && ((this.onLayoutDoc(key) ? newDoc : newDoc[DocData])[key] = this.getValue(this._props.heading));
         const docs = this._props.parent.childDocList;
         return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
     };
@@ -167,7 +165,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF
         action(() => {
             this._createEmbeddingSelected = false;
             const key = this._props.pivotField;
-            this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
+            key && this._props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
             if (this._props.parent.colHeaderData && this._props.headingObject) {
                 const index = this._props.parent.colHeaderData.indexOf(this._props.headingObject);
                 this._props.parent.colHeaderData.splice(index, 1);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 6a3cb759e..641e01b81 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -112,7 +112,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
             if (this._props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
                 return false;
             }
-            this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue));
+            this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = castedValue));
             if (this._props.headingObject) {
                 this._props.headingObject.setHeading(castedValue.toString());
                 this._heading = this._props.headingObject.heading;
@@ -137,7 +137,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
         if (!value && !forceEmptyNote) return false;
         const key = this._props.pivotField;
         const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
-        newDoc[key] = this.getValue(this._props.heading);
+        key && (newDoc[key] = this.getValue(this._props.heading));
         const maxHeading = this._props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0);
         const heading = maxHeading === 0 || this._props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
         newDoc.heading = heading;
@@ -148,7 +148,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
 
     @action
     deleteColumn = () => {
-        this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
+        this._props.pivotField && this._props.docList.forEach(d => (d[this._props.pivotField] = undefined));
         if (this._props.colHeaderData && this._props.headingObject) {
             const index = this._props.colHeaderData.indexOf(this._props.headingObject);
             this._props.colHeaderData.splice(index, 1);
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 08c00d696..1b4349f44 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -574,11 +574,21 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
             <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}>
                 <EditableView
                     key="editableView"
-                    contents={'+key:value'}
+                    contents={'+key=value'}
                     height={13}
                     fontSize={12}
                     GetValue={returnEmptyString}
-                    SetValue={value => value.indexOf(':') !== -1 && KeyValueBox.SetField(doc, value.substring(0, value.indexOf(':')), value.substring(value.indexOf(':') + 1, value.length), true)}
+                    SetValue={input => {
+                        const match = input.match(/([a-zA-Z0-9_-]+)(=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]+)/);
+                        if (match) {
+                            const key = match[1];
+                            const assign = match[2];
+                            const val = match[3];
+                            KeyValueBox.SetField(doc, key, assign + val, false);
+                            return true;
+                        }
+                        return false;
+                    }}
                 />
             </div>
         );
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 0579b07c7..3fdc9a488 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,29 +1,26 @@
 import { Colors } from 'browndash-components';
 import { action, runInAction } from 'mobx';
+import { aggregateBounds } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { aggregateBounds } from '../../../Utils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
-import { undoable, UndoManager } from '../../util/UndoManager';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { UndoManager, undoable } from '../../util/UndoManager';
 import { GestureOverlay } from '../GestureOverlay';
 import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../InkingStroke';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
 // import { InkTranscription } from '../InkTranscription';
+import { DocData } from '../../../fields/DocSymbols';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from '../nodes/DocumentView';
-import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { WebBox } from '../nodes/WebBox';
 import { VideoBox } from '../nodes/VideoBox';
-import { DocData } from '../../../fields/DocSymbols';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
-import { PrefetchProxy } from '../../../fields/Proxy';
-import { MakeTemplate } from '../../util/DropConverter';
+import { WebBox } from '../nodes/WebBox';
+import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 
 ScriptingGlobals.add(function IsNoneSelected() {
     return SelectionManager.Views.length <= 0;
@@ -76,19 +73,7 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
 
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) {
-    if (checkResult) {
-        return Doc.UserDoc().defaultTextLayout;
-    }
-    const view = SelectionManager.Views.length === 1 && SelectionManager.Views[0].ComponentView instanceof FormattedTextBox ? SelectionManager.Views[0] : undefined;
-
-    if (view) {
-        const tempDoc = view.Document;
-        if (!view.layoutDoc.isTemplateDoc) {
-            MakeTemplate(tempDoc);
-        }
-        Doc.UserDoc().defaultTextLayout = new PrefetchProxy(tempDoc);
-        tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', tempDoc);
-    } else Doc.UserDoc().defaultTextLayout = undefined;
+    return DocumentView.setDefaultTemplate(checkResult);
 });
 // toggle: Set overlay status of selected document
 ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
@@ -197,7 +182,7 @@ ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highligh
     map.get(attr)?.setDoc?.();
 });
 
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
+type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'elide' | 'underline' | 'left' | 'center' | 'right' | 'vcent' | 'bullet' | 'decimal';
 type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
 
 ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
@@ -221,6 +206,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     const attrs:attrfuncs[] = [
         ['dictation', { checkResult: () => textView?._recordingDictation ? true:false,
                         toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }],
+        ['elide',      { checkResult: () => false,
+                        toggle: () => editorView ? RichTextMenu.Instance?.elideSelection(): 0}],
         ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
                         toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
         ['bold',      { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 715b23fb6..62f630c6c 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -159,6 +159,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
     remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
     remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
+
+    /**
+     * Tests for whether a comparison box slot (ie, before or after) has renderable text content
+     * @param whichSlot field key for start or end slot
+     * @returns a JSX layout string if a text field is found, othwerise undefined
+     */
+    testForTextFields = (whichSlot: string) => {
+        const slotHasText = Doc.Get(this.dataDoc, whichSlot, true) instanceof RichTextField || typeof Doc.Get(this.dataDoc, whichSlot, true) === 'string';
+        const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
+        const altText = RTFCast(this.Document[this.fieldKey + '_alternate'])?.Text.trim();
+        const layoutTemplateString =
+            slotHasText ? FormattedTextBox.LayoutString(whichSlot):
+            whichSlot.endsWith('1') ? (subjectText !== undefined ? FormattedTextBox.LayoutString(this.fieldKey) : undefined) :
+                                       altText !== undefined     ? FormattedTextBox.LayoutString(this.fieldKey + '_alternate'): undefined; // prettier-ignore
+
+        // A bit hacky to try out the concept of using GPT to fill in flashcards
+        //   If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
+        // and the fieldKey + "_alternate" has text that includes a GPT query (indicated by (( && )) ) that is parameterized (optionally) by the fieldKey text (this) or other metadata (this.<field>).
+        // eg., this.text_alternate is
+        //     "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
+        //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
+        // The GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
+        if (whichSlot.endsWith('2') && !layoutTemplateString?.includes(whichSlot)) {
+            var queryText = altText.replace('(this)', subjectText); // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
+            if (queryText && queryText.match(/\(\(.*\)\)/)) {
+                KeyValueBox.SetField(this.Document, whichSlot, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
+            }
+        }
+        return layoutTemplateString;
+    };
+
     _closeRef = React.createRef<HTMLDivElement>();
     render() {
         const clearButton = (which: string) => {
@@ -176,48 +207,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         /**
          * Display the Docs in the before/after fields of the comparison.  This also supports a GPT flash card use case
          * where if there are no Docs in the slots, but the main fieldKey contains text, then
-         * @param which
+         * @param whichSlot
          * @returns
          */
-        const displayDoc = (which: string) => {
-            const whichDoc = DocCast(this.dataDoc[which]);
+        const displayDoc = (whichSlot: string) => {
+            const whichDoc = DocCast(this.dataDoc[whichSlot]);
             const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
-            const subjectText = RTFCast(this.Document[this.fieldKey])?.Text.trim();
-            // if there is no Doc in the first comparison slot, but the comparison box's fieldKey slot has a RichTextField, then render a text box to show the contents of the document's field key slot
-            // of if there is no Doc in the second comparison slot, but the second slot has a RichTextField, then render a text box to show the contents of the document's field key slot
-            const layoutTemplateString = !targetDoc
-                ? which.endsWith('1') && subjectText !== undefined
-                    ? FormattedTextBox.LayoutString(this.fieldKey)
-                    : which.endsWith('2') && (this.Document[which] instanceof RichTextField || typeof this.Document[which] === 'string')
-                      ? FormattedTextBox.LayoutString(which)
-                      : undefined
-                : undefined;
-
-            // A bit hacky to try out the concept of using GPT to fill in flashcards -- this whole process should probably be packaged into a script to be more generic.
-            //   If the second slot doesn't have anything in it, but the fieldKey slot has text (e.g., this.text is a string)
-            // and the fieldKey + "_alternate" has text, then treat the _alternate's text as a GPT query (indicated by (( && )) ) that is parameterized (optionally)
-            // by the field references in the text (eg., this.text_alternate is
-            //     "((Provide a one sentence definition for (this) that doesn't use any word in (this.excludeWords) ))"
-            //     where (this) is replaced by the text in the fieldKey slot abd this.excludeWords is repalced by the conetnts of the excludeWords field
-            // A GPT call will put the "answer"  in the second slot of the comparison (eg., text_2)
-            if (which.endsWith('2') && !layoutTemplateString && !targetDoc) {
-                var queryText = RTFCast(this.Document[this.fieldKey + '_alternate'])
-                    ?.Text.replace('(this)', subjectText) // TODO: this should be done in KeyValueBox.setField but it doesn't know about the fieldKey ...
-                    .trim();
-                if (subjectText && queryText.match(/\(\(.*\)\)/)) {
-                    KeyValueBox.SetField(this.Document, which, ':=' + queryText, false); // make the second slot be a computed field on the data doc that calls ChatGpt
-                }
-            }
+            const layoutTemplateString = targetDoc ? '' : this.testForTextFields(whichSlot);
             return targetDoc || layoutTemplateString ? (
                 <>
                     <DocumentView
                         {...this._props}
+                        ignoreUsePath={layoutTemplateString ? true : undefined}
                         renderDepth={this.props.renderDepth + 1}
                         LayoutTemplateString={layoutTemplateString}
                         Document={layoutTemplateString ? this.Document : targetDoc}
                         containerViewPath={this.DocumentView?.().docViewPath}
-                        moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
-                        removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
+                        moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
+                        removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         isContentActive={emptyFunction}
@@ -227,7 +234,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         hideLinkButton={true}
                         pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
                     />
-                    {layoutTemplateString ? null : clearButton(which)}
+                    {layoutTemplateString ? null : clearButton(whichSlot)}
                 </> // placeholder image if doc is missing
             ) : (
                 <div className="placeholder">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9848f18e0..e9ce98583 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -10,6 +10,7 @@ import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fi
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
+import { PrefetchProxy } from '../../../fields/Proxy';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -19,10 +20,11 @@ import { DocServer } from '../../DocServer';
 import { Networking } from '../../Network';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
-import { DocOptions, DocUtils, Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
 import { FollowLinkScript } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -36,6 +38,7 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent, ViewBoxInterface } from '../DocComponent';
 import { EditableView } from '../EditableView';
+import { FieldsDropdown } from '../FieldsDropdown';
 import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
 import { AudioAnnoState, StyleProp } from '../StyleProvider';
@@ -47,7 +50,6 @@ import { KeyValueBox } from './KeyValueBox';
 import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
-import { FieldsDropdown } from '../FieldsDropdown';
 interface Window {
     MediaRecorder: MediaRecorder;
 }
@@ -1271,6 +1273,32 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined);
     }, 'set custom view');
 
+    public static setDefaultTemplate(checkResult?: boolean) {
+        if (checkResult) {
+            return Doc.UserDoc().defaultTextLayout;
+        }
+        const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
+        undoable(() => {
+            var tempDoc: Opt<Doc>;
+            if (view) {
+                if (!view.layoutDoc.isTemplateDoc) {
+                    tempDoc = view.Document;
+                    MakeTemplate(tempDoc);
+                    Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
+                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+                    tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
+                } else {
+                    tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
+                    if (!tempDoc) {
+                        tempDoc = view.Document;
+                        while (tempDoc && !Doc.isTemplateDoc(tempDoc)) tempDoc = DocCast(tempDoc.proto);
+                    }
+                }
+            }
+            Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined;
+        }, 'set default template')();
+    }
+
     /**
      * This switches between the current view of a Doc and a specified alternate layout view.
      * The current view of the Doc is stored in the layout_default field so that it can be restored.
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 4ecaaa283..5b47dd91d 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -55,6 +55,7 @@ export interface FieldViewSharedProps {
     ignoreAutoHeight?: boolean;
     disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
     hideClickBehaviors?: boolean; // whether to suppress menu item options for changing click behaviors
+    ignoreUsePath?: boolean; // ignore the usePath field for selecting the fieldKey (eg., on text docs)
     CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
     containerViewPath?: () => DocumentView[];
     fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index f02ad7300..57ae92359 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -5,8 +5,7 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
-import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { SelectionManager } from '../../../util/SelectionManager';
@@ -61,23 +60,12 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
     }
 
     @observable noTooltip = false;
-    showTemplate = (): void => {
-        const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
-        dragFactory && this._props.addDocTab(dragFactory, OpenWhere.addRight);
-    };
-    dragAsTemplate = (): void => {
-        this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
-    };
-    useAsPrototype = (): void => {
-        this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
-    };
 
+    showTemplate = (dragFactory: Doc) => this._props.addDocTab(dragFactory, OpenWhere.addRight);
     specificContextMenu = (): void => {
-        if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) {
-            const cm = ContextMenu.Instance;
-            cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
-            cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
-            cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
+        const dragFactory = DocCast(this.layoutDoc.dragFactory);
+        if (!Doc.noviceMode && dragFactory) {
+            ContextMenu.Instance.addItem({ description: 'Show Template', event: () => this.showTemplate(dragFactory), icon: 'tag' });
         }
     };
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2bcad806f..d85432631 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -115,7 +115,7 @@ export class KeyValueBox extends ObservableReactComponent<FieldViewProps> {
                 field === undefined && (field = res.result);
             }
         }
-        if (!key) return field;
+        if (!key) return false;
         if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
             target[key] = field;
             return true;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index d59489a78..f9e8ce4f3 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -125,7 +125,7 @@ export class KeyValuePair extends ObservableReactComponent<KeyValuePairProps> {
                                 pinToPres: returnZero,
                             }}
                             GetValue={() => Field.toKeyValueString(this._props.doc, this._props.keyName)}
-                            SetValue={(value: string) => (KeyValueBox.SetField(this._props.doc, this._props.keyName, value) ? true : false)}
+                            SetValue={(value: string) => KeyValueBox.SetField(this._props.doc, this._props.keyName, value)}
                         />
                     </div>
                 </td>
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 62cb460c2..6b66d829c 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
@@ -8,12 +8,12 @@ import { Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
 import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast } from '../../../../fields/Types';
+import { Cast, DocCast } from '../../../../fields/Types';
 import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
 import { FilterPanel } from '../../FilterPanel';
@@ -21,6 +21,7 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
+import { DocData } from '../../../../fields/DocSymbols';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -62,6 +63,8 @@ export class DashFieldView {
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
                 editable={node.attrs.editable}
+                expanded={node.attrs.expanded}
+                dataDoc={node.attrs.dataDoc}
                 tbox={tbox}
             />
         );
@@ -89,6 +92,8 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
+    expanded: boolean;
+    dataDoc: boolean;
     node: any;
     getPos: any;
     unclickable: () => boolean;
@@ -101,18 +106,19 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _fieldKey: string;
     _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
-    @observable _expanded = false;
+    @observable _expanded = this._props.expanded;
 
     constructor(props: IDashFieldViewInternal) {
         super(props);
         makeObservable(this);
         this._fieldKey = this._props.fieldKey;
-        this._textBoxDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc;
+        this._textBoxDoc = this._props.tbox.Document;
+        const setDoc = (doc: Doc) => (this._dashDoc = this._props.dataDoc ? doc[DocData] : doc);
 
         if (this._props.docId) {
-            DocServer.GetRefField(this._props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+            DocServer.GetRefField(this._props.docId).then(dashDoc => dashDoc instanceof Doc && setDoc(dashDoc));
         } else {
-            this._dashDoc = this._fieldKey.startsWith('_') ? this._props.tbox.Document : this._props.tbox.dataDoc;
+            setDoc(this._props.tbox.Document);
         }
     }
 
@@ -126,7 +132,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
-    return100 = () => 100;
+    isRowActive = () => this._expanded && this._props.editable;
+    finishEdit = action(() => (this._expanded = false));
+    selectedCell = (): [Doc, number] => [this._dashDoc!, 0];
 
     // set the display of the field's value (checkbox for booleans, span of text for strings)
     @computed get fieldValueContent() {
@@ -137,18 +145,18 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     col={0}
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
-                    maxWidth={this._props.hideKey ? undefined : this._props.tbox._props.PanelWidth}
-                    columnWidth={this._props.hideKey ? () => this._props.tbox._props.PanelWidth() - 20 : returnZero}
-                    selectedCell={() => [this._dashDoc!, 0]}
+                    maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
+                    columnWidth={returnZero}
+                    selectedCell={this.selectedCell}
                     fieldKey={this._fieldKey}
                     rowHeight={returnZero}
-                    isRowActive={() => this._expanded && this._props.editable}
+                    isRowActive={this.isRowActive}
                     padding={0}
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
                     allowCRs={true}
                     oneLine={!this._expanded}
-                    finishEdit={action(() => (this._expanded = false))}
+                    finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
                 />
@@ -173,11 +181,21 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
         }
     };
 
+    toggleFieldHide = undoable(
+        action(() => this._dashDoc && (this._dashDoc[this._fieldKey + '_hideKey'] = !this._dashDoc[this._fieldKey + '_hideKey'])),
+        'hideKey'
+    );
+
+    @computed get _hideKey() {
+        return this._dashDoc && this._dashDoc[this._fieldKey + '_hideKey'];
+    }
+
     // clicking on the label creates a pivot view collection of all documents
     // in the same collection.  The pivot field is the fieldKey of this label
     onPointerDownLabelSpan = (e: any) => {
         setupMoveUpEvents(this, e, returnFalse, returnFalse, e => {
             DashFieldViewMenu.createFieldView = this.createPivotForField;
+            DashFieldViewMenu.toggleFieldHide = this.toggleFieldHide;
             DashFieldViewMenu.Instance.show(e.clientX, e.clientY + 16, this._fieldKey);
         });
     };
@@ -188,6 +206,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     };
 
     @computed get values() {
+        if (this._props.expanded) return [];
         const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
 
         return vals.strings.map(facet => ({ value: facet, label: facet }));
@@ -203,9 +222,9 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     height: this._props.height,
                     pointerEvents: this._props.tbox._props.rootSelected?.() || this._props.tbox.isAnyChildContentActive?.() ? undefined : 'none',
                 }}>
-                {this._props.hideKey ? null : (
+                {this._props.hideKey || this._hideKey ? null : (
                     <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
-                        {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey}
+                        {(Doc.AreProtosEqual(DocCast(this._textBoxDoc.rootDocument) ?? this._textBoxDoc, DocCast(this._dashDoc?.rootDocument) ?? this._dashDoc) ? '' : this._dashDoc?.title + ':') + this._fieldKey}
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
@@ -224,6 +243,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
 export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
     static Instance: DashFieldViewMenu;
     static createFieldView: (e: React.MouseEvent) => void = emptyFunction;
+    static toggleFieldHide: () => void = emptyFunction;
     constructor(props: any) {
         super(props);
         DashFieldViewMenu.Instance = this;
@@ -233,6 +253,10 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
         DashFieldViewMenu.createFieldView(e);
         DashFieldViewMenu.Instance.fadeOut(true);
     };
+    toggleFieldHide = (e: React.MouseEvent) => {
+        DashFieldViewMenu.toggleFieldHide();
+        DashFieldViewMenu.Instance.fadeOut(true);
+    };
 
     @observable _fieldKey = '';
 
@@ -252,6 +276,9 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
                 <button className="antimodeMenu-button" onPointerDown={this.showFields}>
                     <FontAwesomeIcon icon="eye" size="lg" />
                 </button>
+                <button className="antimodeMenu-button" onPointerDown={this.toggleFieldHide}>
+                    <FontAwesomeIcon icon="bullseye" size="lg" />
+                </button>
             </Tooltip>
         );
     }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fb709818c..2b48494f2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -451,7 +451,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             var tr = this._editorView.state.tr as any;
             const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
             tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
-            Doc.MyPublishedDocs.forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
+            Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
             tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
             this._editorView?.dispatch(tr);
         }
@@ -958,8 +958,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
                 icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
             });
-        optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
         !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+        const help = cm.findByDescription('Help...');
+        const helpItems = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
+        !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
         this._downX = this._downY = Number.NaN;
     };
 
@@ -1239,8 +1242,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                             this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
-                    } else if (incomingValue?.str) {
-                        selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue.str)));
+                    } else {
+                        selectAll(this._editorView.state, tx => this._editorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
                     }
                 }
             }
@@ -1339,15 +1342,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
                     const dashField = view.state.schema.nodes.paragraph.create({}, [
-                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false, expanded: true }, undefined, [
                             view.state.schema.marks.linkAnchor.create({
                                 allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }],
-                                title: `from: ${DocCast(pdfAnchor.embedContainer).title}`,
+                                title: StrCast(pdfAnchor.title),
                                 noPreview: true,
-                                docref: false,
+                                docref: true,
+                                fontSize: '8px',
                             }),
-                            view.state.schema.marks.pFontSize.create({ fontSize: '8px' }),
-                            view.state.schema.marks.em.create({}),
                         ]),
                     ]);
 
@@ -1926,11 +1928,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             </div>
         );
     }
-    cycleAlternateText = () => {
-        if (this.layoutDoc._layout_enableAltContentUI) {
-            const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
-            this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
-        }
+    cycleAlternateText = (skipHover?: boolean) => {
+        this.layoutDoc._layout_enableAltContentUI = true;
+        const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
+        this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined;
     };
     @computed get overlayAlternateIcon() {
         const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
@@ -1965,7 +1966,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
     }
     get fieldKey() {
-        const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
+        return this._fieldKey;
+    }
+    @computed get _fieldKey() {
+        const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
         return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : '');
     }
     @observable _isHovering = false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index dc2c06701..bee0d72e3 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -318,6 +318,19 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         });
     }
 
+    elideSelection = () => {
+        const state = this.view?.state;
+        if (!state) return;
+        if (state.selection.empty) return false;
+        const mark = state.schema.marks.summarize.create();
+        const tr = state.tr;
+        tr.addMark(state.selection.from, state.selection.to, mark);
+        const content = tr.selection.content();
+        const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+        this.view?.dispatch?.(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+        return true;
+    };
+
     toggleNoAutoLinkAnchor = () => {
         if (this.view) {
             const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor);
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index c798ae4b3..b97141e92 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -267,7 +267,7 @@ export class RichTextRules {
 
             // toggle alternate text UI  %/
             new InputRule(new RegExp(/%\//), (state, match, start, end) => {
-                setTimeout(this.TextBox.cycleAlternateText);
+                setTimeout(() => this.TextBox.cycleAlternateText(true));
                 return state.tr.deleteRange(start, end);
             }),
 
@@ -283,40 +283,26 @@ export class RichTextRules {
                     : tr;
             }),
 
-            new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => {
-                var count = 0; // ignore first return value which will be the notation that chat is pending a result
-                KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
-                    count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string)));
-                    count++;
-                });
-                return null;
-            }),
-
-            // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
-            // [[<fieldKey> : <Doc>]]
-            // [[:docTitle]] => hyperlink
-            // [[fieldKey]] => show field
-            // [[fieldKey{:,=:}=value]] => show field and also set its value
-            // [[fieldKey:docTitle]] => show field of doc
-            new InputRule(
-                new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)((=:|:)?=)([a-z,A-Z_@\?+\-*/\ 0-9\(\)]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/),
-                (state, match, start, end) => {
-                    const fieldKey = match[1];
-                    const assign = match[2] === '=' ? '' : match[2];
-                    const value = match[4];
-                    const docTitle = match[5]?.replace(':', '');
+            // create a hyperlink to a titled document
+            // @(<doctitle>)
+            new InputRule(new RegExp(/(^|\s)@\(([a-zA-Z_@:\.\? \-0-9]+)\)/), (state, match, start, end) => {
+                const docTitle = match[2];
+                const prefixLength = '@('.length;
+                if (docTitle) {
                     const linkToDoc = (target: Doc) => {
-                        const rstate = this.TextBox.EditorView?.state;
-                        const selection = rstate?.selection.$from.pos;
-                        if (rstate) {
-                            this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+                        const editor = this.TextBox.EditorView;
+                        const selection = editor?.state?.selection.$from.pos;
+                        if (editor) {
+                            const estate = editor.state;
+                            editor.dispatch(estate.tr.setSelection(new TextSelection(estate.doc.resolve(start), estate.doc.resolve(end - prefixLength))));
                         }
 
                         DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
 
-                        const fstate = this.TextBox.EditorView?.state;
-                        if (fstate && selection) {
-                            this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+                        const teditor = this.TextBox.EditorView;
+                        if (teditor && selection) {
+                            const tstate = teditor.state;
+                            teditor.dispatch(tstate.tr.setSelection(new TextSelection(tstate.doc.resolve(selection))));
                         }
                     };
                     const getTitledDoc = (docTitle: string) => {
@@ -326,32 +312,57 @@ export class RichTextRules {
                         const titledDoc = DocServer.FindDocByTitle(docTitle);
                         return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc;
                     };
-                    if (!fieldKey) {
-                        if (docTitle) {
-                            const target = getTitledDoc(docTitle);
-                            if (target) {
-                                setTimeout(() => linkToDoc(target));
-                                return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
-                            }
-                        }
-                        return state.tr;
+                    const target = getTitledDoc(docTitle);
+                    if (target) {
+                        setTimeout(() => linkToDoc(target));
+                        return state.tr.insertText(' ').deleteRange(start, start + prefixLength);
                     }
+                }
+                return state.tr;
+            }),
+
+            // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
+            // [@{this,doctitle,}.fieldKey{:,=,:=,=:=}value]
+            // [@{this,doctitle,}.fieldKey]
+            new InputRule(
+                new RegExp(/\[(@|@this\.|@[a-zA-Z_\? \-0-9]+\.)([a-zA-Z_\?\-0-9]+)((:|=|:=|=:=)([a-zA-Z,_@\?\+\-\*\/\ 0-9\(\)]*))?\]/),
+                (state, match, start, end) => {
+                    const docTitle = match[1].substring(1).replace(/\.$/, '');
+                    const fieldKey = match[2];
+                    const assign = match[4] === ':' ? (match[4] = '') : match[4];
+                    const value = match[5];
+                    const dataDoc = value === undefined ? !fieldKey.startsWith('_') : !assign?.startsWith('=');
+                    const getTitledDoc = (docTitle: string) => {
+                        if (!DocServer.FindDocByTitle(docTitle)) {
+                            Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true }));
+                        }
+                        return DocServer.FindDocByTitle(docTitle);
+                    };
                     // if the value has commas assume its an array (unless it's part of a chat gpt call indicated by '((' )
                     if (value?.includes(',') && !value.startsWith('((')) {
                         const values = value.split(',');
                         const strs = values.some(v => !v.match(/^[-]?[0-9.]$/));
                         this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v)));
                     } else if (value) {
-                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign ? undefined: 
-                            (gptval: FieldResult) => this.Document[DocData][fieldKey] = gptval as string ); // prettier-ignore
+                        KeyValueBox.SetField(this.Document, fieldKey, assign + value, Doc.IsDataProto(this.Document) ? true : undefined, assign.includes(":=") ? undefined:
+                            (gptval: FieldResult) => (dataDoc ? this.Document[DocData]:this.Document)[fieldKey] = gptval as string ); // prettier-ignore
                     }
-                    const target = getTitledDoc(docTitle);
-                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
+                    const target = docTitle ? getTitledDoc(docTitle) : undefined;
+                    const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false, dataDoc });
                     return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
                 },
                 { inCode: true }
             ),
 
+            new InputRule(new RegExp(/(^|[^=])(\(\(.*\)\))/), (state, match, start, end) => {
+                var count = 0; // ignore first return value which will be the notation that chat is pending a result
+                KeyValueBox.SetField(this.Document, '', match[2], false, (gptval: FieldResult) => {
+                    count && this.TextBox.EditorView?.dispatch(this.TextBox.EditorView!.state.tr.insertText(' ' + (gptval as string)));
+                    count++;
+                });
+                return null;
+            }),
+
             // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
             // wiki:title
             new InputRule(new RegExp(/wiki:([a-zA-Z_@:\.\?\-0-9]+ )$/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index a141ef041..b68acc8f8 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -74,6 +74,7 @@ export const marks: { [index: string]: MarkSpec } = {
             allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
             title: { default: null },
             noPreview: { default: false },
+            fontSize: { default: null },
             docref: { default: false }, // flags whether the linked text comes from a document within Dash.  If so, an attribution label is appended after the text
         },
         inclusive: false,
@@ -93,14 +94,16 @@ export const marks: { [index: string]: MarkSpec } = {
             const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
             return node.attrs.docref && node.attrs.title
                 ? [
-                      'div',
+                      'a',
                       ['span', 0],
                       [
                           'span',
                           {
                               ...node.attrs,
                               class: 'prosemirror-attribution',
+                              'data-targethrefs': targethrefs,
                               href: node.attrs.allAnchors[0].href,
+                              style: `font-size: ${node.attrs.fontSize}`,
                           },
                           node.attrs.title,
                       ],
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index c9115be90..905146ee2 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -265,6 +265,8 @@ export const nodes: { [index: string]: NodeSpec } = {
             docId: { default: '' },
             hideKey: { default: false },
             editable: { default: true },
+            expanded: { default: null },
+            dataDoc: { default: false },
         },
         leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field),
         group: 'inline',
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index daae32e9f..4b40d11b9 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -445,6 +445,12 @@ export namespace Doc {
     export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
         return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
     }
+    export function isTemplateDoc(doc: Doc) {
+        return GetT(doc, 'isTemplateDoc', 'boolean', true);
+    }
+    export function isTemplateForField(doc: Doc) {
+        return GetT(doc, 'isTemplateForField', 'string', true);
+    }
     export function IsDataProto(doc: Doc) {
         return GetT(doc, 'isDataDoc', 'boolean', true);
     }
@@ -642,7 +648,7 @@ export namespace Doc {
         cloneLinks: boolean,
         cloneTemplates: boolean
     ): Promise<Doc> {
-        if (Doc.IsBaseProto(doc) || ((Doc.Get(doc, 'isTemplateDoc', true) || Doc.Get(doc, 'isTemplateForField', true)) && !cloneTemplates)) {
+        if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) {
             return doc;
         }
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -735,7 +741,7 @@ export namespace Doc {
                 const docAtKey = DocCast(clone[key]);
                 if (docAtKey && !Doc.IsSystem(docAtKey)) {
                     if (!Array.from(cloneMap.values()).includes(docAtKey)) {
-                        clone[key] = !cloneTemplates && (Doc.Get(docAtKey, 'isTemplateDoc', true) || Doc.Get(docAtKey, 'isTemplateForField', true)) ? docAtKey : cloneMap.get(docAtKey[Id]);
+                        clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]);
                     } else {
                         repairClone(docAtKey, cloneMap, cloneTemplates, visited);
                     }
@@ -857,7 +863,7 @@ export namespace Doc {
     // of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
     export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) {
         // nothing to do if the layout isn't a template or we don't have a target that's different than the template
-        if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) {
+        if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) {
             return templateLayoutDoc;
         }
 
@@ -874,7 +880,7 @@ export namespace Doc {
             expandedTemplateLayout = undefined;
             _pendingMap.add(targetDoc[Id] + expandedLayoutFieldKey);
         } else if (expandedTemplateLayout === undefined && !_pendingMap.has(targetDoc[Id] + expandedLayoutFieldKey)) {
-            if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument ?? Doc.GetProto(targetDoc))) {
+            if (templateLayoutDoc.resolvedDataDoc === targetDoc[DocData]) {
                 expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
             } else {
                 templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype
@@ -910,8 +916,9 @@ export namespace Doc {
             console.log('Warning: GetLayoutDataDocPair childDoc not defined');
             return { layout: childDoc, data: childDoc };
         }
-        const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc;
-        return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
+        const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc;
+        const templateRoot = DocCast(containerDoc?.rootDocument);
+        return { layout: Doc.expandTemplateLayout(childDoc, templateRoot), data: resolvedDataDoc };
     }
 
     export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
@@ -1035,20 +1042,13 @@ export namespace Doc {
     // (ie, the 'data' doc), and then creates another delegate of that (ie, the 'layout' doc).
     // This is appropriate if you're trying to create a document that behaves like all
     // regularly created documents (e.g, text docs, pdfs, etc which all have data/layout docs)
-    export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string): Doc {
-        const delegateProto = new Doc();
-        delegateProto[Initializing] = true;
-        delegateProto.proto = doc;
-        delegateProto.author = Doc.CurrentUserEmail;
-        delegateProto.isDataDoc = true;
-        title && (delegateProto.title = title);
-        const delegate = new Doc(id, true);
-        delegate[Initializing] = true;
-        delegate.proto = delegateProto;
-        delegate.author = Doc.CurrentUserEmail;
-        delegate[Initializing] = false;
-        delegateProto[Initializing] = false;
-        return delegate;
+    export function MakeDelegateWithProto(doc: Doc, id?: string, title?: string) {
+        const ndoc = Doc.ApplyTemplate(doc);
+        if (ndoc) {
+            Doc.GetProto(ndoc).isDataDoc = true;
+            ndoc && (Doc.GetProto(ndoc).proto = doc);
+        }
+        return ndoc;
     }
 
     let _applyCount: number = 0;
@@ -1671,7 +1671,7 @@ ScriptingGlobals.add(function getEmbedding(doc: any) {
     return Doc.MakeEmbedding(doc);
 });
 ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) {
-    return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto);
+    return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto);
 });
 ScriptingGlobals.add(function copyField(field: any) {
     return Field.Copy(field);
diff --git a/src/fields/util.ts b/src/fields/util.ts
index b73520999..c2ec3f13a 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -286,6 +286,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
 // target should be either a Doc or ListImpl.  receiver should be a Proxy<Doc> Or List.
 //
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
+    if (!in_prop) {
+        console.log('WARNING: trying to set an empty property.  This should be fixed. ');
+        return false;
+    }
     let prop = in_prop;
     const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
-- 
cgit v1.2.3-70-g09d2


From bf6d1973cc81ba695afcca102c7229608faaa7e6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 27 Mar 2024 11:02:57 -0400
Subject: changed dashFieldViews to support Tab'ing between other
 dashFieldviews, changed deleting links to clear out the anchors so that
 linkBoxes will go away more easiliy.  changed funcitonPlot to plot the
 equations that are linked to it.  changed equations to link to functions.
 changed undo and other console logging to only happen when undo docked
 buttons are expanded (visible)

---
 src/client/DocServer.ts                            |  4 +-
 src/client/util/LinkManager.ts                     | 12 +++---
 src/client/util/UndoManager.ts                     | 21 ++++-----
 src/client/views/EditableView.tsx                  |  1 -
 src/client/views/PreviewCursor.tsx                 | 12 +++---
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/TemplateMenu.tsx                  |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  2 +-
 src/client/views/nodes/EquationBox.tsx             |  6 ++-
 src/client/views/nodes/FunctionPlotBox.tsx         | 26 ++++++++---
 src/client/views/nodes/LinkBox.tsx                 | 10 +++--
 src/client/views/nodes/LinkDescriptionPopup.tsx    |  6 ++-
 src/client/views/nodes/ScriptingBox.tsx            |  4 +-
 .../views/nodes/formattedText/DashFieldView.tsx    | 50 ++++++++++++++++------
 .../views/nodes/formattedText/EquationView.tsx     |  7 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 ++--
 .../views/nodes/formattedText/RichTextRules.ts     |  6 +--
 src/debug/Viewer.tsx                               |  4 +-
 19 files changed, 117 insertions(+), 70 deletions(-)

(limited to 'src/client/views/TemplateMenu.tsx')

diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 6217cf04b..bd60a205c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -65,10 +65,10 @@ export namespace DocServer {
         cacheDocumentIds = newCacheUpdate;
 
         // print out cached docs
-        console.log('Set cached docs = ');
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('Set cached docs = ');
         const is_filtered = filtered.filter(doc => !Doc.IsSystem(doc));
         const strings = is_filtered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)'));
-        strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
+        Doc.MyDockedBtns.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
 
         rp.post(Utils.prepend('/setCacheDocumentIds'), {
             body: {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index cf16c4d6d..608e05fe9 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -152,10 +152,12 @@ export class LinkManager {
         }
     }
     public deleteLink(linkDoc: Doc) {
-        return Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc);
+        const ret = Doc.RemoveDocFromList(Doc.LinkDBDoc(), 'data', linkDoc);
+        linkDoc[DocData].link_anchor_1 = linkDoc[DocData].link_anchor_2 = undefined;
+        return ret;
     }
     public deleteAllLinksOnAnchor(anchor: Doc) {
-        LinkManager.Instance.relatedLinker(anchor).forEach(linkDoc => LinkManager.Instance.deleteLink(linkDoc));
+        LinkManager.Instance.relatedLinker(anchor).forEach(LinkManager.Instance.deleteLink);
     }
 
     public getAllRelatedLinks(anchor: Doc) {
@@ -209,10 +211,10 @@ export class LinkManager {
         const a1 = DocCast(linkDoc.link_anchor_1);
         const a2 = DocCast(linkDoc.link_anchor_2);
         if (linkDoc.link_matchEmbeddings) {
-            return [a2, a2.annotationOn].includes(anchor) ? '2' : '1';
+            return [a2, a2?.annotationOn].includes(anchor) ? '2' : '1';
         }
-        if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1';
-        if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2';
+        if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a1?.annotationOn, a1))) return '1';
+        if (Doc.AreProtosEqual(DocCast(anchor?.annotationOn, anchor), DocCast(a2?.annotationOn, a2))) return '2';
         if (Doc.AreProtosEqual(anchor, linkDoc)) return '0';
 
         // const a1 = DocCast(linkDoc.link_anchor_1);
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 421855bf3..857ca852f 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,5 +1,5 @@
 import { observable, action, runInAction } from 'mobx';
-import { Field } from '../../fields/Doc';
+import { Doc, Field } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
 import { Without } from '../../Utils';
 
@@ -97,13 +97,14 @@ export namespace UndoManager {
 
     export function AddEvent(event: UndoEvent, value?: any): void {
         if (currentBatch && batchCounter.get() && !undoing) {
-            console.log(
-                '                         '.slice(0, batchCounter.get()) +
-                    'UndoEvent : ' +
-                    event.prop +
-                    ' = ' +
-                    (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value))
-            );
+            Doc.MyDockedBtns.linearView_IsOpen &&
+                console.log(
+                    '                         '.slice(0, batchCounter.get()) +
+                        'UndoEvent : ' +
+                        event.prop +
+                        ' = ' +
+                        (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value))
+                );
             currentBatch.push(event);
             tempEvents?.push(event);
         }
@@ -171,7 +172,7 @@ export namespace UndoManager {
     }
 
     export function StartBatch(batchName: string): Batch {
-        console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
         runInAction(() => batchCounter.set(batchCounter.get() + 1));
         if (currentBatch === undefined) {
             currentBatch = [];
@@ -181,7 +182,7 @@ export namespace UndoManager {
 
     const EndBatch = action((batchName: string, cancel: boolean = false) => {
         runInAction(() => batchCounter.set(batchCounter.get() - 1));
-        console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+        Doc.MyDockedBtns.linearView_IsOpen && console.log('                          '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
         if (batchCounter.get() === 0 && currentBatch?.length) {
             if (!cancel) {
                 undoStack.push(currentBatch);
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 4508d00a7..44c8e4e1f 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -232,7 +232,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> {
                     onPointerDown: this.stopPropagation,
                     onClick: this.stopPropagation,
                     onPointerUp: this.stopPropagation,
-                    onKeyPress: this.stopPropagation,
                     value: this._props.autosuggestProps.value,
                     // @ts-ignore
                     onChange: this._props.autosuggestProps.onChange,
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index a94c18295..4b7771f27 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -18,7 +18,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         return PreviewCursor._instance;
     }
 
-    _onKeyPress?: (e: KeyboardEvent) => void;
+    _onKeyDown?: (e: KeyboardEvent) => void;
     _getTransform?: () => Transform;
     _addDocument?: (doc: Doc | Doc[]) => boolean;
     _addLiveTextDoc?: (doc: Doc) => void;
@@ -32,7 +32,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         makeObservable(this);
         PreviewCursor._instance = this;
         this._clickPoint = observable([0, 0]);
-        document.addEventListener('keydown', this.onKeyPress);
+        document.addEventListener('keydown', this.onKeyDown);
         document.addEventListener('paste', this.paste, true);
     }
 
@@ -120,7 +120,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     };
 
     @action
-    onKeyPress = (e: KeyboardEvent) => {
+    onKeyDown = (e: KeyboardEvent) => {
         // Mixing events between React and Native is finicky.
         //if not these keys, make a textbox if preview cursor is active!
         if (
@@ -146,7 +146,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         ) {
             if ((!e.metaKey && !e.ctrlKey) || (e.keyCode >= 48 && e.keyCode <= 57) || (e.keyCode >= 65 && e.keyCode <= 90)) {
                 //  /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
-                this.Visible && this._onKeyPress?.(e);
+                this.Visible && this._onKeyDown?.(e);
                 ((!e.ctrlKey && !e.metaKey) || e.key !== 'v') && (this.Visible = false);
             }
         } else if (this.Visible) {
@@ -171,7 +171,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
     public static Show(
         x: number,
         y: number,
-        onKeyPress: (e: KeyboardEvent) => void,
+        onKeyDown: (e: KeyboardEvent) => void,
         addLiveText: (doc: Doc) => void,
         getTransform: () => Transform,
         addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
@@ -181,7 +181,7 @@ export class PreviewCursor extends ObservableReactComponent<{}> {
         const self = PreviewCursor.Instance;
         if (self) {
             self._clickPoint = [x, y];
-            self._onKeyPress = onKeyPress;
+            self._onKeyDown = onKeyDown;
             self._addLiveTextDoc = addLiveText;
             self._getTransform = getTransform;
             self._addDocument = addDocument || returnFalse;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 195b1a04c..0de44b62b 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -715,7 +715,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     value={value}
                     style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
                     onChange={e => setter(e.target.value)}
-                    onKeyPress={e => e.stopPropagation()}
+                    onKeyDown={e => e.stopPropagation()}
                 />
                 <div className="inputBox-button">
                     <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e7269df98..5df0bea1a 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -99,7 +99,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
                 .forEach(template => templateMenu.push(<OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
         return (
             <ul className="template-list">
-                {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress} />}
+                {Doc.noviceMode ? null : <input placeholder="+ layout" ref={this._customRef} onKeyDown={this.onCustomKeypress} />}
                 {templateMenu}
                 <CollectionTreeView
                     Document={Doc.MyTemplates}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cb593a993..6b3a56b0b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -101,7 +101,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     @action
-    onKeyPress = (e: KeyboardEvent) => {
+    onKeyDown = (e: KeyboardEvent) => {
         //make textbox and add it to this collection
         // tslint:disable-next-line:prefer-const
         const cm = ContextMenu.Instance;
@@ -311,7 +311,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             const effectiveAcl = GetEffectiveAcl(this._props.Document[DocData]);
             if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
                 PreviewCursor.Instance.Doc = doc;
-                PreviewCursor.Show(x, y, this.onKeyPress, this._props.addLiveTextDocument, this._props.getTransform, this._props.addDocument, this._props.nudge, this._props.slowLoadDocuments);
+                PreviewCursor.Show(x, y, this.onKeyDown, this._props.addLiveTextDocument, this._props.getTransform, this._props.addDocument, this._props.nudge, this._props.slowLoadDocuments);
             }
             this.clearSelection();
         }
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 028d3da53..60def5d45 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                       ? DocCast(linkDoc.link_anchor_2)
                       : DocCast(linkDoc.link_anchor_1)
                   : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) ||
-                    LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null));
+                    LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null)?.annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null));
             return !destDoc || !sourceDoc ? null : (
                 <LinkMenuItem
                     key={linkDoc[Id]}
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index 50d4c7c78..8362db37a 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { LightboxView } from '../LightboxView';
@@ -80,7 +80,9 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 _height: 300,
                 backgroundColor: 'white',
             });
-            this._props.addDocument?.(graph);
+            const link = DocUtils.MakeLink(this.Document, graph, { link_relationship: 'function', link_description: 'input' });
+            //this._props.addDocument?.(graph);
+            link && this._props.addDocument?.(link);
             e.stopPropagation();
         }
         if (e.key === 'Backspace' && !this.dataDoc.text) this._props.removeDocument?.(this.Document);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 2e7a2120e..67445e552 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -7,12 +7,13 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { Cast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { Docs } from '../../documents/Documents';
+import { DocUtils, Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from './FieldView';
 import { PinProps, PresBox } from './trails';
+import { LinkManager } from '../../util/LinkManager';
 
 @observer
 export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@@ -33,7 +34,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     componentDidMount() {
         this._props.setContentViewBox?.(this);
         reaction(
-            () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
+            () => [this.graphFuncs, this.layoutDoc.width, this.layoutDoc.height, this.layoutDoc.xRange, this.layoutDoc.yRange],
             () => this.createGraph()
         );
     }
@@ -45,11 +46,17 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
         if (addAsAnnotation) this.addDocument(anchor);
         return anchor;
     };
+    @computed get graphFuncs() {
+        const links = LinkManager.Instance.getAllRelatedLinks(this.Document)
+            .map(d => LinkManager.getOppositeAnchor(d, this.Document))
+            .filter(d => d)
+            .map(d => d!);
+        return links.concat(DocListCast(this.dataDoc[this.fieldKey])).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
+    }
     createGraph = (ele?: HTMLDivElement) => {
         this._plotEle = ele || this._plotEle;
         const width = this._props.PanelWidth();
         const height = this._props.PanelHeight();
-        const fns = DocListCast(this.dataDoc.data).map(doc => StrCast(doc.text, 'x^2').replace(/\\frac\{(.*)\}\{(.*)\}/, '($1/$2)'));
         try {
             this._plotEle.children.length && this._plotEle.removeChild(this._plotEle.children[0]);
             this._plot = functionPlot({
@@ -59,7 +66,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
                 xAxis: { domain: Cast(this.layoutDoc.xRange, listSpec('number'), [-10, 10]) },
                 yAxis: { domain: Cast(this.layoutDoc.yRange, listSpec('number'), [-1, 9]) },
                 grid: true,
-                data: fns.map(fn => ({
+                data: this.graphFuncs.map(fn => ({
                     fn,
                     // derivative: { fn: "2 * x", updateOnMouseMove: true }
                 })),
@@ -72,7 +79,14 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     @undoBatch
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.complete.docDragData?.droppedDocuments.length) {
-            const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc), true);
+            const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => {
+                ///const ret = res && Doc.AddDocToList(this.dataDoc, this._props.fieldKey, doc);
+                if (res) {
+                    const link = DocUtils.MakeLink(doc, this.Document, { link_relationship: 'function', link_description: 'input' });
+                    link && this._props.addDocument?.(link);
+                }
+                return res;
+            }, true);
             !added && e.preventDefault();
             e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
             return added;
@@ -104,7 +118,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
                 {this.theGraph}
                 <div
                     style={{
-                        display: this._props.isSelected() ? 'none' : undefined,
+                        display: this._props.isContentActive() ? 'none' : undefined,
                         position: 'absolute',
                         width: '100%',
                         height: '100%',
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 0809e2ad6..8ebcefb35 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -23,7 +23,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string = 'link') {
         return FieldView.LayoutString(LinkBox, fieldKey);
     }
-    _disposer: IReactionDisposer | undefined;
+    _disposers: { [name: string]: IReactionDisposer } = {};
     @observable _forceAnimate: number = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor
     @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor
 
@@ -40,11 +40,15 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement());
     };
     componentWillUnmount() {
-        this._disposer?.();
+        Object.keys(this._disposers).forEach(key => this._disposers[key]());
     }
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._disposer = reaction(
+        this._disposers.deleting = reaction(
+            () => !this.anchor1 || !this.anchor2,
+            empty => empty && this._props.removeDocument?.(this.Document)
+        );
+        this._disposers.dragging = reaction(
             () => ({ drag: SnappingManager.IsDragging }),
             ({ drag }) => {
                 !LightboxView.Contains(this.DocumentView?.()) &&
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 1645d0813..2a96ce458 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -69,8 +69,10 @@ export class LinkDescriptionPopup extends React.Component<{}> {
                 }}>
                 <input
                     className="linkDescriptionPopup-input"
-                    onKeyDown={e => e.stopPropagation()}
-                    onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)}
+                    onKeyDown={e => {
+                        e.key === 'Enter' && this.onDismiss(true);
+                        e.stopPropagation();
+                    }}
                     value={this.description}
                     placeholder={this.description || '(Optional) Enter link description...'}
                     onChange={e => this.descriptionChanged(e)}
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index d9d0dbe3e..8c65fd34e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -670,7 +670,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const definedParameters = !this.compileParams.length ? null : (
             <div className="scriptingBox-plist" style={{ width: '30%' }}>
                 {this.compileParams.map((parameter, i) => (
-                    <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+                    <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
                         <EditableView
                             display={'block'}
                             maxHeight={72}
@@ -749,7 +749,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 {!this.compileParams.length || !this.paramsNames ? null : (
                     <div className="scriptingBox-plist">
                         {this.paramsNames.map((parameter: string, i: number) => (
-                            <div key={i} className="scriptingBox-pborder" onKeyPress={e => e.key === 'Enter' && this._overlayDisposer?.()}>
+                            <div key={i} className="scriptingBox-pborder" onKeyDown={e => e.key === 'Enter' && this._overlayDisposer?.()}>
                                 <div className="scriptingBox-wrapper" style={{ maxHeight: '40px' }}>
                                     <div className="scriptingBox-paramNames"> {`${parameter}:${this.paramsTypes[i]} = `} </div>
                                     {this.paramsTypes[i] === 'boolean' ? this.renderEnum(parameter, [true, false]) : null}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 5b03e2236..8802a032f 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import * as ReactDOM from 'react-dom/client';
@@ -22,15 +22,20 @@ import { OpenWhere } from '../DocumentView';
 import './DashFieldView.scss';
 import { FormattedTextBox } from './FormattedTextBox';
 import { DocData } from '../../../../fields/DocSymbols';
+import { NodeSelection, TextSelection } from 'prosemirror-state';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
     root: any;
     node: any;
     tbox: FormattedTextBox;
+    @observable expanded = false;
+    Expanded = () => this.expanded;
 
     unclickable = () => !this.tbox._props.rootSelected?.() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        makeObservable(this);
+        const self = this;
         this.node = node;
         this.tbox = tbox;
         this.dom = document.createElement('div');
@@ -38,11 +43,26 @@ export class DashFieldView {
         this.dom.style.height = node.attrs.height;
         this.dom.style.position = 'relative';
         this.dom.style.display = 'inline-block';
-        this.dom.onkeypress = function (e: any) {
+        const tBox = this.tbox;
+        this.dom.onkeypress = function (e: KeyboardEvent) {
             e.stopPropagation();
         };
-        this.dom.onkeydown = function (e: any) {
+        this.dom.onkeydown = function (e: KeyboardEvent) {
             e.stopPropagation();
+            if (e.key === 'Tab') {
+                e.preventDefault();
+                const editor = tbox.EditorView;
+                if (editor) {
+                    const state = editor.state;
+                    for (var i = state.selection.to; i < state.doc.content.size; i++) {
+                        if (state.doc.nodeAt(i)?.type.name === state.schema.nodes.dashField.name) {
+                            editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(i))));
+                            return;
+                        }
+                    }
+                    tBox.setFocus(state.selection.to + 1);
+                }
+            }
         };
         this.dom.onkeyup = function (e: any) {
             e.stopPropagation();
@@ -51,6 +71,8 @@ export class DashFieldView {
             e.stopPropagation();
         };
 
+        this.expanded = node.attrs.expanded;
+
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
             <DashFieldViewInternal
@@ -63,7 +85,7 @@ export class DashFieldView {
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
                 editable={node.attrs.editable}
-                expanded={node.attrs.expanded}
+                expanded={this.Expanded}
                 dataDoc={node.attrs.dataDoc}
                 tbox={tbox}
             />
@@ -77,9 +99,11 @@ export class DashFieldView {
         });
     }
     deselectNode() {
+        runInAction(() => (this.expanded = false));
         this.dom.classList.remove('ProseMirror-selectednode');
     }
     selectNode() {
+        setTimeout(() => runInAction(() => (this.expanded = true)), 100);
         this.dom.classList.add('ProseMirror-selectednode');
     }
 }
@@ -92,7 +116,7 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
-    expanded: boolean;
+    expanded: () => boolean;
     dataDoc: boolean;
     node: any;
     getPos: any;
@@ -106,7 +130,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     _fieldKey: string;
     _fieldRef = React.createRef<HTMLDivElement>();
     @observable _dashDoc: Doc | undefined = undefined;
-    @observable _expanded = this._props.expanded;
+    @observable _expanded = this._props.expanded();
 
     constructor(props: IDashFieldViewInternal) {
         super(props);
@@ -132,7 +156,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     componentWillUnmount() {
         this._reactionDisposer?.();
     }
-    isRowActive = () => this._expanded && this._props.editable;
+    isRowActive = () => (this._props.expanded() || this._expanded) && this._props.editable;
 
     finishEdit = action(() => {
         if (this._expanded) {
@@ -155,7 +179,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     deselectCell={emptyFunction}
                     selectCell={emptyFunction}
                     maxWidth={this._props.hideKey || this._hideKey ? undefined : this._props.tbox._props.PanelWidth}
-                    columnWidth={this._expanded ? this.columnWidth : returnZero}
+                    columnWidth={this._expanded || this._props.expanded() ? this.columnWidth : returnZero}
                     selectedCell={this.selectedCell}
                     fieldKey={this._fieldKey}
                     rowHeight={returnZero}
@@ -164,7 +188,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     getFinfo={emptyFunction}
                     setColumnValues={returnFalse}
                     allowCRs={true}
-                    oneLine={!this._expanded}
+                    oneLine={!this._expanded && !this._props.expanded()}
                     finishEdit={this.finishEdit}
                     transform={Transform.Identity}
                     menuTarget={null}
@@ -217,7 +241,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
     };
 
     @computed get values() {
-        if (this._props.expanded) return [];
+        if (this._props.expanded()) return [];
         const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []);
 
         return vals.strings.map(facet => ({ value: facet, label: facet }));
@@ -239,13 +263,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi
                     </span>
                 )}
                 {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent}
-                {!this.values.length ? null : (
-                    <select className="dashFieldView-select" onChange={this.selectVal}>
+                {/* {!this.values.length ? null : (
+                    <select className="dashFieldView-select" tabIndex={-1} onChange={this.selectVal}>
                         {this.values.map(val => (
                             <option value={val.value}>{val.label}</option>
                         ))}
                     </select>
-                )}
+                )} */}
             </div>
         );
     }
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index b786c5ffb..b90653acc 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -8,6 +8,7 @@ import { StrCast } from '../../../../fields/Types';
 import './DashFieldView.scss';
 import EquationEditor from './EquationEditor';
 import { FormattedTextBox } from './FormattedTextBox';
+import { DocData } from '../../../../fields/DocSymbols';
 
 export class EquationView {
     dom: HTMLDivElement; // container for label and value
@@ -88,7 +89,6 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
                     }
                     e.stopPropagation();
                 }}
-                onKeyPress={e => e.stopPropagation()}
                 style={{
                     position: 'relative',
                     display: 'inline-block',
@@ -96,12 +96,11 @@ export class EquationViewInternal extends React.Component<IEquationViewInternal>
                     height: this.props.height,
                     background: 'white',
                     borderRadius: '10%',
-                    bottom: 3,
                 }}>
                 <EquationEditor
                     ref={this._ref}
-                    value={StrCast(this._textBoxDoc[this._fieldKey], 'y=')}
-                    onChange={(str: any) => (this._textBoxDoc[this._fieldKey] = str)}
+                    value={StrCast(this._textBoxDoc[DocData][this._fieldKey])}
+                    onChange={(str: any) => (this._textBoxDoc[DocData][this._fieldKey] = str)}
                     autoCommands="pi theta sqrt sum prod alpha beta gamma rho"
                     autoOperatorNames="sin cos tan"
                     spaceBehavesLikeTab={true}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index eb7293054..70b3b52fd 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1616,10 +1616,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             e.stopPropagation();
         }
     };
-    setFocus = () => {
-        const pos = this._editorView?.state.selection.$from.pos || 1;
-        (this.ProseRef?.children?.[0] as any).focus();
-        setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+    setFocus = (ipos?: number) => {
+        const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1);
+        setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100);
+        setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200);
     };
     @action
     onFocused = (e: React.FocusEvent): void => {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index adc031636..c3eaf4dd2 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -385,10 +385,10 @@ export class RichTextRules {
             }),
 
             // create an inline equation node
-            // eq:<equation>>
-            new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+            // %eq
+            new InputRule(new RegExp(/%eq/), (state, match, start, end) => {
                 const fieldKey = 'math' + Utils.GenerateGuid();
-                this.TextBox.dataDoc[fieldKey] = match[1];
+                this.TextBox.dataDoc[fieldKey] = 'y=';
                 const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
                 return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
             }),
diff --git a/src/debug/Viewer.tsx b/src/debug/Viewer.tsx
index f46adef77..ceff01fc2 100644
--- a/src/debug/Viewer.tsx
+++ b/src/debug/Viewer.tsx
@@ -152,7 +152,7 @@ class Viewer extends React.Component {
     };
 
     @action
-    onKeyPress = (e: React.KeyboardEvent<HTMLDivElement>) => {
+    onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
         if (e.key === 'Enter') {
             DocServer.GetRefField(this.idToAdd).then(
                 action((field: any) => {
@@ -168,7 +168,7 @@ class Viewer extends React.Component {
     render() {
         return (
             <>
-                <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyPress} />
+                <input value={this.idToAdd} onChange={this.inputOnChange} onKeyDown={this.onKeyDown} />
                 <div>
                     {this.fields.map((field, index) => (
                         <DebugViewer field={field} key={index} setValue={() => false}></DebugViewer>
-- 
cgit v1.2.3-70-g09d2

