From a3eab5f2b8eca6ff7a21ea9d043b90c953c35f03 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 26 Mar 2025 16:36:15 -0400 Subject: fix so that views use don't use the rootDocument, but rather the current rendering document. got rid of resolvedDataDoc -- just use rootDocument[DocData] --- src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 5f132ecdf..416cd577e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1882,7 +1882,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent Date: Sun, 30 Mar 2025 11:16:37 -0400 Subject: changed Doc.Layout calls to doc[DocLayout]. fixed flashcard ui placement on card view. fixed css scaling for styleprovider icons and annotation resizer --- src/client/util/DropConverter.ts | 4 +- src/client/views/DocComponent.tsx | 6 +- src/client/views/DocumentDecorations.scss | 112 ++++++++++-------- src/client/views/DocumentDecorations.tsx | 18 +-- src/client/views/PropertiesView.tsx | 12 +- src/client/views/StyleProvider.scss | 16 ++- src/client/views/StyleProvider.tsx | 6 +- .../views/collections/CollectionCarouselView.scss | 4 +- .../views/collections/CollectionCarouselView.tsx | 4 +- src/client/views/collections/TabDocView.scss | 12 +- src/client/views/collections/TabDocView.tsx | 27 ++++- src/client/views/collections/TreeView.tsx | 6 +- .../CollectionFreeFormClusters.ts | 17 ++- .../CollectionFreeFormLayoutEngines.tsx | 7 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 22 ++-- .../collections/collectionFreeForm/MarqueeView.tsx | 3 +- .../collectionSchema/CollectionSchemaView.tsx | 2 +- .../collectionSchema/SchemaTableCell.tsx | 3 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 3 +- src/client/views/nodes/ComparisonBox.tsx | 131 +-------------------- src/client/views/nodes/DocumentContentsView.tsx | 101 ++++++++++++---- src/client/views/nodes/DocumentView.tsx | 44 +------ src/client/views/nodes/FieldView.tsx | 9 +- src/client/views/nodes/ImageBox.tsx | 40 ++++--- src/client/views/nodes/KeyValueBox.tsx | 3 +- src/client/views/nodes/KeyValuePair.tsx | 3 +- src/client/views/nodes/PDFBox.scss | 6 +- src/client/views/nodes/PDFBox.tsx | 4 +- .../nodes/formattedText/FormattedTextBox.scss | 5 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 3 +- .../views/nodes/importBox/ImportElementBox.tsx | 2 - src/client/views/pdf/PDFViewer.tsx | 3 +- src/fields/Doc.ts | 48 +++----- src/fields/util.ts | 25 ++-- 34 files changed, 317 insertions(+), 394 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index b5d29be4c..7d3f63448 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,5 +1,5 @@ import { Doc, DocListCast, StrListCast } from '../../fields/Doc'; -import { DocData } from '../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; import { ComputedField, ScriptField } from '../../fields/ScriptField'; @@ -93,7 +93,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.forEach((doc, i) => { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant - if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes(FontIconBox.name)) { + if (doc.type === DocumentType.FONTICON || StrCast(doc[DocLayout].layout).includes(FontIconBox.name)) { if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { // dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon dbox = Doc.MakeEmbedding(dbox); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index e3d47317c..d33f3b713 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -3,7 +3,7 @@ import * as React from 'react'; import { returnFalse } from '../../ClientUtils'; import { DateField } from '../../fields/DateField'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData } from '../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocData, DocLayout } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { DocCast, toList } from '../../fields/Types'; import { GetEffectiveAcl, inheritParentAcls } from '../../fields/util'; @@ -116,7 +116,7 @@ export function ViewBoxBaseComponent

() { * This may or may not inherit from the data doc. */ @computed get layoutDoc() { - return Doc.Layout(this.Document); + return this.Document[DocLayout]; } /** @@ -187,7 +187,7 @@ export function ViewBoxAnnotatableComponent

() { * This is the document being rendered. It may be a template so it may or may no inherit from the data doc. */ @computed get layoutDoc() { - return Doc.Layout(this.Document); + return this.Document[DocLayout]; } /** diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index a5afb1305..732c2645e 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -91,30 +91,71 @@ $resizeHandler: 8px; } } - .documentDecorations-closeButton { + .documentDecorations-closeWrapper { display: flex; - align-items: center; - justify-content: center; - background: #fb9d75; - border: solid 1.5px #a94442; - color: #fb9d75; - transition: 0.1s ease; - opacity: 1; - pointer-events: all; - width: 20px; - height: 20px; - min-width: 20px; - border-radius: 100%; - opacity: 0.5; - cursor: pointer; - - &:hover { - color: #a94442; + .documentDecorations-closeButton { + display: flex; + align-items: center; + justify-content: center; + background: #fb9d75; + border: solid 1.5px #a94442; + color: #fb9d75; + transition: 0.1s ease; opacity: 1; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: #a94442; + opacity: 1; + width: 20px; + min-width: 20px; + } + + > svg { + margin: 0; + } } - > svg { - margin: 0; + .documentDecorations-minimizeButton { + display: none; + align-items: center; + justify-content: center; + background: #ffdd00; + border: solid 1.5px #a94442; + color: #ffdd00; + transition: 0.1s ease; + font-size: 11px; + opacity: 1; + grid-column: 2; + pointer-events: all; + width: 20px; + height: 20px; + min-width: 20px; + border-radius: 100%; + opacity: 0.5; + cursor: pointer; + + &:hover { + color: #a94442; + opacity: 1; + } + + > svg { + margin: 0; + } + } + &:hover { + width: 40px; + min-width: 40px; + .documentDecorations-minimizeButton { + display: flex; + } } } @@ -145,38 +186,9 @@ $resizeHandler: 8px; } } - .documentDecorations-minimizeButton { - display: flex; - align-items: center; - justify-content: center; - background: #ffdd00; - border: solid 1.5px #a94442; - color: #ffdd00; - transition: 0.1s ease; - font-size: 11px; - opacity: 1; - grid-column: 2; - pointer-events: all; - width: 20px; - height: 20px; - min-width: 20px; - border-radius: 100%; - opacity: 0.5; - cursor: pointer; - - &:hover { - color: #a94442; - opacity: 1; - } - - > svg { - margin: 0; - } - } - .documentDecorations-title { opacity: 1; - width: calc(100% - 60px); // = margin-left + margin-right + width: 100%; grid-column: 3; pointer-events: auto; overflow: hidden; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 92b4d6fbf..7842c233a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -320,7 +320,7 @@ export class DocumentDecorations extends ObservableReactComponent { - const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); + const docMax = Math.min(NumCast(doc._width) / 2, NumCast(doc._height) / 2); const radius = Math.min(1, dist / maxDist) * docMax; // set radius based on ratio of drag distance to half diagonal distance of bounding box doc._layout_borderRounding = `${radius}px`; }); @@ -388,7 +388,7 @@ export class DocumentDecorations extends ObservableReactComponent { const accumRot = (NumCast(dv.Document._rotation) / 180) * Math.PI; const localRotCtr = dv.screenToViewTransform().transformPoint(rcScreen.X, rcScreen.Y); - const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document.width) / 2, localRotCtr[1] - NumCast(dv.Document.height) / 2]; + const localRotCtrOffset = [localRotCtr[0] - NumCast(dv.Document._width) / 2, localRotCtr[1] - NumCast(dv.Document._height) / 2]; const startRotCtr = Utils.rotPt(localRotCtrOffset[0], localRotCtrOffset[1], -accumRot); const unrotatedDocPos = { x: NumCast(dv.Document.x) + localRotCtrOffset[0] - startRotCtr.x, y: NumCast(dv.Document.y) + localRotCtrOffset[1] - startRotCtr.y }; infos.set(dv.Document, { unrotatedDocPos, startRotCtr, accumRot }); @@ -623,8 +623,8 @@ export class DocumentDecorations extends ObservableReactComponent { doc.$data = new InkField(inkPts.map( (ipt) => ({// (new x — oldx) + newWidth * (oldxpoint /oldWidth) - X: NumCast(doc.x) - x + (NumCast(doc.width) * ipt.X) / width, - Y: NumCast(doc.y) - y + (NumCast(doc.height) * ipt.Y) / height, + X: NumCast(doc.x) - x + (NumCast(doc._width) * ipt.X) / width, + Y: NumCast(doc.y) - y + (NumCast(doc._height) * ipt.Y) / height, }))); // prettier-ignore Doc.SetNativeWidth(doc, undefined); Doc.SetNativeHeight(doc, undefined); @@ -719,7 +719,7 @@ export class DocumentDecorations extends ObservableReactComponent )} {sharingMenu} - {!useLock ? null : ( + {!useLock || hideTitle ? null : ( toggle ability to interact with document} placement="top">

@@ -818,8 +818,10 @@ export class DocumentDecorations extends ObservableReactComponent - {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')} - {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')} +
+ {hideDeleteButton ? null : topBtn('close', 'times', undefined, () => this.onCloseClick(true), 'Close')} + {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, () => this.onCloseClick(undefined), 'Minimize')} +
{titleArea} {hideOpenButton ?
: topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection, opption: in editor view)')}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 7e9cd002b..7fcb15afe 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -13,7 +13,7 @@ import ResizeObserver from 'resize-observer-polyfill'; import { ClientUtils, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast, returnEmptyDoclist } from '../../fields/Doc'; -import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols'; +import { AclAdmin, DocAcl, DocData, DocLayout } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -205,7 +205,7 @@ export class PropertiesView extends ObservableReactComponent(reqdKeys); const docs: Doc[] = DocumentView.Selected().length < 2 // - ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] + ? [this.layoutFields ? this.selectedDoc[DocLayout] : this.dataDoc] : DocumentView.Selected().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => Object.keys(doc) @@ -264,7 +264,7 @@ export class PropertiesView extends ObservableReactComponent (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); // prettier-ignore docs.forEach(doc => { @@ -330,7 +330,7 @@ export class PropertiesView extends ObservableReactComponent Doc.toggleLockedPosition(doc), 'toggleBackground'); @@ -87,7 +89,7 @@ export function DefaultStyleProvider(doc: Opt, props: Opt {this.content} {this.navButtons} diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index 397e35ca9..931cdac2b 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -4,7 +4,6 @@ height: 100%; width: 100%; } - input.lm_title:focus, input.lm_title { max-width: unset !important; @@ -22,7 +21,6 @@ input.lm_title { .lm_iconWrap { display: flex; color: black; - width: 15px; height: 15px; align-items: center; align-self: center; @@ -30,6 +28,16 @@ input.lm_title { margin: 3px; border-radius: 20%; + width: auto; + svg:nth-of-type(2) { + display: none; + } + &:hover { + svg:nth-of-type(2) { + display: block; + } + } + .moreInfoDot { background-color: white; border-radius: 100%; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 620be2726..568a08792 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -10,7 +10,7 @@ import ResizeObserver from 'resize-observer-polyfill'; import { ClientUtils, DashColor, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; @@ -41,6 +41,7 @@ import { CollectionDockingView } from './CollectionDockingView'; import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; +import { Tooltip } from '@mui/material'; interface TabMinimapViewProps { doc: Doc; @@ -311,7 +312,7 @@ export class TabDocView extends ObservableReactComponent { @observable _view: DocumentView | undefined = undefined; @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update - @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore + @computed get layoutDoc() { return this._document?.[DocLayout]; } // prettier-ignore @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore @@ -353,7 +354,6 @@ export class TabDocView extends ObservableReactComponent { if (tab.element[0].children[1].children.length === 1) { iconWrap.className = 'lm_iconWrap lm_moreInfo'; - iconWrap.title = 'click for menu, drag to embed in document'; const dragBtnDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -377,7 +377,26 @@ export class TabDocView extends ObservableReactComponent { ); }; - const docIcon = ; + const docIcon = ( + <> + + + + + { + if (doc.layout_fieldKey === 'layout_icon') { + const odoc = Doc.GetEmbeddings(doc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(doc); + Doc.deiconifyView(odoc); + } + this.addDocTab(doc, OpenWhere.lightboxAlways); + }} + /> + + + ); const closeIcon = ; ReactDOM.createRoot(iconWrap).render(docIcon); ReactDOM.createRoot(closeWrap).render(closeIcon); diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 9889766d5..4dc937864 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { ClientUtils, lightOrDark, return18, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; +import { DocData, DocLayout } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -156,7 +156,7 @@ export class TreeView extends ObservableReactComponent { return this.Document[DocData]; } @computed get layoutDoc() { - return Doc.Layout(this.Document); + return this.Document[DocLayout]; } @computed get fieldKey() { return StrCast(this.Document._treeView_FieldKey, Doc.LayoutFieldKey(this.Document)); @@ -1309,7 +1309,7 @@ export class TreeView extends ObservableReactComponent { const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); const outdent = !parentCollectionDoc ? undefined : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView._props.parentTreeView : undefined); const addDocument = (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false); - const childLayout = Doc.Layout(pair.layout); + const childLayout = pair.layout[DocLayout]; const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); return aspect ? Math.min(NumCast(childLayout._width), rowWidth()) / aspect : NumCast(childLayout._height); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts index 3838852dd..903d92c90 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts @@ -31,16 +31,14 @@ export class CollectionFreeFormClusters { get selectDocuments() { return this._view.selectDocuments; } // prettier-ignore static overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) { - const doc2Layout = Doc.Layout(doc2); - const doc1Layout = Doc.Layout(doc1); const x2 = NumCast(doc2.x) - clusterDistance; const y2 = NumCast(doc2.y) - clusterDistance; - const w2 = NumCast(doc2Layout._width) + clusterDistance; - const h2 = NumCast(doc2Layout._height) + clusterDistance; + const w2 = NumCast(doc2._width) + clusterDistance; + const h2 = NumCast(doc2._height) + clusterDistance; const x = NumCast(doc1.x) - clusterDistance; const y = NumCast(doc1.y) - clusterDistance; - const w = NumCast(doc1Layout._width) + clusterDistance; - const h = NumCast(doc1Layout._height) + clusterDistance; + const w = NumCast(doc1._width) + clusterDistance; + const h = NumCast(doc1._height) + clusterDistance; return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 }); } handlePointerDown(probe: number[]) { @@ -49,12 +47,11 @@ export class CollectionFreeFormClusters { .reduce((cluster, cd) => { const grouping = this.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1); if (grouping !== -1) { - const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance / 2; const cy = NumCast(cd.y) - this._clusterDistance / 2; - const cw = NumCast(layoutDoc._width) + this._clusterDistance; - const ch = NumCast(layoutDoc._height) + this._clusterDistance; - return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster; + const cw = NumCast(cd._width) + this._clusterDistance; + const ch = NumCast(cd._height) + this._clusterDistance; + return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster; } return cluster; }, -1); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 4ea1de680..158bac7ba 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,5 +1,6 @@ /* eslint-disable no-use-before-define */ import { Doc, Field, FieldType, FieldResult } from '../../../../fields/Doc'; +import { DocLayout } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; import { RefField } from '../../../../fields/RefField'; @@ -237,7 +238,7 @@ export function computePivotLayout(poolData: Map, pivotDoc: Do payload: val, }); val?.docs.forEach((doc, i) => { - const layoutDoc = Doc.Layout(doc); + const layoutDoc = doc[DocLayout]; let wid = pivotAxisWidth; let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { @@ -249,7 +250,7 @@ export function computePivotLayout(poolData: Map, pivotDoc: Do y: -y + (pivotAxisWidth - hgt) / 2, width: wid, height: hgt, - backgroundColor: StrCast(layoutDoc.backgroundColor, 'white'), + backgroundColor: StrCast(doc.backgroundColor, 'white'), pair: { layout: doc }, replica: val.replicas[i], }); @@ -362,7 +363,7 @@ export function computeTimelineLayout(poolData: Map, pivotDoc: function layoutDocsAtTime(keyDocs: Doc[], key: number) { keyDocs.forEach(doc => { const stack = findStack(x, stacking); - const layoutDoc = Doc.Layout(doc); + const layoutDoc = doc[DocLayout]; let wid = pivotAxisWidth; let hgt = pivotAxisWidth / (Doc.NativeAspect(layoutDoc) || 1); if (hgt > pivotAxisWidth) { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 09d43c5b0..f01fb8fc7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -11,7 +11,7 @@ import ReactLoading from 'react-loading'; import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; -import { DocData, Height, Width } from '../../../../fields/DocSymbols'; +import { DocData, DocLayout, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -286,7 +286,7 @@ export class CollectionFreeFormView extends CollectionSubView this.fitContentBounds?.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(this.Document.freeform_panX, 1)); panY = () => this.fitContentBounds?.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(this.Document.freeform_panY, 1)); - zoomScaling = () => this.fitContentBounds?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.rootDocument)?.[this.scaleFieldKey], 1)); + zoomScaling = () => this.fitContentBounds?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1); // , NumCast(DocCast(this.Document.rootDocument)?.[this.scaleFieldKey], 1)); PanZoomCenterXf = () => (this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.centeringShiftX}px, ${this.centeringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`); ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); @@ -440,7 +440,7 @@ export class CollectionFreeFormView extends CollectionSubView { - const layoutDoc = Doc.Layout(d); + const layoutDoc = d[DocLayout]; const delta = Utils.rotPt(x - dropPos[0], y - dropPos[1], fromScreenXf.Rotate); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); @@ -1440,9 +1440,8 @@ export class CollectionFreeFormView extends CollectionSubView { - const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(layoutdoc._width), NumCast(doc.y) + NumCast(layoutdoc._height)); + const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(doc._width), NumCast(doc.y) + NumCast(doc._height)); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale !== undefined) { @@ -1489,9 +1488,9 @@ export class CollectionFreeFormView extends CollectionSubView { - const textDoc = DocCast(textBox.Document, textBox.Document); + const textDoc = DocCast(textBox.Document); const newDoc = Doc.MakeCopy(textDoc, true); - newDoc['$' + Doc.LayoutFieldKey(newDoc, textBox._props.LayoutTemplateString)] = undefined; // the copy should not copy the text contents of it source, just the render style + newDoc['$' + Doc.LayoutFieldKey(newDoc)] = undefined; // the copy should not copy the text contents of it source, just the render style newDoc.x = NumCast(textDoc.x) + (below ? 0 : NumCast(textDoc._width) + 10); newDoc.y = NumCast(textDoc.y) + (below ? NumCast(textDoc._height) + 10 : 0); DocumentView.SetSelectOnLoad(newDoc); @@ -1600,14 +1599,13 @@ export class CollectionFreeFormView extends CollectionSubView /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); const childDoc = pair.layout; - const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values - const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed + const contentFrameNumber = Cast(childDoc._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed const { z, zIndex } = childDoc; const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber); const { x, y, autoDim, _width, _height, opacity, _rotation } = layoutFrameNumber === undefined // -1 for width/height means width/height should be PanelWidth/PanelHeight (prevents collectionfreeformdocumentview width/height from getting out of synch with panelWIdth/Height which causes detailView to re-render and lose focus because HTMLtag scaling gets set to a bad intermediate value) - ? { autoDim: 1, _width: Cast(childDoc._width, 'number'), _height: Cast(childDoc._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this._props.childOpacity?.() } + ? { autoDim: 1, _width: Cast(childDoc._width, 'number'), _height: Cast(childDoc._height, 'number'), _rotation: Cast(childDoc._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this._props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber); // prettier-ignore const rotation = Cast(_rotation,'number', @@ -1625,8 +1623,8 @@ export class CollectionFreeFormView extends CollectionSubView { const selection: Doc[] = []; const selectFunc = (doc: Doc) => { - const layoutDoc = Doc.Layout(doc); - const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(layoutDoc._width), height: NumCast(layoutDoc._height) }; + const bounds = { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }; if (!this._lassoFreehand) { intersectRect(bounds, this.Bounds) && selection.push(doc); } else { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 05670562e..5803acca0 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -200,7 +200,7 @@ export class CollectionSchemaView extends CollectionSubView() { this._props.setContentViewBox?.(this); document.addEventListener('keydown', this.onKeyDown); - Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1])); + Object.entries(this._documentOptions).forEach(pair => this.fieldInfos.set(pair[0], pair[1] as FInfo)); this._keysDisposer = observe( this.dataDoc[this.fieldKey ?? 'data'] as List, change => { diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 72838074e..173984dc7 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -32,6 +32,7 @@ import { FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaCellField } from './SchemaCellField'; +import { DocLayout } from '../../../../fields/DocSymbols'; /** * SchemaTableCells make up the majority of the visual representation of the SchemaView. @@ -104,7 +105,7 @@ export class SchemaTableCell extends ObservableReactComponent() }; @action handleRenderGPTClick = () => { - const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined; - if (phonTrans) { - this._inputValue = StrCast(phonTrans); - this.askGPTPhonemes(this._inputValue); - this._renderSide = this.backKey; - this._outputValue = ''; - } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); + if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC); }; onPointerMove = ({ movementX }: PointerEvent) => { @@ -467,45 +460,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() this._props.DocumentView?.()._props.addDocument?.(newAudio); }; - /** - * Gets the transcription of an audio recording by sending the - * recording to backend. - */ - pushInfo = () => - axios - .post( - 'http://localhost:105/recognize/', // - { file: DocCast(this.Document.audio).$url }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => { - this.Document.phoneticTranscription = response.data.transcription; - }); - - /** - * Extracts the id of the youtube video url. - * @param url - * @returns - */ - getYouTubeVideoId = (url: string) => { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=|\?v=)([^#&?]*).*/; - const match = url.match(regExp); - return match && match[2].length === 11 ? match[2] : null; - }; - - /** - * Gets the transcript of a youtube video by sending the video url to the backend. - * @returns transcription of youtube recording - */ - youtubeUpload = async () => - axios - .post( - 'http://localhost:105/youtube/', // - { file: this.getYouTubeVideoId(this.frontText) }, - { headers: { 'Content-Type': 'application/json' } } - ) - .then(response => response.data.transcription); - /** * Calls GPT for each flashcard type. */ @@ -539,45 +493,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() layoutWidth = () => NumCast(this.layoutDoc.width, 200); layoutHeight = () => NumCast(this.layoutDoc.height, 200); - /** - * Ask GPT for advice on how to improve speech by comparing the phonetic transcription of - * a users audio recording with the phonetic transcription of their intended sentence. - * @param phonemes - */ - askGPTPhonemes = async (phonemes: string) => { - const sentence = this.frontText; - const phon6 = 'huː ɑɹ juː tədeɪ'; - const phon4 = 'kamo estas hɔi'; - const promptEng = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in American speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon6 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in American speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart. The goal is to be understood, not sound like a native speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "ceeffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - const promptSpa = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - 'como estás hoy' + - '" that is standard in Spanish speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phon4 + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in Spanish speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Only note the difference if they are not allophones of the same phoneme and if they are far away on the vowel chart; say good job if it would be understood by a native Spanish speaker. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Identify "ɔi" sounds like "oy". Ignore accents and do not say anything to the user about this.'; - const promptAll = - 'Consider all possible phonetic transcriptions of the intended sentence "' + - sentence + - '" that is standard in ' + - this.convertAbr() + - ' speech without showing the user. Compare each word in the following phonemes with those phonetic transcriptions without displaying anything to the user: "' + - phonemes + - '". Steps to do this: Align the words with each word in the intended sentence by combining the phonemes to get a pronunciation that resembles the word in order. Do not describe phonetic corrections with the phonetic alphabet - describe it by providing other examples of how it should sound. Note if a word or sound missing, including missing vowels and consonants. If there is an additional word that does not match with the provided sentence, say so. For each word, if any letters mismatch and would sound weird in ' + - this.convertAbr() + - ' speech and they are not allophones of the same phoneme and they are far away from each on the ipa vowel chat and that pronunciation is not normal for the meaning of the word, note this difference and explain how it is supposed to sound. Just so you know, "i" sounds like "ee" as in "bee", not "ih" as an "lick". Interpret "ɹ" as the same as "r". Interpret "ʌ" as the same as "ə". Do not make "θ" and "f" interchangable. Do not make "n" and "ɲ" interchangable. Do not make "e" and "i" interchangable. If "ɚ", "ɔː", and "ɔ" are options for pronunciation, do not choose "ɚ". Ignore differences with colons. Ignore redundant letters and words and sounds and the splitting of words; do not mention this since there could be repeated words in the sentence. Provide a response like this: "Lets work on improving the pronunciation of "coffee." You said "cawffee," which is close, but we need to adjust the vowel sound. In American English, "coffee" is pronounced /ˈkɔːfi/, with a long "aw" sound. Try saying "kah-fee." Your intonation is good, but try putting a bit more stress on "like" in the sentence "I would like a coffee with milk." This will make your speech sound more natural. Keep practicing, and lets try saying the whole sentence again!"'; - - switch (this._recognition.lang) { - case 'en-US': this._outputValue = await gptAPICall(promptEng, GPTCallType.PRONUNCIATION); break; - case 'es-ES': this._outputValue = await gptAPICall(promptSpa, GPTCallType.PRONUNCIATION); break; - default: this._outputValue = await gptAPICall(promptAll, GPTCallType.PRONUNCIATION); break; - } // prettier-ignore - }; - /** * Display a user's speech to text result. * @param e @@ -637,31 +552,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); }; - testForTextFields = (whichSlot: string) => { - const slotData = Doc.Get(this.dataDoc, whichSlot, true); - const slotHasText = slotData instanceof RichTextField || typeof slotData === '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 === this.frontKey ? (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.). - // 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_0) - if (whichSlot === this.backKey && !layoutTemplateString?.includes(whichSlot)) { - const queryText = altText?.replace('(this)', subjectText); // TODO: this should be done in Doc.setField but it doesn't know about the fieldKey ... - if (queryText?.match(/\(\(.*\)\)/)) { - Doc.SetField(this.Document, whichSlot, ':=' + queryText); // make the second slot be a computed field on the data doc that calls ChatGpt - } - } - return layoutTemplateString; - }; childActiveFunc = () => this._childActive; contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); @@ -682,24 +572,21 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent() displayDoc = (whichSlot: string) => { const whichDoc = DocCast(this.dataDoc[whichSlot]); const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); - const layoutString = targetDoc ? '' : this.testForTextFields(whichSlot); - return targetDoc || layoutString ? ( + return targetDoc ? ( <> ()
- {/*
this.openContextMenu(e.clientX, e.clientY, false)}> - -
*/} - {/* */} - {/*
this.openContextMenu(e.clientX, e.clientY, true)} style={{ left: '50px', zIndex: '100' }}> - -
*/} - {/* */} diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index d1eae1784..6f004bed3 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -11,10 +11,85 @@ import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { ObservableReactComponent, ObserverJsxParser } from '../ObservableReactComponent'; import './DocumentView.scss'; -import { FieldViewProps } from './FieldView'; +import { FieldViewProps, FieldViewSharedProps } from './FieldView'; +import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; +import { Property } from 'csstype'; + +interface DocOnlyProps { + LayoutTemplate?: () => Opt; + LayoutTemplateString?: string; + hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected + hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected + hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDocumentButtonBar?: boolean; + hideOpenButton?: boolean; + hideDeleteButton?: boolean; + hideLinkAnchors?: boolean; + hideLinkButton?: boolean; + hideCaptions?: boolean; + contentPointerEvents?: Property.PointerEvents | undefined; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + dontCenter?: 'x' | 'y' | 'xy'; + showTags?: boolean; + showAIEditor?: boolean; + hideFilterStatus?: boolean; + childHideDecorationTitle?: boolean; + childHideResizeHandles?: boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + dragWhenActive?: boolean; + dontHideOnDrag?: boolean; + onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + DataTransition?: () => string | undefined; + NativeWidth?: () => number; + NativeHeight?: () => number; + contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; + dragConfig?: (data: DragManager.DocumentDragData) => void; + dragStarting?: () => void; + dragEnding?: () => void; + + reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) +} +const DocOnlyProps = [ + 'layoutFieldKey', + 'LayoutTemplate', + 'LayoutTemplateString', + 'hideDecorations', // whether to suppress all DocumentDecorations when doc is selected + 'hideResizeHandles', // whether to suppress resized handles on doc decorations when this document is selected + 'hideTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDecorationTitle', // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + 'hideDocumentButtonBar', + 'hideOpenButton', + 'hideDeleteButton', + 'hideLinkAnchors', + 'hideLinkButton', + 'hideCaptions', + 'contentPointerEvents', // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + 'dontCenter', + 'showTags', + 'showAIEditor', + 'hideFilterStatus', + 'childHideDecorationTitle', + 'childHideResizeHandles', + 'childDragAction', // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. + 'dragWhenActive', + 'dontHideOnDrag', + 'onClickScriptDisable', // undefined = only when selected + 'DataTransition', + 'NativeWidth', + 'NativeHeight', + 'contextMenuItems', + 'dragConfig', + 'dragStarting', + 'dragEnding', + + 'reactParent', // parent React component view (see CollectionFreeFormDocumentView) +]; + +export interface DocumentViewProps extends DocOnlyProps, FieldViewSharedProps {} type BindingProps = Without; -export interface JsxBindings { +interface JsxBindings { props: BindingProps; } @@ -77,7 +152,7 @@ export class HTMLtag extends React.Component { } } -export interface DocumentContentsViewProps extends FieldViewProps { +interface DocumentContentsViewProps extends DocumentViewProps, FieldViewProps { layoutFieldKey: string; } @observer @@ -118,24 +193,6 @@ export class DocumentContentsView extends ObservableReactComponent, onInput: Opt): JsxBindings { - const docOnlyProps = [ - // these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews - 'hideResizeHandles', - 'hideTitle', - 'bringToFront', - 'childContentPointerEvents', - 'LayoutTemplateString', - 'LayoutTemplate', - 'showTags', - 'layoutFieldKey', - 'dontCenter', - 'DataTransition', - 'contextMenuItems', - // 'onClick', // don't need to omit this since it will be set - 'onDoubleClickScript', - 'onPointerDownScript', - 'onPointerUpScript', - ]; const templateDataDoc = this._props.TemplateDataDocument ?? (this.layoutDoc !== this._props.Document ? this._props.Document[DocData] : undefined); const list: BindingProps & React.DetailedHTMLProps, HTMLDivElement> = { ...this._props, @@ -146,7 +203,7 @@ export class DocumentContentsView extends ObservableReactComponent string | undefined; - NativeWidth?: () => number; - NativeHeight?: () => number; - contextMenuItems?: () => { script?: ScriptField; method?: () => void; filter?: ScriptField; label: string; icon: string }[]; - dragConfig?: (data: DragManager.DocumentDragData) => void; - dragStarting?: () => void; - dragEnding?: () => void; - - reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView) -} @observer -export class DocumentViewInternal extends DocComponent() { +export class DocumentViewInternal extends DocComponent() { // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. @@ -809,7 +778,7 @@ export class DocumentViewInternal extends DocComponent )} - {this.widgetDecorations ?? null} +
{this.widgetDecorations ?? null}
); } @@ -1271,9 +1240,8 @@ export class DocumentView extends DocComponent() { public get ContentDiv() { return this._docViewInternal?._contentDiv; } // prettier-ignore public get ComponentView() { return this._docViewInternal?._componentView; } // prettier-ignore public get allLinks() { return this._docViewInternal?._allLinks ?? []; } // prettier-ignore - public get TagBtnHeight() { - return this._docViewInternal?.TagsBtnHeight; - } + public get TagBtnHeight() { return this._docViewInternal?.TagsBtnHeight; } // prettier-ignore + public get UIBtnScaling() { return this._docViewInternal?.uiBtnScaling; } // prettier-ignore get LayoutFieldKey() { return Doc.LayoutFieldKey(this.Document, this._props.LayoutTemplateString); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 933868c46..74059bd12 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -48,8 +48,6 @@ export type StyleProviderFuncType = ( export interface FieldViewSharedProps { Document: Doc; TemplateDataDocument?: Doc; - LayoutTemplateString?: string; - LayoutTemplate?: () => Opt; renderDepth: number; scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView) @@ -62,7 +60,8 @@ 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) + suppressSetHeight?: boolean; + dontCenter?: 'x' | 'y' | 'xy' | undefined; LocalRotation?: () => number | undefined; // amount of rotation applied to freeformdocumentview containing document view containerViewPath?: () => DocumentView[]; fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document @@ -86,7 +85,6 @@ export interface FieldViewSharedProps { onPointerUpScript?: () => ScriptField; onKey?: (e: KeyboardEvent, textBox: FormattedTextBox) => boolean | undefined; fitWidth?: (doc: Doc) => boolean | undefined; - dontCenter?: 'x' | 'y' | 'xy' | undefined; searchFilterDocs: () => Doc[]; showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; @@ -102,7 +100,6 @@ export interface FieldViewSharedProps { waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt; - suppressSetHeight?: boolean; } /** @@ -110,13 +107,13 @@ export interface FieldViewSharedProps { * */ export interface FieldViewProps extends FieldViewSharedProps { DocumentView?: () => DocumentView; - fieldKey: string; isSelected: () => boolean; select: (ctrlPressed: boolean, shiftPress?: boolean) => void; docViewPath: () => DocumentView[]; setHeight?: (height: number) => void; NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal isHovering?: () => boolean; + fieldKey: string; // properties intended to be used from within layout strings (otherwise use the function equivalents that work more efficiently with React) // See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index d89fe0bd4..030473be4 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -47,6 +47,7 @@ import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; import { RichTextField } from '../../../fields/RichTextField'; +const DefaultPath = '/assets/unknown-file-icon-hi.png'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define private static _instance: ImageEditorData; @@ -426,7 +427,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return ClientUtils.CorsProxy(url.href); - if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith('/assets/unknown-file-icon-hi.png')) return `/assets/unknown-file-icon-hi.png`; + if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower) || lower.endsWith(DefaultPath)) return DefaultPath; const ext = extname(url.href); return url.href.replace(ext, (this._error ? '_o' : this._curSuffix) + ext); @@ -440,7 +441,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { @computed get nativeSize() { TraceMobx(); - if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; + if (this.paths.length && this.paths[0].includes(DefaultPath)) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 }; const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '_nativeOrientation'], 1); @@ -527,7 +528,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { @computed get paths() { const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']); // retrieve alternate documents that may be rendered as alternate images - const defaultUrl = new URL(ClientUtils.prepend('/assets/unknown-file-icon-hi.png')); + const defaultUrl = new URL(ClientUtils.prepend(DefaultPath)); const altpaths = alts ?.map(doc => (doc instanceof Doc ? (ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url ?? defaultUrl) : defaultUrl)) @@ -703,21 +704,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { } screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.ScreenToLocalBoxXf().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!this.dataDoc[this.fieldKey]) { - this.chooseImage(); - } else if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { - setupMoveUpEvents( - this, - e, - action(moveEv => { - MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); - return true; - }), - returnFalse, - () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), - false - ); + if (!e.altKey && e.button === 0) { + if (NumCast(this.layoutDoc._freeform_scale, 1) <= NumCast(this.dataDoc.freeform_scaleMin, 1) && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) { + setupMoveUpEvents( + this, + e, + action(moveEv => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this.marqueeref.current?.onInitiateSelection([moveEv.clientX, moveEv.clientY]); + return true; + }), + returnFalse, + () => { + if (!this.dataDoc[this.fieldKey]) this.chooseImage(); + else MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + }, + false + ); + } } }; @action diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index bb711a5fc..be897b3f3 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -22,6 +22,7 @@ import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import { OpenWhere } from './OpenWhere'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { DocLayout } from '../../../fields/DocSymbols'; export type KVPScript = { script: CompiledScript; @@ -125,7 +126,7 @@ export class KeyValueBox extends ViewBoxBaseComponent() { public static SetField = undoable((doc: Doc, key: string, value: string, forceOnDelegateIn?: boolean, setResult?: (value: FieldResult) => void) => { const script = KeyValueBox.CompileKVPScript(value); if (!script) return false; - const ldoc = key.startsWith('_') ? Doc.Layout(doc) : doc; + const ldoc = key.startsWith('_') ? doc[DocLayout] : doc; const forceOnDelegate = forceOnDelegateIn || (ldoc !== doc && !value.startsWith('=')); // an '=' value redirects a key targeting the render template to the root document. // also, if ldoc and doc are equal, allow redirecting to data document when not using an equal diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index e3c481a75..c9e0aea5a 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -16,6 +16,7 @@ import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import { OpenWhere } from './OpenWhere'; +import { DocLayout } from '../../../fields/DocSymbols'; // Represents one row in a key value plane @@ -60,7 +61,7 @@ export class KeyValuePair extends ObservableReactComponent { }; render() { - let doc = this._props.keyName.startsWith('_') ? Doc.Layout(this._props.doc) : this._props.doc; + let doc = this._props.keyName.startsWith('_') ? this._props.doc[DocLayout] : this._props.doc; const layoutField = doc !== this._props.doc; const key = layoutField ? this._props.keyName.replace(/^_/, '') : this._props.keyName; let protoCount = 0; diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index f2160feb7..eaea272dc 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -23,9 +23,9 @@ // glr: This should really be the same component as text and PDFs .pdfBox-sidebarBtn { background: global.$black; - height: 25px; - width: 25px; - right: 5px; + height: 20px; + width: 20px; + right: 0px; color: global.$white; display: flex; position: absolute; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 78ddafa88..36d260fb9 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -499,8 +499,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { title="Toggle Sidebar" style={{ display: !this._props.isContentActive() ? 'none' : undefined, - top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 0, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + transformOrigin: 'top right', + transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index f9de4ab5a..547a2efa8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -141,8 +141,8 @@ audiotag:hover { position: absolute; top: 0; right: 0; - width: 17px; - height: 17px; + width: 20px; + height: 20px; font-size: 11px; border-radius: 3px; color: white; @@ -153,6 +153,7 @@ audiotag:hover { align-items: center; cursor: grabbing; box-shadow: global.$standard-box-shadow; + transform-origin: top right; // transition: 0.2s; opacity: 0.3; &:hover { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 416cd577e..51b9a4333 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1941,6 +1941,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
@@ -2050,7 +2051,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx index 317719032..7e470a642 100644 --- a/src/client/views/nodes/importBox/ImportElementBox.tsx +++ b/src/client/views/nodes/importBox/ImportElementBox.tsx @@ -22,9 +22,7 @@ export class ImportElementBox extends ViewBoxBaseComponent() { return (
{ panelHeight = () => this._props.PanelHeight() / (this._props.NativeDimScaling?.() || 1); transparentFilter = () => [...this._props.childFilters(), ClientUtils.TransparentBackgroundFilter]; opaqueFilter = () => [...this._props.childFilters(), ClientUtils.noDragDocsFilter, ...(SnappingManager.CanEmbed && this._props.isContentActive() ? [] : [ClientUtils.OpaqueBackgroundFilter])]; - childStyleProvider = (doc: Doc | undefined, props: Opt, property: string) => { + childStyleProvider = (doc: Doc | undefined, props: Opt, property: string) => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none'; const isInk = doc.layout_isSvg && !props?.LayoutTemplateString; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 65719374f..734af4ccd 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -64,7 +64,7 @@ export namespace Field { .trim() .replace(/^new List\((.*)\)$/, '$1'); }; - const notOnTemplate = !key.startsWith('_') || Doc.Layout(doc) === doc; + const notOnTemplate = !key.startsWith('_') || doc[DocLayout] === doc; const isOnDelegate = notOnTemplate && !Doc.IsDataProto(doc) && ((key.startsWith('_') && !Field.IsField(cfield)) || Object.keys(doc).includes(key.replace(/^_/, ''))); return (isOnDelegate ? '=' : '') + (!Field.IsField(cfield) ? '' : valFunc(cfield)); } @@ -407,9 +407,9 @@ export class Doc extends RefField { } @computed get __DATA__(): Doc { const self = this[SelfProxy]; - return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).rootDocument, Doc, null) || self); + return self.rootDocument && !self.isTemplateForField ? self : Doc.GetProto(Cast(self[DocLayout].rootDocument, Doc, null) || self); } - @computed get __LAYOUT__(): Doc | undefined { + @computed get __LAYOUT__(): Doc { const self = this[SelfProxy]; const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null); if (templateLayoutDoc) { @@ -422,7 +422,7 @@ export class Doc extends RefField { } return Cast(self['layout_' + templateLayoutDoc.title + '(' + renderFieldKey + ')'], Doc, null) || templateLayoutDoc; } - return undefined; + return self; } public async [HandleUpdate](diff: { $set: { [key: string]: FieldType } } | { $unset?: unknown }) { @@ -687,15 +687,11 @@ export namespace Doc { } export function MakeEmbedding(doc: Doc, id?: string) { - const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); - const layout = Doc.LayoutField(embedding); - if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) { - Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); - } + const embedding = (!Doc.IsDataProto(doc) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); embedding.createdFrom = doc; - embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; - !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); embedding.author = ClientUtils.CurrentUserEmail(); + embedding.proto_embeddingId = doc.$proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); return embedding; } @@ -768,9 +764,8 @@ export namespace Doc { // prune doc and do nothing } else if ( !Doc.IsSystem(docAtKey) && - (key.includes('layout[') || - key.startsWith('layout') || // - ['embedContainer', 'annotationOn', 'proto'].includes(key) || + (key.startsWith('layout') || + ['embedContainer', 'annotationOn', 'proto'].includes(key) || // (['link_anchor_1', 'link_anchor_2'].includes(key) && doc.author === ClientUtils.CurrentUserEmail())) ) { assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks, cloneTemplates)); @@ -1117,10 +1112,10 @@ export namespace Doc { Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } // get the layout string that the template uses to specify its layout - const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField))); + const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout])); // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document. - Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); + templateField[DocLayout].layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`); return true; } @@ -1157,16 +1152,13 @@ export namespace Doc { // a layout field or 'layout' is given. export function Layout(doc: Doc, template?: Doc): Doc { const expandedTemplate = template && Cast(doc['layout_' + template.title + '(' + StrCast(template.isTemplateForField, 'data') + ')'], Doc, null); - return expandedTemplate || doc[DocLayout] || doc; - } - export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; + return expandedTemplate || doc[DocLayout]; } export function LayoutField(doc: Doc) { return doc[StrCast(doc.layout_fieldKey, 'layout')]; } export function LayoutFieldKey(doc: Doc, templateLayoutString?: string): string { - const match = StrCast(templateLayoutString || Doc.Layout(doc).layout).match(/fieldKey={'([^']+)'}/); + const match = StrCast(templateLayoutString || doc[DocLayout].layout).match(/fieldKey={'([^']+)'}/); return match?.[1] || ''; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey } export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { @@ -1325,17 +1317,7 @@ export namespace Doc { } export function getDocTemplate(doc?: Doc) { - return !doc - ? undefined - : doc.isTemplateDoc - ? doc - : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc - ? doc.dragFactory - : Cast(Doc.Layout(doc), Doc, null)?.isTemplateDoc - ? Cast(Doc.Layout(doc), Doc, null).rootDocument - ? Doc.Layout(doc).proto - : Doc.Layout(doc) - : undefined; + return !doc ? undefined : doc.isTemplateDoc ? doc : Cast(doc.dragFactory, Doc, null)?.isTemplateDoc ? doc.dragFactory : doc[DocLayout].isTemplateDoc ? (doc[DocLayout].rootDocument ? doc[DocLayout].proto : doc[DocLayout]) : undefined; } export function toggleLockedPosition(doc: Doc) { @@ -1737,7 +1719,7 @@ ScriptingGlobals.add(function idToDoc(id: string): Doc { }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function renameEmbedding(doc: Doc) { - return StrCast(doc.$title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; + return StrCast(doc.title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function getProto(doc: Doc) { diff --git a/src/fields/util.ts b/src/fields/util.ts index abbe543e8..205c500a5 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -303,30 +303,23 @@ export function SetPropSetterCb(prop: string, propSetter: ((target: Doc, value: // // target should be either a Doc or ListImpl. receiver should be a Proxy Or List. // -export function setter(target: ListImpl | Doc, inProp: string | symbol | number, value: unknown, receiver: Doc | ListImpl): boolean { - if (!inProp) { +export function setter(target: ListImpl | Doc, prop: string | symbol | number, value: unknown, receiver: Doc | ListImpl): boolean { + if (!prop) { console.log('WARNING: trying to set an empty property. This should be fixed. '); return false; } - let prop = inProp; - const effectiveAcl = inProp === 'constructor' || typeof inProp === 'symbol' ? AclAdmin : GetPropAcl(target, prop); + const effectiveAcl = prop === 'constructor' || typeof prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop); if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true; // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl_') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value as SharingPermissions))) return true; - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('$')) { - prop = prop.substring(1); - if (target.__DATA__ instanceof Doc) { - target.__DATA__[prop] = value as FieldResult; - return true; - } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('$')) { + target.__DATA__[prop.substring(1)] = value as FieldResult; + return true; } - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { - if (!prop.startsWith('__')) prop = prop.substring(1); - if (target.__LAYOUT__ instanceof Doc) { - target.__LAYOUT__[prop] = value as FieldResult; - return true; - } + if (target instanceof Doc && typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_') && !prop.startsWith('__')) { + target.__LAYOUT__[prop.substring(1)] = value as FieldResult; + return true; } if (target.__fieldTuples[prop] instanceof ComputedField) { if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { -- cgit v1.2.3-70-g09d2 From f061db01d1936bf3e776b140786ba3bf1c6b71f3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 31 Mar 2025 14:29:57 -0400 Subject: moved audio annotation indicator/playback button to TagsView. cleaned up tagsview colors and unobscured by bottom resizer --- .../components/src/components/Button/Button.scss | 5 +- .../src/components/IconButton/IconButton.scss | 5 +- src/client/util/DictationManager.ts | 39 ++++++------ src/client/views/DocViewUtils.ts | 3 - src/client/views/DocumentButtonBar.tsx | 2 +- src/client/views/DocumentDecorations.tsx | 4 +- src/client/views/StyleProvider.scss | 5 -- src/client/views/StyleProvider.tsx | 18 +----- src/client/views/TagsView.scss | 6 +- src/client/views/TagsView.tsx | 21 ++++--- src/client/views/nodes/DocumentView.tsx | 12 ++-- src/client/views/nodes/IconTagBox.scss | 1 - src/client/views/nodes/IconTagBox.tsx | 69 +++++++++++++++------- .../views/nodes/formattedText/FormattedTextBox.tsx | 4 +- 14 files changed, 100 insertions(+), 94 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/packages/components/src/components/Button/Button.scss b/packages/components/src/components/Button/Button.scss index b2fb48344..c86db9fad 100644 --- a/packages/components/src/components/Button/Button.scss +++ b/packages/components/src/components/Button/Button.scss @@ -63,13 +63,16 @@ filter: opacity(0); &.active { - filter: opacity(0.5); + filter: opacity(0.3); } } &:hover { .background { filter: opacity(0.3); + &.active { + filter: opacity(0); + } } } } diff --git a/packages/components/src/components/IconButton/IconButton.scss b/packages/components/src/components/IconButton/IconButton.scss index 3f0dd26ea..f95d94ae4 100644 --- a/packages/components/src/components/IconButton/IconButton.scss +++ b/packages/components/src/components/IconButton/IconButton.scss @@ -54,13 +54,16 @@ filter: opacity(0); &.active { - filter: opacity(0.5); + filter: opacity(0.3); } } &:hover { .background { filter: opacity(0.3); + &.active { + filter: opacity(0); + } } } } diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 44fbda319..2eef3da0e 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -356,23 +356,16 @@ export namespace DictationManager { action: (target: DocumentView, matches: RegExpExecArray) => { const count = interpretNumber(matches[1]); const what = matches[2]; - const dataDoc = Doc.GetProto(target.Document); - const fieldKey = 'data'; - if (isNaN(count)) { - return; - } - for (let i = 0; i < count; i++) { - let created: Doc | undefined; - switch (what) { - case 'image': - created = Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'); - break; - case 'nested collection': - created = Docs.Create.FreeformDocument([], {}); - break; - default: + if (!isNaN(count)) { + for (let i = 0; i < count; i++) { + const created = (() => { + switch (what) { + case 'image': return Docs.Create.ImageDocument('https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg'); + case 'nested collection':return Docs.Create.FreeformDocument([], {}); + } // prettier-ignore + })(); + created && Doc.AddDocToList(target.dataDoc, Doc.LayoutFieldKey(target.Document), created); } - created && Doc.AddDocToList(dataDoc, fieldKey, created); } }, restrictTo: [DocumentType.COL], @@ -388,13 +381,15 @@ export namespace DictationManager { }, ]; } - export function recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { + export function recordAudioAnnotation(doc: Doc, fieldIn: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) { + const field = '$' + fieldIn + '_audioAnnotations'; let gumStream: MediaStream | undefined; let recorder: MediaRecorder | undefined; navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => { - let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null); + let audioTextAnnos = Cast(doc[field + '_text'], listSpec('string'), null); if (audioTextAnnos) audioTextAnnos.push(''); - else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List(['']); + else audioTextAnnos = doc[field + '_text'] = new List(['']); + doc._layout_showTags = true; DictationManager.Controls.listen({ interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore continuous: { indefinite: false }, @@ -415,16 +410,16 @@ export namespace DictationManager { const [{ result }] = await Networking.UploadFilesToServer({ file: file as Blob & { name: string; lastModified: number; webkitRelativePath: string } }); if (!(result instanceof Error)) { const audioField = new AudioField(result.accessPaths.agnostic.client); - const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null); + const audioAnnos = Cast(doc[field], listSpec(AudioField), null); if (audioAnnos) audioAnnos.push(audioField); - else dataDoc[field + '_audioAnnotations'] = new List([audioField]); + else doc[field] = new List([audioField]); } }; recorder.start(); const stopFunc = () => { recorder?.stop(); DictationManager.Controls.stop(/* false */); - dataDoc.audioAnnoState = AudioAnnoState.stopped; + doc._audioAnnoState = AudioAnnoState.stopped; gumStream?.getAudioTracks()[0].stop(); }; if (onRecording) onRecording(stopFunc); diff --git a/src/client/views/DocViewUtils.ts b/src/client/views/DocViewUtils.ts index 1f5f29c7e..5710187b5 100644 --- a/src/client/views/DocViewUtils.ts +++ b/src/client/views/DocViewUtils.ts @@ -1,6 +1,3 @@ -/* eslint-disable prefer-destructuring */ -/* eslint-disable default-param-last */ -/* eslint-disable no-use-before-define */ import { runInAction } from 'mobx'; import { Doc, SetActiveAudioLinker } from '../../fields/Doc'; import { DocUtils } from '../documents/DocUtils'; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index b17dbc93d..9a57a5e6c 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -358,7 +358,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( view => view && DictationManager.recordAudioAnnotation( - view.dataDoc, + view.Document, view.LayoutFieldKey, stopFunc => { this._stopFunc = stopFunc; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7842c233a..875eb45e0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -834,7 +834,7 @@ export class DocumentDecorations extends ObservableReactComponent
-
+ {(seldocview.TagPanelHeight ?? 0) > 30 ? null :
}
)} @@ -866,7 +866,7 @@ export class DocumentDecorations extends ObservableReactComponent - {DocumentView.Selected().length > 1 ? : null} + {DocumentView.Selected().length > 1 ? : null}
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 501b9a292..99796f1fb 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -1,5 +1,4 @@ .styleProvider-filter, -.styleProvider-audio, .styleProvider-paint, .styleProvider-paint-selected, .styleProvider-lock { @@ -32,9 +31,6 @@ margin: auto !important; } } -.styleProvider-audio { - right: 40; -} .styleProvider-paint-selected, .styleProvider-paint { top: 15; @@ -43,7 +39,6 @@ right: -40; } .styleProvider-lock:hover, -.styleProvider-audio:hover, .styleProvider-filter:hover { opacity: 1; } diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index a59d39cfb..432a05146 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -1,7 +1,6 @@ import { Dropdown, DropdownType, IconButton, IListItemProps, Shadows, Size, Type } from '@dash/components'; import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; import { action, untracked } from 'mobx'; import { extname } from 'path'; import * as React from 'react'; @@ -9,11 +8,11 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; import { FaFilter } from 'react-icons/fa'; import { ClientUtils, DashColor, lightOrDark } from '../../ClientUtils'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; +import { DocLayout } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkInkTool } from '../../fields/InkField'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; -import { AudioAnnoState } from '../../server/SharedMediaTypes'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { IsFollowLinkScript } from '../documents/DocUtils'; import { SnappingManager } from '../util/SnappingManager'; @@ -26,7 +25,6 @@ import { FieldViewProps } from './nodes/FieldView'; import { StyleProp } from './StyleProp'; import './StyleProvider.scss'; import { styleProviderQuiz } from './StyleProviderQuiz'; -import { DocLayout } from '../../fields/DocSymbols'; function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground'); @@ -387,26 +385,12 @@ export function DefaultStyleProvider(doc: Opt, props: Opt ); }; - const audio = () => { - const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); - const audioAnnosCount = (audioDoc: Doc) => StrListCast(audioDoc[fieldKey + 'audioAnnotations']).length; - if (!doc || renderDepth === -1 || !audioAnnosCount(doc)) return null; - const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: 'blue' }; - return ( - {StrListCast(doc[fieldKey + 'audioAnnotations_text']).lastElement()}
}> -
DocumentView.getFirstDocumentView(doc)?.playAnnotation()}> - -
- - ); - }; return ( <> {paint()} {lock()} {filter()} - {audio()} ); } diff --git a/src/client/views/TagsView.scss b/src/client/views/TagsView.scss index b21d303fb..231d5e11a 100644 --- a/src/client/views/TagsView.scss +++ b/src/client/views/TagsView.scss @@ -12,7 +12,7 @@ .tagsView-list { display: flex; flex-wrap: wrap; - height: 1; + height: 100%; .iconButton-container { min-height: unset !important; } @@ -57,10 +57,6 @@ align-items: center; } -.tagsView-editing-box { - margin-top: 20px; -} - .tagsView-input-box { margin: auto; align-self: center; diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx index f1bcaac2c..39c952483 100644 --- a/src/client/views/TagsView.tsx +++ b/src/client/views/TagsView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, Colors, IconButton } from '@dash/components'; +import { Button, Colors, IconButton, Type } from '@dash/components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; @@ -20,6 +20,7 @@ import { DocumentView } from './nodes/DocumentView'; import { FaceRecognitionHandler } from './search/FaceRecognitionHandler'; import { IconTagBox } from './nodes/IconTagBox'; import { Id } from '../../fields/FieldSymbols'; +import { StyleProp } from './StyleProp'; /** * The TagsView is a metadata input/display panel shown at the bottom of a DocumentView in a freeform collection. @@ -254,6 +255,7 @@ export class TagItem extends ObservableReactComponent { interface TagViewProps { Views: DocumentView[]; + background: string; } /** @@ -269,7 +271,7 @@ export class TagsView extends ObservableReactComponent { @observable _panelHeightDirty = 0; @observable _currentInput = ''; - @observable _isEditing = !StrListCast(this.View.dataDoc.tags).length; + @observable _isEditing: boolean | undefined = undefined; _heightDisposer: IReactionDisposer | undefined; _lastXf = this.View.screenToContentsTransform(); @@ -295,7 +297,9 @@ export class TagsView extends ObservableReactComponent { } @computed get isEditing() { - return this._isEditing && (this._props.Views.length > 1 || (DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View))); + const selected = DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View); + if (this._isEditing === undefined) return selected && !StrListCast(this.View.dataDoc.tags).length && !StrListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']).length; + return this._isEditing && (this._props.Views.length > 1 || selected); } /** @@ -349,7 +353,7 @@ export class TagsView extends ObservableReactComponent { ref={r => r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)} style={{ display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined, - backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT, + backgroundColor: this.isEditing ? this._props.background : Colors.TRANSPARENT, borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT, height: !this._props.Views.lastElement()?.isSelected() ? 0 : undefined, }}> @@ -361,14 +365,17 @@ export class TagsView extends ObservableReactComponent { tooltip="Close Menu" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, upEv => { - this.setToEditing(!this._isEditing); + this.setToEditing(!this.isEditing); upEv.stopPropagation(); }) } - icon={} + type={Type.TERT} + background="transparent" + color={this.View._props.styleProvider?.(this.View.Document, this.View.ComponentView?._props, StyleProp.FontColor) as string} + icon={} /> )} - + {Array.from(tagsList) .filter(tag => (tag.startsWith('#') || tag.startsWith('@')) && !Doc.MyFilterHotKeys.some(key => key.toolType === tag)) .map(tag => ( diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index c3026d7be..070a13103 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -750,7 +750,7 @@ export class DocumentViewInternal extends DocComponent - {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? : null} + {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? : null}
) : ( <> @@ -774,7 +774,7 @@ export class DocumentViewInternal extends DocComponent this.historyRef(this._oldHistoryWheel, (this._oldHistoryWheel = r))}>
{this._componentView?.componentAIView?.() ?? null} - {this._props.DocumentView?.() ? : null} + {this._props.DocumentView?.() ? : null}
)} @@ -1319,7 +1319,7 @@ export class DocumentView extends DocComponent() { } public playAnnotation = () => { - const audioAnnoState = this.dataDoc.audioAnnoState ?? AudioAnnoState.stopped; + const audioAnnoState = this.Document._audioAnnoState ?? AudioAnnoState.stopped; const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '_audioAnnotations'], listSpec(AudioField), null); const anno = audioAnnos?.lastElement(); if (anno instanceof AudioField) { @@ -1331,13 +1331,13 @@ export class DocumentView extends DocComponent() { autoplay: true, loop: false, volume: 0.5, - onend: action(() => { this.dataDoc.audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore + onend: action(() => { this.Document._audioAnnoState = AudioAnnoState.stopped; }), // prettier-ignore }); - this.dataDoc.audioAnnoState = AudioAnnoState.playing; + this.Document._audioAnnoState = AudioAnnoState.playing; break; case AudioAnnoState.playing: (this.dataDoc[AudioPlay] as Howl)?.stop(); - this.dataDoc.audioAnnoState = AudioAnnoState.stopped; + this.Document._audioAnnoState = AudioAnnoState.stopped; break; default: } diff --git a/src/client/views/nodes/IconTagBox.scss b/src/client/views/nodes/IconTagBox.scss index 202b0c701..d6cf95958 100644 --- a/src/client/views/nodes/IconTagBox.scss +++ b/src/client/views/nodes/IconTagBox.scss @@ -4,7 +4,6 @@ display: flex; position: relative; pointer-events: none; - background-color: rgb(218, 218, 218); border-radius: 50px; align-items: center; gap: 5px; diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index ddabd61e1..e3924eca7 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -1,22 +1,23 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; -import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; +import { AudioAnnoState } from '../../../server/SharedMediaTypes'; import { undoable } from '../../util/UndoManager'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { TagItem } from '../TagsView'; import { DocumentView } from './DocumentView'; import './IconTagBox.scss'; +import { Size, Toggle, ToggleType, Type } from '@dash/components'; +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { StyleProp } from '../StyleProp'; export interface IconTagProps { Views: DocumentView[]; - IsEditing: boolean; + IsEditing: boolean | undefined; } /** @@ -52,13 +53,46 @@ export class IconTagBox extends ObservableReactComponent { * @param key metadata icon button * @returns an icon for the metdata button */ - getButtonIcon = (doc: Doc, key: Doc): JSX.Element => { + getButtonIcon = (dv: DocumentView, key: Doc): JSX.Element => { const icon = StrCast(key.icon) as IconProp; const tag = StrCast(key.toolType); - const isActive = TagItem.docHasTag(doc, tag); - const color = isActive ? '#4476f7' : '#323232'; // TODO should use theme colors + const color = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + return ( + } + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.setIconTag(tag, !TagItem.docHasTag(this.View.Document, tag))} + color={color} + /> + ); + }; - return ; + /** + * Displays a button to play audio annotations on the document. + * NOTE: This should be generalized -- audio should + * @returns + */ + renderAudioButtons = (dv: DocumentView, anno: string) => { + const fcolor = dv._props.styleProvider?.(dv.layoutDoc, dv.ComponentView?._props, StyleProp.FontColor) as string; + const audioIconColors: { [key: string]: string } = { playing: 'green', stopped: fcolor ?? 'blue', recording: 'red' }; + const audioAnnoState = (audioDoc: Doc) => StrCast(audioDoc.audioAnnoState, AudioAnnoState.stopped); + const color = audioIconColors[audioAnnoState(this.View.Document)]; + return ( + } + size={Size.XSMALL} + type={Type.PRIM} + onClick={() => this.View?.playAnnotation()} + color={color} + /> + ); }; /** @@ -69,22 +103,15 @@ export class IconTagBox extends ObservableReactComponent { .map(key => ({ key, tag: StrCast(key.toolType) })) .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected)) .map(({ key, tag }) => ( - Click to add/remove this card from the {tag} group
}> - + Click to add/remove the {tag} tag
}> + {this.getButtonIcon(this.View, key)} )); // prettier-ignore - return !buttons.length ? null : ( + const audioannos = StrListCast(this.View.Document[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']); + return !buttons.length && !audioannos.length ? null : (
+ {audioannos.length ? this.renderAudioButtons(this.View, audioannos.lastElement()) : null} {buttons}
); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 51b9a4333..6955e5401 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -17,7 +17,7 @@ import { BsMarkdownFill } from 'react-icons/bs'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -246,7 +246,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent void = emptyFunction; target.$mediaState = mediaState.Recording; - DictationManager.recordAudioAnnotation(target[DocData], Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + DictationManager.recordAudioAnnotation(target, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, -- cgit v1.2.3-70-g09d2 From 6c8effb77029db96bccab92152a6b68a0aefc132 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 1 Apr 2025 13:30:06 -0400 Subject: fixed text views to write/read font_ from data doc. --- src/client/views/global/globalScripts.ts | 4 +-- .../views/nodes/formattedText/FormattedTextBox.tsx | 38 +++++++++++----------- 2 files changed, 21 insertions(+), 21 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 79d0ee88f..04b2873d1 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -367,8 +367,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: toggle: () => editorView?.state && RichTextMenu.Instance?.changeListType(list) }]); // prettier-ignore const attrs:attrfuncs[] = [ - ['dictation', { checkResult: () => !!textView?._recordingDictation, - toggle: () => textView && runInAction(() => { textView._recordingDictation = !textView._recordingDictation;} ) }], + ['dictation', { checkResult: () => !!textView?.recordingDictation, + toggle: () => textView && runInAction(() => { textView.recordingDictation = !textView.recordingDictation;} ) }], ['fitBox', { checkResult: () => RichTextMenu.Instance?.fitBox ?? false, toggle: () => RichTextMenu.Instance?.toggleFitBox()}], ['elide', { checkResult: () => false, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 6955e5401..f7e6d8e1e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -141,20 +141,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - if (this.EditorView && this._recordingDictation) { + if (this.EditorView && this.recordingDictation) { this.stopDictation(/* true */); this._break = true; const { state } = this.EditorView; const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); this.EditorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); - if (this._recordingDictation) { + if (this.recordingDictation) { this.recordDictation(); } } @@ -1333,14 +1333,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent this._recordingDictation, + () => this.recordingDictation, () => { this.stopDictation(/* true */); - this._recordingDictation && this.recordDictation(); + this.recordingDictation && this.recordDictation(); }, { fireImmediately: true } ); - if (this._recordingDictation) setTimeout(this.recordDictation); + if (this.recordingDictation) setTimeout(this.recordDictation); } this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), @@ -1560,8 +1560,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent disposer?.()); this.endUndoTypingBatch(); @@ -1596,7 +1596,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent @@ -1919,7 +1919,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - this._recordingDictation = !this._recordingDictation; + this.recordingDictation = !this.recordingDictation; }) ) }> -- cgit v1.2.3-70-g09d2 From 031a607100700f818f96b7fbf478f1b75292be9b Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 5 Apr 2025 00:01:53 -0400 Subject: fixed sizing of text box annotations sidebar button. cleaned up TagsView to not overlap button bar below doc decorations or bottom resize handle --- src/client/views/AntimodeMenu.tsx | 2 + src/client/views/DocumentDecorations.scss | 3 +- src/client/views/DocumentDecorations.tsx | 34 +++++------ src/client/views/SidebarAnnos.scss | 10 ++++ src/client/views/SidebarAnnos.tsx | 7 +-- src/client/views/TagsView.scss | 3 +- src/client/views/TagsView.tsx | 21 +++++-- .../views/global/globalCssVariables.module.scss | 2 +- src/client/views/nodes/DocumentView.scss | 4 +- src/client/views/nodes/DocumentView.tsx | 3 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 65 ++++++++++++++-------- 11 files changed, 98 insertions(+), 56 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx index 99dee6410..b5e56cad5 100644 --- a/src/client/views/AntimodeMenu.tsx +++ b/src/client/views/AntimodeMenu.tsx @@ -4,6 +4,7 @@ import { SnappingManager } from '../util/SnappingManager'; import './AntimodeMenu.scss'; import { ObservableReactComponent } from './ObservableReactComponent'; +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface AntimodeMenuProps {} /** @@ -162,6 +163,7 @@ export abstract class AntimodeMenu extends Observab transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, position: this.Pinned ? 'unset' : undefined, + border: `${SnappingManager.userColor} solid 1px`, }}> {buttons}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss index 732c2645e..28cebe23c 100644 --- a/src/client/views/DocumentDecorations.scss +++ b/src/client/views/DocumentDecorations.scss @@ -41,6 +41,7 @@ $resizeHandler: 8px; .documentDecorations-tagsView { position: absolute; height: 100%; + width: 100%; pointer-events: all; border-radius: 50%; color: black; @@ -327,7 +328,7 @@ $resizeHandler: 8px; .documentDecorations-rightResizer { pointer-events: auto; background: global.$medium-gray-dim; - //opacity: 0.2; + opacity: 0.2; &:hover { opacity: 1; } diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 875eb45e0..452dd60ee 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -49,7 +49,7 @@ export class DocumentDecorations extends ObservableReactComponent(); - private _resizeBorderWidth = 16; + private _resizeBorderWidth = 8; private _linkBoxHeight = 20 + 3; // link button height + margin private _titleHeight = 20; private _resizeUndo?: UndoManager.Batch; @@ -98,11 +98,11 @@ export class DocumentDecorations extends ObservableReactComponent
- {(seldocview.TagPanelHeight ?? 0) > 30 ? null :
} + {seldocview.TagPanelHeight !== 0 || seldocview.TagPanelEditing ? null :
}
)} @@ -850,12 +850,12 @@ export class DocumentDecorations extends ObservableReactComponent )} - {hideDocumentButtonBar || this._showNothing ? null : ( + {seldocview.TagPanelEditing || hideDocumentButtonBar || this._showNothing ? null : (
1 ? 0 : `${seldocview.showTags ? 4 + seldocview.TagPanelHeight : 4}px`, - transform: `translate(${-this._resizeBorderWidth / 2 + 10}px, ${this._resizeBorderWidth + bounds.b - bounds.y + this._titleHeight}px) `, + top: DocumentView.Selected().length > 1 || !seldocview.showTags ? 0 : `${seldocview.TagPanelHeight}px`, + transform: `translate(${-this._resizeBorderWidth + 10}px, ${(seldocview.TagPanelHeight === 0 ? 2 * this._resizeBorderWidth : this._resizeBorderWidth) + bounds.b - bounds.y + this._titleHeight}px) `, }}> DocumentView.Selected()} />
@@ -864,7 +864,7 @@ export class DocumentDecorations extends ObservableReactComponent {DocumentView.Selected().length > 1 ? : null}
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss index d7de2b641..abfd04f11 100644 --- a/src/client/views/SidebarAnnos.scss +++ b/src/client/views/SidebarAnnos.scss @@ -1,3 +1,13 @@ +.sidebarAnnos-container { + position: absolute; + width: 100%; + height: 100%; + right: 0; + .sidebarAnnos-stacking { + width: 100%; + position: relative; + } +} .sidebarAnnos-tagList { display: flex; flex-direction: row; diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 3c0611f03..b7e6318b1 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -213,14 +213,11 @@ export class SidebarAnnos extends ObservableReactComponent
e.stopPropagation()}> {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null} @@ -228,7 +225,7 @@ export class SidebarAnnos extends ObservableReactComponent -
+
{ super(props); makeObservable(this); } - InsetDist = 25; // how far tag panel is moved up to overlap DocumentView. @observable _panelHeightDirty = 0; @observable _currentInput = ''; @@ -298,8 +297,8 @@ export class TagsView extends ObservableReactComponent { @computed get isEditing() { const selected = DocumentView.Selected().length === 1 && DocumentView.Selected().includes(this.View); - if (this._isEditing === undefined) return selected && !StrListCast(this.View.dataDoc.tags).length && !StrListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']).length; - return this._isEditing && (this._props.Views.length > 1 || selected); + if (this._isEditing === undefined) return selected && this.View.TagPanelEditing; // && !StrListCast(this.View.dataDoc.tags).length && !StrListCast(this.View.dataDoc[Doc.LayoutFieldKey(this.View.Document) + '_audioAnnotations_text']).length; + return this._isEditing && (this._props.Views.length > 1 || (selected && this.View.TagPanelEditing)); } /** @@ -309,7 +308,10 @@ export class TagsView extends ObservableReactComponent { @action setToEditing = (editing = true) => { this._isEditing = editing; - editing && this._props.Views.length === 1 && this.View.select(false); + if (this._props.Views.length === 1) { + this.View.TagPanelEditing = editing; + editing && this.View.select(false); + } }; /** @@ -350,7 +352,16 @@ export class TagsView extends ObservableReactComponent { return this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1) ? null : (
r && new ResizeObserver(action(() => this._props.Views.length === 1 && (this.View.TagPanelHeight = Math.max(0, (r?.getBoundingClientRect().height ?? 0) - this.InsetDist)))).observe(r)} + ref={r => + r && + new ResizeObserver( + action(() => { + if (this._props.Views.length === 1) { + this.View.TagPanelHeight = Math.floor(r?.children[0].children[0].getBoundingClientRect().height ?? 0) - Math.floor(r?.children[0].children[0].children[0].getBoundingClientRect().height ?? 0); + } + }) + ).observe(r?.children[0]) + } style={{ display: SnappingManager.IsResizing === this.View.Document[Id] ? 'none' : undefined, backgroundColor: this.isEditing ? this._props.background : Colors.TRANSPARENT, diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss index ad0c5c21d..82f6caa52 100644 --- a/src/client/views/global/globalCssVariables.module.scss +++ b/src/client/views/global/globalCssVariables.module.scss @@ -4,7 +4,7 @@ $white: #ffffff; $off-white: #fdfdfd; $light-gray: #dfdfdf; $medium-gray: #9f9f9f; -$medium-gray-dim: #9f9f9f30; +$medium-gray-dim: #9f9f9f; $dark-gray: #323232; $black: #000000; diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index dd5fd0d0c..f60f33608 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -273,7 +273,9 @@ .documentView-noAIWidgets { transform-origin: top left; - position: relative; + position: absolute; + bottom: 0; + pointer-events: none; } .documentView-editorView-history { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 37f888ddd..4cf60d356 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -748,7 +748,7 @@ export class DocumentViewInternal extends DocComponent {this._props.DocumentView?.() && !this._props.docViewPath().slice(-2)[0].ComponentView?.isUnstyledView?.() ? : null}
@@ -1163,6 +1163,7 @@ export class DocumentView extends DocComponent() { @observable private _selected = false; @observable public static CurrentlyPlaying: DocumentView[] = []; // audio or video media views that are currently playing @observable public TagPanelHeight = 0; + @observable public TagPanelEditing = false; @computed get showTags() { return this.Document._layout_showTags || this._props.showTags; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index f7e6d8e1e..d90be007f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -154,11 +154,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const defaultSidebar = 250; - const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!); + const dw = DivWidth(this._ref.current); + const prevWidth = 1 - this.sidebarWidth() / dw / this.nativeScaling(); if (preview) this._showSidebar = true; else { - this.layoutDoc[this.sidebarKey + '_freeform_scale_max'] = 1; - this.layoutDoc._layout_showSidebar = - (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '0%'; + this.layoutDoc._layout_sidebarWidthPercent = + this.sidebarWidthPercent === '0%' // + ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` // + : '0%'; + this.layoutDoc._layout_showSidebar = this.sidebarWidthPercent !== '0%'; } - this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); + this.layoutDoc._width = + !preview && this.SidebarShown // + ? NumCast(this.layoutDoc._width) + defaultSidebar + : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); @@ -769,7 +775,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta; - const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; + const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; @@ -1223,7 +1229,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent BoolCast(this.Document._freeform_fitContentsToBox); - sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.sidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); @@ -1901,12 +1907,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { this.dataDoc[this.sidebarKey + '_height'] = height; }; - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props .ScreenToLocalTransform() - .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1)); + .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / this.nativeScaling(), 0) + .scale(1 / this.nativeScaling()); @computed get audioHandle() { return !this.recordingDictation ? null : ( @@ -1927,6 +1933,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent ); } + private _sideBtnWidth = 35; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale ; } // prettier-ignore + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore + @computed get sidebarHandle() { TraceMobx(); const annotated = DocListCast(this.dataDoc[this.sidebarKey]).filter(d => d?.author).length; @@ -1941,7 +1961,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
@@ -1986,7 +2006,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent +
{renderComponent(StrCast(this.layoutDoc[this.sidebarKey + '_type_collection']))}
); @@ -2061,7 +2081,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable @@ -2078,8 +2098,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) => @@ -2107,7 +2127,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent
- {this.noSidebar || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null} -- cgit v1.2.3-70-g09d2 From 40b8e5c30e7251db6258cac5b4ed5fb4a6a2418d Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 7 Apr 2025 15:20:36 -0400 Subject: added multiToggle option to allow popup to stay up until explicitly untoggled. fixed multitoggle to honor toggleStatus. fixed popup to follow target. fixed setting text highlight background fro popup menu. fixed flashcardui buttons to have background sized properly. --- package-lock.json | 24 ++++++++++++ package.json | 1 + .../src/components/MultiToggle/MultiToggle.tsx | 16 +++++++- packages/components/src/components/Popup/Popup.tsx | 43 +++++++++++++++------- src/client/util/CurrentUserUtils.ts | 5 ++- .../views/collections/CollectionCardDeckView.tsx | 3 +- .../views/collections/FlashcardPracticeUI.tsx | 7 ++-- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 1 + .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 3 +- 10 files changed, 81 insertions(+), 24 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/package-lock.json b/package-lock.json index 3b11eda7c..9acca90ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", @@ -12081,6 +12082,29 @@ "@testing-library/dom": ">=7.21.4" } }, + "node_modules/@thednp/position-observer": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@thednp/position-observer/-/position-observer-1.0.7.tgz", + "integrity": "sha512-MkUAMMgqZPxy71hpcrKr9ZtedMk+oIFbFs5B8uKD857iuYKRJxgJtC1Itus14EEM4qMyeN0x47AUZJmZJQyXbQ==", + "license": "MIT", + "dependencies": { + "@thednp/shorty": "^2.0.10" + }, + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, + "node_modules/@thednp/shorty": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.10.tgz", + "integrity": "sha512-H+hs1lw3Yc1NfwG0b7F7YmVjxQZ31NO2+6zx+I+9XabHxdwPKjvYJnkKKXr7bSItgm2AFrfOn5+3veB6W4iauw==", + "license": "MIT", + "engines": { + "node": ">=16", + "pnpm": ">=8.6.0" + } + }, "node_modules/@tokenizer/token": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", diff --git a/package.json b/package.json index fe76189bf..1038c470e 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@pinecone-database/pinecone": "^2.2.2", "@react-google-maps/api": "^2.19.2", "@react-spring/web": "^9.7.5", + "@thednp/position-observer": "^1.0.7", "@turf/turf": "^7.1.0", "@types/bezier-js": "^4.1.3", "@types/brotli": "^1.3.4", diff --git a/packages/components/src/components/MultiToggle/MultiToggle.tsx b/packages/components/src/components/MultiToggle/MultiToggle.tsx index 0a6fb81c9..c1d610c34 100644 --- a/packages/components/src/components/MultiToggle/MultiToggle.tsx +++ b/packages/components/src/components/MultiToggle/MultiToggle.tsx @@ -17,6 +17,7 @@ export interface IMultiToggleProps extends IGlobalProps { selectedItems?: (string | number) | (string | number)[]; onSelectionChange?: (val: (string | number) | (string | number)[], added: boolean) => unknown; toggleStatus?: boolean; + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. } function promoteToArrayOrUndefined(d: (string | number)[] | (string | number) | undefined) { @@ -32,12 +33,23 @@ export const MultiToggle = (props: IMultiToggleProps) => { init = false; const [selectedItemsLocal, setSelectedItemsLocal] = useState(initVal as (string | number) | (string | number)[]); - const { items, selectedItems = selectedItemsLocal, tooltip, tooltipPlacement = 'top', onSelectionChange, color, background } = props; + const { + items, // + selectedItems = selectedItemsLocal, + tooltip, + toggleStatus, + tooltipPlacement = 'top', + onSelectionChange, + color, + background, + } = props; const itemsMap = new Map(); items.forEach(item => itemsMap.set(item.val, item)); return (
{ {promoteToArray(selectedItems).length < 2 ? null :
+
}
} - isToggle={true} + showUntilToggle={props.showUntilToggle ?? true} toggleFunc={() => { const selItem = items.find(item => promoteToArray(selectedItems).includes(item.val)); selItem && setSelectedItemsLocal([selItem.val]); diff --git a/packages/components/src/components/Popup/Popup.tsx b/packages/components/src/components/Popup/Popup.tsx index 9e72ece87..9e91a75cf 100644 --- a/packages/components/src/components/Popup/Popup.tsx +++ b/packages/components/src/components/Popup/Popup.tsx @@ -3,6 +3,7 @@ import { IGlobalProps, Placement, Size } from '../../global'; import { Toggle, ToggleType } from '../Toggle'; import './Popup.scss'; import { Popper } from '@mui/material'; +import PositionObserver from '@thednp/position-observer'; export enum PopupTrigger { CLICK = 'click', @@ -23,9 +24,10 @@ export interface IPopupProps extends IGlobalProps { isOpen?: boolean; setOpen?: (b: boolean) => void; background?: string; - isToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. + showUntilToggle?: boolean; // whether popup stays open when background is clicked. muyst click toggle button tp close it. toggleFunc?: () => void; popupContainsPt?: (x: number, y: number) => boolean; + multitoggle?: boolean; } /** @@ -57,20 +59,21 @@ export const Popup = (props: IPopupProps) => { fillWidth, iconPlacement = 'left', background, + multitoggle, popupContainsPt, } = props; const triggerRef = useRef(null); const popperRef = useRef(null); - const toggleRef = useRef(null); + const [toggleRef, setToggleRef] = useState(null); let timeout = setTimeout(() => {}); const handlePointerAwayDown = (e: PointerEvent) => { const rect = popperRef.current?.getBoundingClientRect(); - const rect2 = toggleRef.current?.getBoundingClientRect(); + const rect2 = toggleRef?.getBoundingClientRect(); if ( - !props.isToggle && + !props.showUntilToggle && (!rect2 || !(rect2.left < e.clientX && rect2.top < e.clientY && rect2.right > e.clientX && rect2.bottom > e.clientY)) && rect && !(rect.left < e.clientX && rect.top < e.clientY && rect.right > e.clientX && rect.bottom > e.clientY) && @@ -82,22 +85,36 @@ export const Popup = (props: IPopupProps) => { } }; + let observer: PositionObserver | undefined = undefined; + const [previousPosition, setPreviousPosition] = useState(toggleRef?.getBoundingClientRect()); + useEffect(() => { if (isOpen) { window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); window.addEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - return () => window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); - } - }, [isOpen, popupContainsPt]); - + if (toggleRef && multitoggle) { + (observer = new PositionObserver(entries => { + entries.forEach(entry => { + const currentPosition = entry.boundingClientRect; + if (Math.floor(currentPosition.top) !== Math.floor(previousPosition?.top ?? 0) || Math.floor(currentPosition.left) !== Math.floor(previousPosition?.left ?? 0)) { + // Perform actions when position changes + setPreviousPosition(currentPosition); // Update previous position + } + }); + })).observe(toggleRef); + } + return () => { + window.removeEventListener('pointerdown', handlePointerAwayDown, { capture: true }); + observer?.disconnect(); + }; + } else observer?.disconnect(); + }, [isOpen, toggleRef, popupContainsPt]); return (
{ - if (trigger === PopupTrigger.CLICK) setOpen(!isOpen); - }} + className={`trigger-container ${fillWidth && 'fillWidth'}`} + onClick={() => trigger === PopupTrigger.CLICK && setOpen(!isOpen)} onPointerEnter={() => { if (trigger === PopupTrigger.HOVER || trigger === PopupTrigger.HOVER_DELAY) { clearTimeout(timeout); @@ -109,7 +126,7 @@ export const Popup = (props: IPopupProps) => { timeout = setTimeout(() => setOpen(false), 1000); } }}> -
+
setToggleRef(R)}> {toggle ?? ( ; ignoreClick?: boolean; + showUntilToggle?: boolean; // whether the popup should stay open when the background is clicked. buttonText?: string; backgroundColor?: string; waitForDoubleClickToClick?: boolean; @@ -778,7 +779,7 @@ pie title Minerals in my tap water { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType: Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, - { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, + { title: "Ink", toolTip: "Ink", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Ink, showUntilToggle: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, subMenu: [ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: InkInkTool.Pen, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", toolType: InkInkTool.Highlight, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }}, @@ -786,7 +787,7 @@ pie title Minerals in my tap water ]}, { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.StrokeWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}, numBtnMin: 1, linearBtnWidth:40}, { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: InkProperty.StrokeColor,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"!activeInkTool()"}}, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, showUntilToggle: true, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, subMenu: [ { title: "Stroke", toolTip: "Eraser complete strokes",btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkEraserTool.Stroke, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, { title: "Segment", toolTip: "Erase between intersections",btnType:ButtonType.ToggleButton,icon:"xmark", toolType:InkEraserTool.Segment, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index 756b37f99..50de7c601 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -23,7 +23,8 @@ import { undoable, UndoManager } from '../../util/UndoManager'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; -import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocumentViewProps } from '../nodes/DocumentContentsView'; +import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCardDeckView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx index 8cd9c5452..f24a8acb7 100644 --- a/src/client/views/collections/FlashcardPracticeUI.tsx +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -63,8 +63,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent doc.title === 'Filter'); } // prettier-ignore @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore - btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale); - btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); + btnHeight = () => NumCast(this.filterDoc?.height); + btnWidth = () => (!this.filterDoc ? 1 : NumCast(this.filterDoc._width)); /** * Sets the practice mode answer style for flashcards @@ -135,7 +135,6 @@ export class FlashcardPracticeUI extends ObservableReactComponent= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`, }}> +
{!this.filterDoc ? null : (
() { multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} toggleStatus={toggleStatus} + showUntilToggle={BoolCast(this.Document.showUntilToggle)} // allow the toggle to be clickable unless ignoreClick is set on the Document label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: , diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index d90be007f..341340363 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1761,7 +1761,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) { // test if parent of new focused element is a UI button (should be more specific than testing className) - if (newFocusEle?.className === 'fonticonbox' || newFocusEle?.className === 'popup-container') { + if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { return this.EditorView?.focus(); // keep focus on text box } } diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 758b4035e..10c95f1e1 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -367,7 +367,8 @@ export class RichTextMenu extends AntimodeMenu { setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.TextView && this.view && fontField !== 'fitBox') { - if (this.view.hasFocus()) { + const anchorNode = window.getSelection()?.anchorNode; + if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs); -- cgit v1.2.3-70-g09d2 From 840d5b3b3a0e907b0ca74abb9ad57e09bfbc0b76 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 8 Apr 2025 10:52:59 -0400 Subject: changed border color icon --- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/MainView.tsx | 1 + src/client/views/nodes/formattedText/FormattedTextBox.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ca94791d0..3ce20fc7b 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -836,7 +836,7 @@ pie title Minerals in my tap water { 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_)'} }, // Only when a document is selected - { title: "Border", icon: "pen", toolTip: "Border Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected + { title: "Border", icon: "border-style", toolTip: "Border Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBorderColor(value, _readOnly_)'} }, // Only when a document is selected { title: "B.Width", toolTip: "Border width", btnType: ButtonType.NumberSliderButton, ignoreClick: true, scripts: {script: '{ return setBorderWidth(value, _readOnly_);}'}, numBtnMin: 0, linearBtnWidth:40}, { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!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: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index e70f9e5ed..2b2b77384 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -529,6 +529,7 @@ export class MainView extends ObservableReactComponent { fa.faColumns, fa.faChevronCircleUp, fa.faUpload, + fa.faBorderStyle, fa.faBorderAll, fa.faBraille, fa.faPersonChalkboard, diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 341340363..2a1ec0881 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1760,7 +1760,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) { - // test if parent of new focused element is a UI button (should be more specific than testing className) + // bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className) if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { return this.EditorView?.focus(); // keep focus on text box } -- cgit v1.2.3-70-g09d2 From d60734c4cdd8fe64d50e3de32182cc6b04afc747 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 8 Apr 2025 14:26:56 -0400 Subject: fixed gptImageLabel calls and added ImageCastToNameType to deal with filenames with '.'s in them. fixed vertical centered text. --- src/client/apis/gpt/GPT.ts | 1 - src/client/documents/DocUtils.ts | 4 +- src/client/documents/DocumentTypes.ts | 19 +++-- src/client/documents/Documents.ts | 8 +- src/client/util/CurrentUserUtils.ts | 87 +++++++++++----------- src/client/views/GlobalKeyHandler.ts | 9 +-- src/client/views/MainView.tsx | 21 ++---- src/client/views/PropertiesButtons.tsx | 2 +- src/client/views/collections/CollectionView.tsx | 1 - .../collectionFreeForm/FaceCollectionBox.tsx | 4 +- .../collectionFreeForm/ImageLabelBox.tsx | 20 +++-- .../collections/collectionFreeForm/MarqueeView.tsx | 5 +- src/client/views/nodes/FontIconBox/FontIconBox.tsx | 4 +- .../views/nodes/formattedText/FormattedTextBox.tsx | 2 +- .../views/nodes/formattedText/RichTextMenu.tsx | 6 +- src/client/views/search/FaceRecognitionHandler.tsx | 4 +- src/fields/Doc.ts | 2 +- src/fields/Types.ts | 8 +- 18 files changed, 97 insertions(+), 110 deletions(-) (limited to 'src/client/views/nodes/formattedText') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 140aebfe0..9cb47995c 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -271,7 +271,6 @@ const gptImageLabel = async (src: string, prompt: string): Promise => { ], }); if (response.choices[0].message.content) { - console.log(response.choices[0].message.content); return response.choices[0].message.content; } return 'Missing labels'; diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 69bf4815e..a657b673a 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -726,12 +726,12 @@ export namespace DocUtils { : { _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200, _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35, - _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, + text_centered: BoolCast(Doc.UserDoc().textCentered), text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), @@ -752,12 +752,12 @@ export namespace DocUtils { : { _width: width || BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 * 6 : 200, _height: BoolCast(Doc.UserDoc().fitBox) ? Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')) * 1.5 : 35, - _layout_centered: BoolCast(Doc.UserDoc()._layout_centered), _layout_fitWidth: true, _layout_autoHeight: true, backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, + text_centered: BoolCast(Doc.UserDoc().textCentered), text_fitBox: BoolCast(Doc.UserDoc().fitBox), text_align: StrCast(Doc.UserDoc().textAlign), text_fontColor: StrCast(Doc.UserDoc().fontColor), diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 03626107f..dd0985182 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -47,25 +47,30 @@ export enum DocumentType { JOURNAL = 'journal', // AARAV ADD } export enum CollectionViewType { - Invalid = 'invalid', + // general collections Freeform = 'freeform', - Calendar = 'calendar', Card = 'card', Carousel = 'carousel', Carousel3D = '3D Carousel', - Docking = 'docking', Grid = 'grid', - Linear = 'linear', - Map = 'map', Masonry = 'masonry', Multicolumn = 'multicolumn', Multirow = 'multirow', NoteTaking = 'notetaking', - Pile = 'pileup', Pivot = 'pivot', Schema = 'schema', Stacking = 'stacking', - StackedTimeline = 'stacked timeline', Time = 'time', Tree = 'tree', + // under development + Calendar = 'calendar', + // special collections + Docking = 'docking', + Pile = 'pileup', + StackedTimeline = 'stacked timeline', + Linear = 'linear', + Invalid = 'invalid', } + +export const specialCollectionTypes = [CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear, CollectionViewType.Invalid]; +export const standardViewTypes = Object.values(CollectionViewType).filter(key => !specialCollectionTypes.includes(key)); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index f1655e3cf..bab9edf0e 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -283,7 +283,6 @@ export class DocumentOptions { _layout_autoHeightMargins?: NUMt = new NumInfo('Margin heights to be added to the computed auto height of a Doc'); _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false); - _layout_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false); _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); @@ -309,6 +308,7 @@ export class DocumentOptions { text_fontSize?: string; text_fontFamily?: string; text_fontWeight?: string; + text_centered?: BOOLt = new BoolInfo('whether text should be vertically centered in Doc'); text_fitBox?: BOOLt = new BoolInfo("whether text box should be scaled to fit it's containing render box"); text_align?: STRt = new StrInfo('horizontal text alignment default', undefined, undefined, ['left', 'center', 'right']); title_align?: STRt = new StrInfo('horizontal title alignment in label box', undefined, undefined, ['left', 'center', 'right']); @@ -840,7 +840,7 @@ export namespace Docs { return TextDocument(RichTextField.textToRtf(text, img?.[Id]), { title, // _layout_autoHeight: true, - _layout_centered: true, + text_centered: true, text_align: 'center', _layout_fitWidth: true, ...opts, @@ -1072,10 +1072,6 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id); } - export function MapCollectionDocument(documents: Array, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map }); - } - export function CarouselDocument(documents: Array, options: DocumentOptions) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel }); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 3ce20fc7b..4c492cae0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -15,7 +15,7 @@ import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { Gestures } from "../../pen-gestures/GestureTypes"; import { DocServer } from "../DocServer"; import { DocUtils, FollowLinkScript } from '../documents/DocUtils'; -import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; +import { CollectionViewType, DocumentType, standardViewTypes } from "../documents/DocumentTypes"; import { Docs, DocumentOptions, FInfo, FInfoFieldType } from "../documents/Documents"; import { DashboardView } from "../views/DashboardView"; import { OverlayView } from "../views/OverlayView"; @@ -481,22 +481,24 @@ pie title Minerals in my tap water const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(this.target?.data).filter(doc => !docList(this.target.viewed).includes(doc)).length.toString())"; const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails"; return [ - { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), ignoreClick: true, icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, - { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), ignoreClick: true, icon: "search", }, - { title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), ignoreClick: true, icon: "folder-open", }, - { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), ignoreClick: true, icon: "wrench", }, - { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), ignoreClick:false, icon: "upload", }, - { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field - { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}}, - { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}}, - { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), ignoreClick: true, icon: "folder-open", hidden: false }, - { title: "Faces", toolTip: "Unique Faces", target: this.setupFaceCollection(doc, "myFaceCollection"), ignoreClick: true, icon: "face-smile", hidden: false }, - { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, + { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} }, + { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), icon: "search", }, + { title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", }, + { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", }, + { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", }, + { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field + { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue: badgeValue}}, + { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}}, + { title: "Image Grouper", toolTip: "Image Grouper", target: this.setupImageGrouper(doc, "myImageGrouper"), icon: "folder-open", hidden: false }, + { title: "Faces", toolTip: "Unique Faces", target: this.setupFaceCollection(doc, "myFaceCollection"), icon: "face-smile", hidden: false }, + { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} }, ].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(this)'}})); } /// the empty panel that is filled with whichever left menu button's panel has been selected static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { + const panel = DocCast(doc[field]); + if (panel) panel.proto = undefined; DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as {[key:string]: FieldType}), {title:"leftSidebarPanel", isSystem:true, undoIgnoreFields: new List(['proto'])}); } @@ -737,12 +739,12 @@ pie title Minerals in my tap water } static viewTools(): Button[] { return [ - { title: "Tags", icon: "id-card", toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, - { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform - { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)", - btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform - { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Tags", icon: "id-card", toolTip: "Toggle Tags display", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"toggle-tags",funcs: { }, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, + { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform + { title: "Fit All", icon: "object-group", toolTip:"Fit Docs to View (double tap to persist)", + btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform + { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: { hidden: `!SelectedDocType("${CollectionViewType.Freeform}", this.expertMode)`}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform ] } static textTools():Button[] { @@ -760,14 +762,14 @@ pie title Minerals in my tap water { title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment",ignoreClick: true, subMenu: [ - { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, - { 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: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, + { 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: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", 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: "Fit Box", toolTip: "Fit text to box", btnType: ButtonType.ToggleButton, icon: "object-group",toolType:"fitBox", 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()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, @@ -825,13 +827,9 @@ pie title Minerals in my tap water { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, ]; } - static contextMenuTools(doc:Doc):Button[] { + static contextMenuTools():Button[] { return [ - { btnList: new List([CollectionViewType.Freeform, CollectionViewType.Card, CollectionViewType.Carousel,CollectionViewType.Carousel3D, - CollectionViewType.Masonry, CollectionViewType.Multicolumn, CollectionViewType.Multirow, CollectionViewType.Linear, - CollectionViewType.Map, CollectionViewType.NoteTaking, CollectionViewType.Pivot, CollectionViewType.Schema, CollectionViewType.Stacking, - CollectionViewType.Calendar, CollectionViewType.Grid, CollectionViewType.Tree, CollectionViewType.Time, ]), - title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, shiftKey, _readOnly_); }'}}, + { title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, expertMode: false, btnList: new List(standardViewTypes), ignoreClick: true, width: 100, scripts: { script: '{ return setView(value, shiftKey, _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: 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_); }'} }, @@ -843,18 +841,19 @@ pie title Minerals in my tap water { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, { title: "Chat", icon: "lightbulb", toolTip: "Toggle Chat Assistant",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat", funcs: {}, width: 30, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} }, - { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string}, - { title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: CurrentUserUtils.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available - - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected - { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected - { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected - { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected + { title: "Filter", icon: "=", toolTip: "Filter cards by tags", btnType: ButtonType.MultiToggleButton, + subMenu: this.filterTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},ignoreClick:true, width: 30}, + { title: "Sort", icon: "Sort", toolTip: "Sort Documents", subMenu: this.sortTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: this.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: this.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: this.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "View", icon: "View", toolTip: "View tools", subMenu: this.viewTools(), expertMode: false, toolType:DocumentType.COL, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: this.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available + + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: this.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected + { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: this.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected + { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: this.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected + { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: this.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected ]; } @@ -900,7 +899,7 @@ pie title Minerals in my tap water static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); - const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); + const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } /// Initializes all the default buttons for the top bar context menu diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 37060d20c..94c023330 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -11,14 +11,13 @@ import { GroupManager } from '../util/GroupManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; -import { UndoManager, undoable } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { InkStrokeProperties } from './InkStrokeProperties'; import { MainView } from './MainView'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { CollectionFreeFormDocumentView } from './nodes/CollectionFreeFormDocumentView'; import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; @@ -242,7 +241,7 @@ export class KeyManager { { const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports); if (importBtn) { - MainView.Instance.selectMenu(importBtn); + MainView.Instance.selectLeftSidebarButton(importBtn); } } break; @@ -250,7 +249,7 @@ export class KeyManager { { const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails); if (trailsBtn) { - MainView.Instance.selectMenu(trailsBtn); + MainView.Instance.selectLeftSidebarButton(trailsBtn); } } break; @@ -260,7 +259,7 @@ export class KeyManager { } else { const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher); if (searchBtn) { - MainView.Instance.selectMenu(searchBtn); + MainView.Instance.selectLeftSidebarButton(searchBtn); } } break; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 2b2b77384..be6e2fecb 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -848,7 +848,7 @@ export class MainView extends ObservableReactComponent { } @action - selectMenu = (button: Doc) => { + selectLeftSidebarButton = (button: Doc) => { const title = StrCast(button.$title); const willOpen = !this._leftMenuFlyoutWidth || this._panelContent !== title; this.closeFlyout(); @@ -860,7 +860,9 @@ export class MainView extends ObservableReactComponent { case 'Help': break; default: - this.expandFlyout(button); + this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; + this._sidebarContent.proto = DocCast(button.target); + SnappingManager.SetLastPressedBtn(button[Id]); } } return true; @@ -936,19 +938,6 @@ export class MainView extends ObservableReactComponent { ); } - - expandFlyout = action((button: Doc) => { - // bcz: What's going on here!? --- may be fixed now, so commenting out ... - // Chrome(not firefox) seems to have a bug when the flyout expands and there's a zoomed freeform tab. All of the div below the CollectionFreeFormView's main div - // generate the wrong value from getClientRectangle() -- specifically they return an 'x' that is the flyout's width greater than it should be. - // interactively adjusting the flyout fixes the problem. So does programmatically changing the value after a timeout to something *fractionally* different (ie, 1.5, not 1);) - this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; - // setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); - - this._sidebarContent.proto = DocCast(button.target); - SnappingManager.SetLastPressedBtn(button[Id]); - }); - closeFlyout = action(() => { SnappingManager.SetLastPressedBtn(''); this._panelContent = 'none'; @@ -1162,7 +1151,7 @@ export class MainView extends ObservableReactComponent { // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function selectMainMenu(doc: Doc) { - MainView.Instance.selectMenu(doc); + MainView.Instance.selectLeftSidebarButton(doc); }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function hideUI() { diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index a1e8fe7ba..8f18de33f 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -118,7 +118,7 @@ export class PropertiesButtons extends React.Component { // select text return this.propertyToggleBtn( on => (on ? 'ALIGN TOP' : 'ALIGN CENTER'), - '_layout_centered', + 'text_centered', on => `${on ? 'Text is aligned with top of document' : 'Text is aligned with center of document'} `, () => // 'eye' ); diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7ba8989ce..6a7336bca 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -130,7 +130,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent func(CollectionViewType.Calendar), icon: 'columns' }, { description: 'Pivot', event: () => func(CollectionViewType.Pivot), icon: 'columns' }, { description: 'Time', event: () => func(CollectionViewType.Time), icon: 'columns' }, - { description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' }, { description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' }, ]; diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx index b40189d76..e4d38eb4a 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -11,7 +11,7 @@ import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { List } from '../../../../fields/List'; -import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { DocCast, ImageCast, ImageCastToNameType, NumCast, StrCast } from '../../../../fields/Types'; import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; @@ -187,7 +187,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent() { ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); })}> {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png']; + const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]) ?? ['-missing-', '.png']; return (
() { const imageInfos = this._selectedImages.map(async doc => { if (!doc.$tags_chat) { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); - return imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => + const url = ImageCastWithSuffix(doc[Doc.LayoutFieldKey(doc)], '_o') ?? ''; + return imageUrlToBase64(url).then(hrefBase64 => !hrefBase64 ? undefined : gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => ({ doc, labels }))) ; // prettier-ignore @@ -199,13 +199,11 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { groupImagesInBox = action(async () => { this.startLoading(); - for (const doc of this._selectedImages) { - for (let index = 0; index < (doc.$tags_chat as List).length; index++) { - const label = (doc.$tags_chat as List)[index]; - const embedding = await gptGetEmbedding(label); - doc[`$tags_embedding_${index + 1}`] = new List(embedding); - } - } + await Promise.all( + this._selectedImages + .map(doc => ({ doc, labels: doc.$tags_chat as List })) + .map(({ doc, labels }) => labels.map((label, index) => gptGetEmbedding(label).then(embedding => (doc[`$tags_embedding_${index + 1}`] = new List(embedding))))) + ); const labelToEmbedding = new Map(); // Create embeddings for the labels. @@ -312,7 +310,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent() { {this._displayImageInformation ? (
{this._selectedImages.map(doc => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + const [name, type] = ImageCastToNameType(doc[Doc.LayoutFieldKey(doc)]); return (
() { multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} toggleStatus={toggleStatus} - showUntilToggle={BoolCast(this.Document.showUntilToggle)} // allow the toggle to be clickable unless ignoreClick is set on the Document + showUntilToggle={BoolCast(this.Document.showUntilToggle)} label={selectedItems.length === 1 ? selectedItems[0] : this.label} items={items.map(item => ({ icon: , @@ -413,7 +413,7 @@ export class FontIconBox extends ViewBoxBaseComponent() { case ButtonType.TextButton: return